Skip to content

feat: Support HTTP web app#310

Open
clement0010 wants to merge 15 commits into
masterfrom
feat/ct/support-http-web-app
Open

feat: Support HTTP web app#310
clement0010 wants to merge 15 commits into
masterfrom
feat/ct/support-http-web-app

Conversation

@clement0010
Copy link
Copy Markdown
Contributor

@clement0010 clement0010 commented May 28, 2026

Related Tickets & Documents

Changes

  • Add new webapphandler package to handle HTTP web application
    • Add web app configuration and validation
    • Add template utility to parse and evaluate prefix {{twingate.<key>}} suffix template
  • Supporting running one HTTP proxy per resource type and update connect package's channel-based routing by ResourceType instead of TransportProtocol type
  • Add resourceType label to roundtripper and http middleware metrics

Notes

🤖 Generated with Claude Code

@clement0010 clement0010 force-pushed the feat/ct/support-http-web-app branch from b7034ad to add758a Compare May 28, 2026 14:05
@clement0010 clement0010 changed the title fix: Use omitzero instead of omitempty for Device.Location JSON tag feat: Support HTTP web app May 28, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

Codecov Report

❌ Patch coverage is 95.71429% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.35%. Comparing base (518c321) to head (289c6cf).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
internal/proxy/proxy.go 79.54% 7 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #310      +/-   ##
==========================================
+ Coverage   85.71%   86.35%   +0.63%     
==========================================
  Files          37       40       +3     
  Lines        2716     2828     +112     
==========================================
+ Hits         2328     2442     +114     
+ Misses        264      262       -2     
  Partials      124      124              
Flag Coverage Δ
integration 52.88% <52.38%> (-1.25%) ⬇️
unit 78.53% <90.95%> (+0.29%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/config/config.go 89.29% <100.00%> (+0.49%) ⬆️
internal/connect/conn.go 83.63% <100.00%> (+3.08%) ⬆️
internal/connect/connect.go 97.43% <100.00%> (+0.03%) ⬆️
internal/connect/listener.go 88.17% <100.00%> (+2.15%) ⬆️
internal/httpproxy/proxy.go 100.00% <100.00%> (ø)
internal/kuberneteshandler/kubernetes.go 94.02% <ø> (ø)
internal/metrics/http_middleware.go 98.83% <100.00%> (+0.10%) ⬆️
internal/metrics/round_tripper.go 100.00% <100.00%> (ø)
internal/token/gat_claims.go 100.00% <100.00%> (ø)
internal/webapphandler/config.go 100.00% <100.00%> (ø)
... and 3 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
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

Adds support for HTTP web app resources alongside the existing Kubernetes and SSH handlers. The HTTP proxy now dispatches requests to handlers by GAT resource.type, a new webapphandler package implements a reverse proxy that propagates identity to upstream via configurable header templates, and the second TLS upgrade is skipped for WEB_APP.

Changes:

  • Introduce internal/webapphandler (reverse proxy + header template config) and a generic internal/utils/parser for {{twingate.var}} placeholders.
  • Refactor httpproxy to a resource-type-keyed handler map and wire web app + Kubernetes through it; proxy.NewProxy builds the map.
  • Extend token.GATClaims with WEB_APP resource type and Device.Location.GeoIP; extend ProxyConn with raw bearer Token, GetToken(), and ShouldUpgradeTLS() (skips inner TLS for web app); add WebAppConfig to gateway YAML config.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
internal/webapphandler/web_app.go New reverse proxy that targets http://<address> and applies configured header templates.
internal/webapphandler/web_app_test.go Tests rewrite header substitution and geo handling.
internal/webapphandler/web_app_config.go Parses raw header templates and restricts them to an allowlist of variables.
internal/webapphandler/web_app_config_test.go Tests config parsing, unsupported variable, and invalid syntax cases.
internal/utils/parser/parser.go New {{twingate.var}} single-variable template parser/evaluator.
internal/utils/parser/parser_test.go Covers parser and evaluator behavior including error paths.
internal/token/gat_claims.go Adds GeoIP/DeviceLocation types and ResourceTypeWebApp.
internal/token/gat_claims_test.go Extends fixture with device location data.
internal/proxy/proxy.go Builds handler map; unconditionally registers round-tripper metrics; wires WebApp handler.
internal/proxy/proxy_test.go Updates test for new Handlers map field.
internal/metrics/round_tripper.go Adds resourceType label and a resourceType arg to InstrumentRoundTripper.
internal/metrics/round_tripper_test.go Updates expected labels and exercises multiple resource types.
internal/metrics/http_middleware.go Adds labelResourceType constant.
internal/kuberneteshandler/kubernetes.go Passes "kubernetes" to instrumented round tripper.
internal/httpproxy/proxy.go Switches Config.HandlerHandlers map; introduces newResourceRouter.
internal/httpproxy/proxy_test.go Tests resource dispatch and unknown-resource 404.
internal/connect/listener.go Uses ShouldUpgradeTLS() instead of inline tp != TransportSSH check.
internal/connect/listener_test.go Extends mock with GetToken and ShouldUpgradeTLS.
internal/connect/connect.go Returns bearer Token in Info.
internal/connect/connect_test.go Asserts connectInfo.Token is populated/empty appropriately.
internal/connect/conn.go Stores token on ProxyConn, exposes GetToken/ShouldUpgradeTLS, logs resource type.
internal/connect/conn_test.go Adds ShouldUpgradeTLS matrix test.
internal/config/config.go Adds WebAppConfig and includes WebApp in the "at least one protocol" validation.

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

Comment thread internal/webapphandler/handler.go
Comment thread internal/webapphandler/web_app.go Outdated
Comment thread internal/proxy/proxy.go Outdated
Comment thread internal/utils/parser/parser.go Outdated
Comment thread internal/webapphandler/web_app.go Outdated
Comment thread internal/webapphandler/web_app.go Outdated
Comment thread internal/config/config.go
Comment thread internal/config/config.go
Comment thread internal/webapphandler/web_app_test.go Outdated
@clement0010 clement0010 force-pushed the feat/ct/support-http-web-app branch 2 times, most recently from 6953252 to 4c2a306 Compare May 29, 2026 02:44
Comment thread internal/proxy/proxy.go
Comment on lines +53 to +55
if config.Kubernetes != nil || config.WebApp != nil {
roundTripperMetrics = metrics.RegisterRoundTripperMetrics(registry)
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When we have 3+ HTTP resource types, I think we should refactor the config to register the handler + metrics more nicely and avoid this check... It's out of the scope of this PR for now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be simpler if each resource type has its own HTTP server + proxy handler instead of all them sharing the same HTTP server? In that case, I think the HTTP proxy could still stay generic and doesn't have to know about the resource type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I updated the code to support each resource type to own its HTTP server. e7ceee9. This should simplify the setup quite a bit

Comment thread internal/httpproxy/template/template.go Outdated
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't use Go's text/template package because it doesn't support the {{namespace.key}} syntax — it requires a leading dot (e.g., {{ .twingate.key}}).

Copy link
Copy Markdown
Contributor

@minhtule minhtule May 31, 2026

Choose a reason for hiding this comment

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

I think this parser should be in webapphandler than here. Also it doesn't need to be under /utils/parser path. I would put it in internal/webapphandler/template.go

Copy link
Copy Markdown
Contributor Author

@clement0010 clement0010 Jun 2, 2026

Choose a reason for hiding this comment

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

My thinking is that this parser could also be shared with other protocols (e.g. MCP) that support header rewrite or other applications that need to parse a template.

We can move this under webapphandler for now, but I think we might still need to make this a shared utility package eventually 🤔

Comment thread internal/httpproxy/template/template.go Outdated
)

const (
allowedNamespace = "twingate"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We can extend this to support more namespaces like internal and external in the future, but I don't think we need to do it for now 🤔

`\.` +
`([a-zA-Z0-9_-]+)` + // key
`\s*}}` + // closing braces
`([^{}]*)$`, // suffix (no brackets allowed)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FYI, This won't allow header templates with multiple variables like X-Header: {{twingate.username}} {{twingate.groups}}. Not sure there's a real use case for it 🤔

We could support it with some additional changes, but I'd rather not add complexity to the parser unless we need to. We can follow-up PR if a use case comes up.

@clement0010 clement0010 requested a review from minhtule May 29, 2026 02:49
Name: "api_server_requests_total",
Help: "Total number of requests from Gateway to API Server processed",
}, []string{"type", "method", "code"}),
}, []string{labelResourceType, "type", "method", "code"}),
Copy link
Copy Markdown
Contributor Author

