Skip to content

Conversation

@LaurenceJJones
Copy link
Member

Add experimental HTTP template server feature that allows users to render ban and captcha templates via HTTP endpoint instead of using Lua scripts.

Key features:

  • HTTP server with configurable listen address/port and TLS support
  • Template rendering using Go's native text/template package
  • Host configuration lookup using Host header from HAProxy
  • Support for ban and captcha remediation types
  • Catch-all route for simplified HAProxy configuration
  • HEAD requests return 403 for ban/captcha remediations

Changes:

  • Add HTTPTemplateServerConfig to bouncer configuration
  • Implement HTTP template server in pkg/httptemplate
  • Add Go template renderer in pkg/template
  • Create .tmpl template files for ban and captcha
  • Add GetProviderInfo helper in captcha package
  • Update Dockerfile to create log directory and copy templates
  • Add HAProxy configuration example for HTTP template server
  • Update all config files to use canonical header names (X-Crowdsec-*)

This is an experimental feature and must be explicitly enabled via http_template_server.enabled in the configuration.

Add experimental HTTP template server feature that allows users to render
ban and captcha templates via HTTP endpoint instead of using Lua scripts.

Key features:
- HTTP server with configurable listen address/port and TLS support
- Template rendering using Go's native text/template package
- Host configuration lookup using Host header from HAProxy
- Support for ban and captcha remediation types
- Catch-all route for simplified HAProxy configuration
- HEAD requests return 403 for ban/captcha remediations

Changes:
- Add HTTPTemplateServerConfig to bouncer configuration
- Implement HTTP template server in pkg/httptemplate
- Add Go template renderer in pkg/template
- Create .tmpl template files for ban and captcha
- Add GetProviderInfo helper in captcha package
- Update Dockerfile to create log directory and copy templates
- Add HAProxy configuration example for HTTP template server
- Update all config files to use canonical header names (X-Crowdsec-*)

This is an experimental feature and must be explicitly enabled via
http_template_server.enabled in the configuration.
Remove full header map from log entry to prevent exposure of sensitive
data such as cookies, authorization tokens, etc. Log only the essential
message that remediation header was not found.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request adds an experimental HTTP template server feature that enables HAProxy to render ban and captcha pages through HTTP endpoints instead of Lua scripts. The server leverages Go's native text/template package for rendering and provides a RESTful alternative to the existing Lua-based approach.

Key Changes:

  • Implements HTTP template server with configurable listen address/port and optional TLS support
  • Adds Go template renderer and .tmpl template files for ban and captcha pages
  • Updates all HAProxy configuration files to use canonical header naming convention (X-Crowdsec-*)

Reviewed Changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
templates/captcha.tmpl New Go template for captcha page with dark mode support and retry logic
templates/ban.tmpl New Go template for ban page with dark mode support
pkg/template/renderer.go Template rendering wrapper using Go's text/template package
pkg/httptemplate/server.go HTTP server implementation handling ban/captcha rendering and host configuration lookup
pkg/cfg/config.go Configuration structures for HTTP template server including TLS settings
internal/remediation/captcha/providers.go Added GetProviderInfo helper to retrieve provider-specific frontend configuration
cmd/root.go Integration of HTTP template server into main application lifecycle
Dockerfile Updated to copy new .tmpl templates and standardize directory paths
debian/rules Added installation of .tmpl template files
rpm/SPECS/crowdsec-haproxy-spoa-bouncer.spec Added .tmpl template files to package
config/haproxy-httptemplate.cfg New HAProxy configuration example demonstrating HTTP template server usage
config/haproxy.cfg Updated header names to canonical format (X-Crowdsec-*)
config/haproxy-upstreamproxy.cfg Updated header names to canonical format (X-Crowdsec-*)
config/crowdsec-spoa-bouncer.yaml Added HTTP template server configuration section
docker-compose.yaml Updated volume paths to match standardized directory structure
docker-compose.proxy-test.yaml Updated volume paths to match standardized directory structure
docker-compose.httptemplate-test.yaml New Docker Compose configuration for testing HTTP template server

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

<div class="flex flex-col justify-between items-center border-2 border-black dark:border-white rounded-xl p-4 h-3/5 md:h-2/5 w-full sm:w-2/3 lg:w-1/2">
<div class="fixed p-4 top-0 right-0 dark-mode-toggle"></div>
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" class="warning">
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate class attribute on the SVG element. The class "warning" is redundant since there's already a class attribute with "h-24 w-24". Either merge them into one class attribute or remove the duplicate.

Suggested change
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" class="warning">
<svg fill="black" class="h-24 w-24 warning" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +16
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent use of tabs and spaces for indentation. Lines 12-16 use tabs, but line 11 and others in the file appear to use spaces. The file should use consistent indentation throughout (either all tabs or all spaces).

Suggested change
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket

Copilot uses AI. Check for mistakes.
Comment on lines +165 to +169
if matchedHost == nil {
s.logger.Error("captcha remediation but no host configuration found")
http.Error(w, "Internal Server Error: No host configuration for captcha", http.StatusInternalServerError)
return
}
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling logic is inconsistent. When matchedHost is nil for a captcha remediation at line 137-140, the code logs a warning and returns an error. However, at line 165-168, the same condition triggers an error log with a different message. The first check should be sufficient and the second check is redundant. Consider removing lines 165-168 or clarifying why both checks are needed.

