Add /api/certificates/request endpoint for programmatic certificate requests, update docs and add test script
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 37s
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 37s
This commit is contained in:
parent
7b0b4c0476
commit
ef488a253d
51
README.md
51
README.md
@ -205,6 +205,47 @@ Authorization: Bearer your-api-key
|
||||
|
||||
## New Certificate Management Endpoints
|
||||
|
||||
### Request Certificate Generation
|
||||
Request certificate generation for one or more domains.
|
||||
|
||||
```bash
|
||||
POST /api/certificates/request
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"domains": ["example.com", "api.example.com"],
|
||||
"force_renewal": false,
|
||||
"include_www": true
|
||||
}
|
||||
|
||||
# Response
|
||||
{
|
||||
"status": "completed",
|
||||
"summary": {
|
||||
"total": 2,
|
||||
"successful": 2,
|
||||
"failed": 0
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"domain": "example.com",
|
||||
"status": "success",
|
||||
"message": "Certificate obtained successfully",
|
||||
"cert_path": "/etc/haproxy/certs/example.com.pem",
|
||||
"domains_covered": ["example.com", "www.example.com"]
|
||||
},
|
||||
{
|
||||
"domain": "api.example.com",
|
||||
"status": "success",
|
||||
"message": "Certificate obtained successfully",
|
||||
"cert_path": "/etc/haproxy/certs/api.example.com.pem",
|
||||
"domains_covered": ["api.example.com"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Renew All Certificates
|
||||
Trigger renewal of all Let's Encrypt certificates and reload HAProxy.
|
||||
|
||||
@ -321,6 +362,16 @@ curl -X POST http://localhost:8000/api/ssl \
|
||||
curl -X POST http://localhost:8000/api/certificates/renew \
|
||||
-H "Authorization: Bearer your-secure-api-key-here"
|
||||
|
||||
# Request certificate generation for another service
|
||||
curl -X POST http://localhost:8000/api/certificates/request \
|
||||
-H "Authorization: Bearer your-secure-api-key-here" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"domains": ["api.example.com"],
|
||||
"force_renewal": false,
|
||||
"include_www": false
|
||||
}'
|
||||
|
||||
# Download certificate for another service
|
||||
curl -H "Authorization: Bearer your-secure-api-key-here" \
|
||||
http://localhost:8000/api/certificates/example.com/download \
|
||||
|
@ -23,7 +23,19 @@ This document summarizes the new features and improvements added to the HAProxy
|
||||
- Returns detailed status of renewal process
|
||||
- **Error Handling**: Comprehensive error logging and status reporting
|
||||
|
||||
### 3. Certificate Download Endpoints
|
||||
### 3. Certificate Request API
|
||||
- **Endpoint**: `POST /api/certificates/request`
|
||||
- **Functionality**:
|
||||
- Request certificate generation for one or more domains
|
||||
- Support for multiple domains in a single request
|
||||
- Optional www subdomain inclusion
|
||||
- Force renewal option
|
||||
- Automatic domain addition to database if not exists
|
||||
- Batch processing with detailed results
|
||||
- **Use Case**: Allow other services to request certificate generation through the HAProxy service
|
||||
- **Response**: Detailed status for each domain with success/failure information
|
||||
|
||||
### 4. Certificate Download Endpoints
|
||||
- **Endpoints**:
|
||||
- `GET /api/certificates/<domain>/download` - Combined certificate (cert + key)
|
||||
- `GET /api/certificates/<domain>/key` - Private key only
|
||||
@ -31,7 +43,7 @@ This document summarizes the new features and improvements added to the HAProxy
|
||||
- **Use Case**: Allow other services to securely download certificates for their own use
|
||||
- **Security**: All endpoints require API key authentication
|
||||
|
||||
### 4. Certificate Status Monitoring
|
||||
### 5. Certificate Status Monitoring
|
||||
- **Endpoint**: `GET /api/certificates/status`
|
||||
- **Functionality**:
|
||||
- Lists all certificates with expiration dates
|
||||
@ -39,7 +51,7 @@ This document summarizes the new features and improvements added to the HAProxy
|
||||
- Provides certificate file paths
|
||||
- Enables proactive certificate management
|
||||
|
||||
### 5. Comprehensive Error Logging and Alerting
|
||||
### 6. Comprehensive Error Logging and Alerting
|
||||
- **Logging System**:
|
||||
- Structured JSON logging for all operations
|
||||
- Separate error log file (`/var/log/haproxy-manager-errors.log`)
|
||||
@ -143,6 +155,7 @@ This document summarizes the new features and improvements added to the HAProxy
|
||||
- `GET /api/reload` - Reload HAProxy
|
||||
|
||||
### New Endpoints
|
||||
- `POST /api/certificates/request` - Request certificate generation for domains
|
||||
- `POST /api/certificates/renew` - Renew all certificates
|
||||
- `GET /api/certificates/status` - Get certificate status
|
||||
- `GET /api/certificates/<domain>/download` - Download combined certificate
|
||||
|
@ -289,6 +289,7 @@ def index():
|
||||
@app.route('/api/ssl', methods=['POST'])
|
||||
@require_api_key
|
||||
def request_ssl():
|
||||
"""Legacy endpoint for requesting SSL certificate for a single domain"""
|
||||
data = request.get_json()
|
||||
domain = data.get('domain')
|
||||
|
||||
@ -310,6 +311,9 @@ def request_ssl():
|
||||
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem'
|
||||
combined_path = f'{SSL_CERTS_DIR}/{domain}.pem'
|
||||
|
||||
# Ensure SSL certs directory exists
|
||||
os.makedirs(SSL_CERTS_DIR, exist_ok=True)
|
||||
|
||||
with open(combined_path, 'w') as combined:
|
||||
subprocess.run(['cat', cert_path, key_path], stdout=combined)
|
||||
|
||||
@ -326,7 +330,12 @@ def request_ssl():
|
||||
conn.close()
|
||||
generate_config()
|
||||
log_operation('request_ssl', True, f'SSL certificate obtained for {domain}')
|
||||
return jsonify({'status': 'success'})
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'domain': domain,
|
||||
'cert_path': combined_path,
|
||||
'message': 'Certificate obtained successfully'
|
||||
})
|
||||
else:
|
||||
error_msg = f'Failed to obtain SSL certificate: {result.stderr}'
|
||||
log_operation('request_ssl', False, error_msg)
|
||||
@ -491,6 +500,137 @@ def get_certificate_status():
|
||||
log_operation('get_certificate_status', False, str(e))
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
@app.route('/api/certificates/request', methods=['POST'])
|
||||
@require_api_key
|
||||
def request_certificates():
|
||||
"""Request certificate generation for one or more domains"""
|
||||
data = request.get_json()
|
||||
domains = data.get('domains', [])
|
||||
force_renewal = data.get('force_renewal', False)
|
||||
include_www = data.get('include_www', True)
|
||||
|
||||
if not domains:
|
||||
log_operation('request_certificates', False, 'No domains provided')
|
||||
return jsonify({'status': 'error', 'message': 'At least one domain is required'}), 400
|
||||
|
||||
if not isinstance(domains, list):
|
||||
domains = [domains] # Convert single domain to list
|
||||
|
||||
results = []
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for domain in domains:
|
||||
try:
|
||||
# Prepare domain list for certbot (include www subdomain if requested)
|
||||
certbot_domains = [domain]
|
||||
if include_www and not domain.startswith('www.'):
|
||||
certbot_domains.append(f'www.{domain}')
|
||||
|
||||
# Build certbot command
|
||||
cmd = [
|
||||
'certbot', 'certonly', '-n', '--standalone',
|
||||
'--preferred-challenges', 'http', '--http-01-port=8688'
|
||||
]
|
||||
|
||||
if force_renewal:
|
||||
cmd.append('--force-renewal')
|
||||
|
||||
# Add domains
|
||||
for d in certbot_domains:
|
||||
cmd.extend(['-d', d])
|
||||
|
||||
# Request certificate
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Combine cert files and copy to HAProxy certs directory
|
||||
cert_path = f'/etc/letsencrypt/live/{domain}/fullchain.pem'
|
||||
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem'
|
||||
combined_path = f'{SSL_CERTS_DIR}/{domain}.pem'
|
||||
|
||||
# Ensure SSL certs directory exists
|
||||
os.makedirs(SSL_CERTS_DIR, exist_ok=True)
|
||||
|
||||
with open(combined_path, 'w') as combined:
|
||||
subprocess.run(['cat', cert_path, key_path], stdout=combined)
|
||||
|
||||
# Update database (add domain if it doesn't exist)
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if domain exists
|
||||
cursor.execute('SELECT id FROM domains WHERE domain = ?', (domain,))
|
||||
domain_exists = cursor.fetchone()
|
||||
|
||||
if domain_exists:
|
||||
# Update existing domain
|
||||
cursor.execute('''
|
||||
UPDATE domains
|
||||
SET ssl_enabled = 1, ssl_cert_path = ?
|
||||
WHERE domain = ?
|
||||
''', (combined_path, domain))
|
||||
else:
|
||||
# Add new domain with SSL enabled
|
||||
cursor.execute('''
|
||||
INSERT INTO domains (domain, ssl_enabled, ssl_cert_path)
|
||||
VALUES (?, 1, ?)
|
||||
''', (domain, combined_path))
|
||||
|
||||
results.append({
|
||||
'domain': domain,
|
||||
'status': 'success',
|
||||
'message': 'Certificate obtained successfully',
|
||||
'cert_path': combined_path,
|
||||
'domains_covered': certbot_domains
|
||||
})
|
||||
success_count += 1
|
||||
|
||||
else:
|
||||
error_msg = f'Failed to obtain certificate for {domain}: {result.stderr}'
|
||||
results.append({
|
||||
'domain': domain,
|
||||
'status': 'error',
|
||||
'message': error_msg,
|
||||
'stderr': result.stderr
|
||||
})
|
||||
error_count += 1
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'Exception while processing {domain}: {str(e)}'
|
||||
results.append({
|
||||
'domain': domain,
|
||||
'status': 'error',
|
||||
'message': error_msg
|
||||
})
|
||||
error_count += 1
|
||||
|
||||
# Regenerate HAProxy config if any certificates were successful
|
||||
if success_count > 0:
|
||||
try:
|
||||
generate_config()
|
||||
log_operation('request_certificates', True, f'Successfully obtained {success_count} certificates, {error_count} failed')
|
||||
except Exception as e:
|
||||
log_operation('request_certificates', False, f'Certificates obtained but config generation failed: {str(e)}')
|
||||
|
||||
# Return results
|
||||
response = {
|
||||
'status': 'completed',
|
||||
'summary': {
|
||||
'total': len(domains),
|
||||
'successful': success_count,
|
||||
'failed': error_count
|
||||
},
|
||||
'results': results
|
||||
}
|
||||
|
||||
if error_count == 0:
|
||||
return jsonify(response), 200
|
||||
elif success_count > 0:
|
||||
return jsonify(response), 207 # Multi-status (some succeeded, some failed)
|
||||
else:
|
||||
return jsonify(response), 500 # All failed
|
||||
|
||||
@app.route('/api/domain', methods=['DELETE'])
|
||||
@require_api_key
|
||||
def remove_domain():
|
||||
|
186
scripts/test-certificate-request.sh
Executable file
186
scripts/test-certificate-request.sh
Executable file
@ -0,0 +1,186 @@
|
||||
#!/bin/bash
|
||||
|
||||
# HAProxy Manager Certificate Request Test Script
|
||||
# This script tests the new certificate request endpoint
|
||||
|
||||
BASE_URL="http://localhost:8000"
|
||||
API_KEY="${HAPROXY_API_KEY:-}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
local status=$1
|
||||
local message=$2
|
||||
|
||||
case $status in
|
||||
"PASS")
|
||||
echo -e "${GREEN}✓ PASS${NC}: $message"
|
||||
;;
|
||||
"FAIL")
|
||||
echo -e "${RED}✗ FAIL${NC}: $message"
|
||||
;;
|
||||
"INFO")
|
||||
echo -e "${BLUE}ℹ INFO${NC}: $message"
|
||||
;;
|
||||
"WARN")
|
||||
echo -e "${YELLOW}⚠ WARN${NC}: $message"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to make API request
|
||||
api_request() {
|
||||
local method=$1
|
||||
local endpoint=$2
|
||||
local data=$3
|
||||
|
||||
local headers=""
|
||||
if [ -n "$API_KEY" ]; then
|
||||
headers="-H \"Authorization: Bearer $API_KEY\""
|
||||
fi
|
||||
|
||||
if [ -n "$data" ]; then
|
||||
headers="$headers -H \"Content-Type: application/json\" -d '$data'"
|
||||
fi
|
||||
|
||||
eval "curl -s -w \"%{http_code}\" -o /tmp/cert_request_response.json $headers -X $method $BASE_URL$endpoint"
|
||||
}
|
||||
|
||||
# Test single domain certificate request
|
||||
test_single_domain_request() {
|
||||
print_status "INFO" "Testing single domain certificate request..."
|
||||
|
||||
local test_domain="test-$(date +%s).example.com"
|
||||
local data="{\"domains\": [\"$test_domain\"], \"force_renewal\": false, \"include_www\": false}"
|
||||
|
||||
local response=$(api_request "POST" "/api/certificates/request" "$data")
|
||||
local status_code=$(echo "$response" | tail -c 4)
|
||||
|
||||
if [ "$status_code" = "200" ] || [ "$status_code" = "207" ] || [ "$status_code" = "401" ]; then
|
||||
print_status "PASS" "Single domain request endpoint responded (status: $status_code)"
|
||||
|
||||
if [ "$status_code" != "401" ]; then
|
||||
# Parse response
|
||||
local success_count=$(jq -r '.summary.successful' /tmp/cert_request_response.json 2>/dev/null)
|
||||
local failed_count=$(jq -r '.summary.failed' /tmp/cert_request_response.json 2>/dev/null)
|
||||
|
||||
if [ "$success_count" = "1" ]; then
|
||||
print_status "PASS" "Certificate request successful for $test_domain"
|
||||
elif [ "$failed_count" = "1" ]; then
|
||||
print_status "WARN" "Certificate request failed for $test_domain (expected for test domain)"
|
||||
else
|
||||
print_status "FAIL" "Unexpected response format"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_status "FAIL" "Single domain request failed with status $status_code"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test multiple domain certificate request
|
||||
test_multiple_domain_request() {
|
||||
print_status "INFO" "Testing multiple domain certificate request..."
|
||||
|
||||
local test_domains="[\"test1-$(date +%s).example.com\", \"test2-$(date +%s).example.com\"]"
|
||||
local data="{\"domains\": $test_domains, \"force_renewal\": false, \"include_www\": true}"
|
||||
|
||||
local response=$(api_request "POST" "/api/certificates/request" "$data")
|
||||
local status_code=$(echo "$response" | tail -c 4)
|
||||
|
||||
if [ "$status_code" = "200" ] || [ "$status_code" = "207" ] || [ "$status_code" = "401" ]; then
|
||||
print_status "PASS" "Multiple domain request endpoint responded (status: $status_code)"
|
||||
|
||||
if [ "$status_code" != "401" ]; then
|
||||
local total=$(jq -r '.summary.total' /tmp/cert_request_response.json 2>/dev/null)
|
||||
if [ "$total" = "2" ]; then
|
||||
print_status "PASS" "Multiple domain request processed correctly"
|
||||
else
|
||||
print_status "FAIL" "Multiple domain request response format error"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_status "FAIL" "Multiple domain request failed with status $status_code"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test certificate request with force renewal
|
||||
test_force_renewal_request() {
|
||||
print_status "INFO" "Testing certificate request with force renewal..."
|
||||
|
||||
local test_domain="test-force-$(date +%s).example.com"
|
||||
local data="{\"domains\": [\"$test_domain\"], \"force_renewal\": true, \"include_www\": false}"
|
||||
|
||||
local response=$(api_request "POST" "/api/certificates/request" "$data")
|
||||
local status_code=$(echo "$response" | tail -c 4)
|
||||
|
||||
if [ "$status_code" = "200" ] || [ "$status_code" = "207" ] || [ "$status_code" = "401" ]; then
|
||||
print_status "PASS" "Force renewal request endpoint responded (status: $status_code)"
|
||||
else
|
||||
print_status "FAIL" "Force renewal request failed with status $status_code"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test invalid request (no domains)
|
||||
test_invalid_request() {
|
||||
print_status "INFO" "Testing invalid request (no domains)..."
|
||||
|
||||
local data="{\"domains\": [], \"force_renewal\": false, \"include_www\": false}"
|
||||
|
||||
local response=$(api_request "POST" "/api/certificates/request" "$data")
|
||||
local status_code=$(echo "$response" | tail -c 4)
|
||||
|
||||
if [ "$status_code" = "400" ] || [ "$status_code" = "401" ]; then
|
||||
print_status "PASS" "Invalid request properly rejected (status: $status_code)"
|
||||
else
|
||||
print_status "FAIL" "Invalid request not properly rejected (status: $status_code)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test certificate status endpoint
|
||||
test_certificate_status() {
|
||||
print_status "INFO" "Testing certificate status endpoint..."
|
||||
|
||||
local response=$(api_request "GET" "/api/certificates/status")
|
||||
local status_code=$(echo "$response" | tail -c 4)
|
||||
|
||||
if [ "$status_code" = "200" ] || [ "$status_code" = "401" ]; then
|
||||
print_status "PASS" "Certificate status endpoint responded (status: $status_code)"
|
||||
|
||||
if [ "$status_code" != "401" ]; then
|
||||
local cert_count=$(jq -r '.certificates | length' /tmp/cert_request_response.json 2>/dev/null)
|
||||
print_status "INFO" "Found $cert_count certificates in status"
|
||||
fi
|
||||
else
|
||||
print_status "FAIL" "Certificate status failed with status $status_code"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main test execution
|
||||
main() {
|
||||
echo "HAProxy Manager Certificate Request Test Suite"
|
||||
echo "=============================================="
|
||||
echo "Base URL: $BASE_URL"
|
||||
echo "API Key: ${API_KEY:-"Not configured"}"
|
||||
echo ""
|
||||
|
||||
test_invalid_request
|
||||
test_single_domain_request
|
||||
test_multiple_domain_request
|
||||
test_force_renewal_request
|
||||
test_certificate_status
|
||||
|
||||
echo ""
|
||||
echo "Test completed. Check /tmp/cert_request_response.json for detailed responses."
|
||||
echo ""
|
||||
echo "Note: Certificate requests for test domains will likely fail as they don't"
|
||||
echo "resolve to this server. This is expected behavior for testing."
|
||||
}
|
||||
|
||||
# Run tests
|
||||
main "$@"
|
Loading…
x
Reference in New Issue
Block a user