@clement0010 clement0010 May 29, 2026

Choose a reason for hiding this comment

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

We also need to update the grafana-dashboard.json file to support this new label, i'll follow up with a separate PR instead

…bernetes-access-gateway into feat/ct/support-http-web-app
Comment thread internal/connect/conn.go Outdated
Comment on lines +111 to +113
func (p *ProxyConn) ShouldUpgradeTLS() bool {
return p.TransportProtocol() == TransportTLS && p.GATClaims().Resource.Type != token.ResourceTypeWebApp
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is okayish for now but I think eventually this logic should be on the GAT's resource. It knows whether the inner connection should be not encrypted or TLS/SSH.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After updating the code to HTTP proxy per resource type, I think it's simpler to have this check on the GAT claims. Also, we could remove the TransportProtocol type to simplify the check.

Comment thread internal/webapphandler/handler.go
Comment on lines +31 to +33
if err := rewrite(r, conn, cfg.headers); err != nil {
cfg.logger.Error("failed to rewrite headers", zap.Error(err))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think when header rewriting fails, it's safer to fail the request.

Comment thread internal/webapphandler/web_app.go Outdated
Comment thread internal/config/config.go Outdated
Comment thread internal/metrics/round_tripper.go Outdated
Comment thread internal/proxy/proxy.go
Comment on lines +53 to +55
if config.Kubernetes != nil || config.WebApp != nil {
roundTripperMetrics = metrics.RegisterRoundTripperMetrics(registry)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be simpler if each resource type has its own HTTP server + proxy handler instead of all them sharing the same HTTP server? In that case, I think the HTTP proxy could still stay generic and doesn't have to know about the resource type.

Comment thread internal/httpproxy/utils/parser/parser.go Outdated
Comment thread internal/httpproxy/parser/parser.go Outdated
Comment thread internal/httpproxy/template/template.go Outdated
@clement0010 clement0010 force-pushed the feat/ct/support-http-web-app branch from a1a51d4 to e7ceee9 Compare June 2, 2026 08:19
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.

3 participants