Suggested change
if matchedHost == nil {
s.logger.Error("captcha remediation but no host configuration found")
http.Error(w, "Internal Server Error: No host configuration for captcha", http.StatusInternalServerError)
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +131
} else {
console.error('Captcha response not found after maximum retries, submitting anyway');
form.submit(); // Submit anyway as fallback
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The setTimeout creates a closure that retries with exponential backoff, but if the form submission ultimately fails after all retries (line 131), it still submits the form. This could result in an invalid captcha submission being sent to the server. Consider logging an error to the console or displaying a user-friendly error message instead of submitting with no captcha response.

Copilot uses AI. Check for mistakes.
## Copy .tmpl files explicitly to ensure they're included
RUN mkdir -p /var/lib/crowdsec-haproxy-spoa-bouncer/html
COPY --from=build /go/src/cs-spoa-bouncer/templates/ban.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.tmpl
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.tmpl
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dockerfile now only copies .tmpl template files explicitly, but the old Dockerfile copied all templates with templates/* which would have included ban.html and captcha.html. These HTML templates are still needed for Lua-based rendering (as evidenced by the HAProxy configs that set CROWDSEC_BAN_TEMPLATE_PATH to .html files). Either add explicit COPY commands for the .html templates, or revert to using templates/* to copy all template files.

Suggested change
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.tmpl
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.tmpl
COPY --from=build /go/src/cs-spoa-bouncer/templates/ban.html /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.html
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.html /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.html

Copilot uses AI. Check for mistakes.
<div class="flex flex-col justify-between items-center border-2 border-black dark:border-white rounded-xl p-4 h-3/5 md:h-2/5 w-full sm:w-2/3 lg:w-1/2">
<div class="fixed p-4 top-0 right-0 dark-mode-toggle"></div>
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" class="warning">
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate class attribute on the SVG element. The class "warning" is redundant since there's already a class attribute with "h-24 w-24". Either merge them into one class attribute or remove the duplicate.

Suggested change
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" class="warning">
<svg fill="black" class="h-24 w-24 warning" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +97
document.body.classList.toggle('dark');
svg.forEach(element => {
element.getAttribute('fill') === 'black' ? element.setAttribute('fill', 'white') : element.setAttribute('fill', 'black');
});
if (document.body.classList.contains('dark')) {
button.innerHTML = sunSvgString;
} else {
button.innerHTML = moonSvgString;
}
});
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation in the event listener callback. Lines 87-88 use 2 spaces but line 86 properly uses 4 spaces (two levels of indentation). The code should use consistent 2-space indentation throughout the callback.

Suggested change
document.body.classList.toggle('dark');
svg.forEach(element => {
element.getAttribute('fill') === 'black' ? element.setAttribute('fill', 'white') : element.setAttribute('fill', 'black');
});
if (document.body.classList.contains('dark')) {
button.innerHTML = sunSvgString;
} else {
button.innerHTML = moonSvgString;
}
});
document.body.classList.toggle('dark');
svg.forEach(element => {
element.getAttribute('fill') === 'black' ? element.setAttribute('fill', 'white') : element.setAttribute('fill', 'black');
});
if (document.body.classList.contains('dark')) {
button.innerHTML = sunSvgString;
} else {
button.innerHTML = moonSvgString;
}
});

Copilot uses AI. Check for mistakes.
}

// handleRender is the main endpoint that handles both ban and captcha rendering
// It reads the remediation type from X-CrowdSec-Remediation header (set by HAProxy)
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment references "X-CrowdSec-Remediation" (with capital 'S') but the actual code uses "X-Crowdsec-Remediation" (lowercase 's' after 'crowd'). The comment should be updated to match the actual header name used in the code for consistency.

Suggested change
// It reads the remediation type from X-CrowdSec-Remediation header (set by HAProxy)
// It reads the remediation type from X-Crowdsec-Remediation header (set by HAProxy)

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +29
// NewRenderer creates a new template renderer with the given template content
func NewRenderer(templateContent string) (*Renderer, error) {
tmpl, err := template.New("template").Parse(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The template name is hardcoded as "template" which is not descriptive. Consider using a more meaningful name (e.g., the name parameter passed to NewRenderer) to improve error messages and debugging.

Suggested change
// NewRenderer creates a new template renderer with the given template content
func NewRenderer(templateContent string) (*Renderer, error) {
tmpl, err := template.New("template").Parse(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
// NewRenderer creates a new template renderer with the given template name and content
func NewRenderer(name string, templateContent string) (*Renderer, error) {
tmpl, err := template.New(name).Parse(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to parse template %q: %w", name, err)

Copilot uses AI. Check for mistakes.
volumes:
- sockets:/run/
- geodb:/var/lib/crowdsec/data/
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spoa service doesn't mount template volumes (unlike the other docker-compose files which mount templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/). The HTTP template server needs access to .tmpl template files. Either add the volume mount or ensure the templates are baked into the Docker image.

Suggested change
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/

Copilot uses AI. Check for mistakes.
@LaurenceJJones LaurenceJJones added this to the 0.2.1 milestone Nov 18, 2025
@LaurenceJJones
Copy link
Member Author

link #107

Return 403 Forbidden for HEAD requests and favicon (.ico) file requests
instead of rendering templates. This prevents unnecessary template rendering
for these request types and provides a cleaner response.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants