-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add experimental HTTP template server #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
There was a problem hiding this 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
.tmpltemplate 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"> |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| <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"> |
| timeout server 1m | ||
| timeout connect 10s | ||
| timeout http-keep-alive 2m | ||
| timeout queue 15s | ||
| timeout tunnel 4h # for websocket |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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).
| 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 |
| 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
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| 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 | |
| } |
| } else { | ||
| console.error('Captcha response not found after maximum retries, submitting anyway'); | ||
| form.submit(); // Submit anyway as fallback |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| ## 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 |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| 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 |
| <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"> |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| <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"> |
| 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
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| 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; | |
| } | |
| }); |
| } | ||
|
|
||
| // 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) |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| // 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) |
| // 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) |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| // 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) |
| volumes: | ||
| - sockets:/run/ | ||
| - geodb:/var/lib/crowdsec/data/ | ||
| - ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| - ./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/ |
|
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.
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:
Changes:
This is an experimental feature and must be explicitly enabled via http_template_server.enabled in the configuration.