All checks were successful
check / check (push) Successful in 3m8s
Three findings from the review of the per-target request headers feature. Configured headers no longer follow a redirect off the origin the target names. net/http withholds only Authorization and Cookie across a host change, so an operator's X-Api-Key or PRIVATE-TOKEN would follow a 302 to a host they never configured. Redirects are still followed — refusing them would break every destination that legitimately redirects and would record the 3xx as the delivery's result — but a hop to another host, another port, or down from https to http drops every header the target configured. The shared SSRF-safe transport is kept on that client, so each hop is still dialled through the private-IP guard. Trailer joins the reserved names. net/http strips it from the request it writes, so a configured one was accepted, stored, and provably never sent. The invalid-header-name error no longer quotes the text before the first colon. That text is only a name if it parses as one; when it does not, a pasted value whose own colon split the line put half a token into a 400 body. TestParseTargetHeaders_ErrorsNeverQuoteAValue asserted this invariant while only exercising the after-the-colon case, and now covers the before-the-colon one. README documents the http target's config keys, the 300-second timeout ceiling, the reserved-header list and the redirect behaviour; the edit form's hint gains Trailer and the redirect note.
84 lines
4.6 KiB
HTML
84 lines
4.6 KiB
HTML
{{template "base" .}}
|
|
|
|
{{define "title"}}Edit {{.Target.Name}} - Webhooker{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="max-w-2xl mx-auto px-6 py-8">
|
|
<div class="mb-6">
|
|
<a href="/source/{{.Webhook.ID}}" class="text-sm text-primary-600 hover:text-primary-700">← Back to {{.Webhook.Name}}</a>
|
|
<h1 class="text-2xl font-medium text-gray-900 mt-2">Edit Target</h1>
|
|
<p class="text-sm text-gray-500 mt-1">Type: {{.Target.Type}}. A target's type cannot be changed; create a new target to deliver a different way.</p>
|
|
</div>
|
|
|
|
<div class="card p-6">
|
|
{{if .Error}}
|
|
<div class="alert-error">{{.Error}}</div>
|
|
{{end}}
|
|
|
|
{{if or (eq .Target.Type "http") (eq .Target.Type "slack")}}
|
|
<div class="mb-6 rounded-md bg-gray-50 p-4 text-sm text-gray-700">
|
|
This form shows the target's stored destination in full, including any credential carried in its URL or headers. It is the only page that does; everywhere else the value is masked.
|
|
</div>
|
|
{{end}}
|
|
|
|
<form method="POST" action="/source/{{.Webhook.ID}}/targets/{{.Target.ID}}/edit" class="space-y-6">
|
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
|
|
|
<div class="form-group">
|
|
<label for="name" class="label">Name</label>
|
|
<input type="text" id="name" name="name" value="{{.Target.Name}}" required class="input">
|
|
</div>
|
|
|
|
{{if eq .Target.Type "http"}}
|
|
<div class="form-group">
|
|
<label for="url" class="label">Destination URL</label>
|
|
<input type="url" id="url" name="url" value="{{.Target.Config.URL}}" required class="input">
|
|
<p class="text-xs text-gray-500 mt-1">Revalidated on save; destinations that resolve to private or link-local addresses are rejected.</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="headers" class="label">Headers</label>
|
|
<textarea id="headers" name="headers" rows="4" class="input" placeholder="Authorization: Bearer ...">{{.Target.Config.Headers}}</textarea>
|
|
<p class="text-xs text-gray-500 mt-1">One <code>Name: value</code> per line, sent with every delivery. Leave blank for none. <code>Host</code>, <code>Content-Length</code>, <code>Transfer-Encoding</code>, <code>Connection</code>, <code>Trailer</code> and <code>User-Agent</code> are set by the delivery engine and are rejected here rather than silently ignored. Headers set here are dropped if a redirect leaves the destination's own origin, so a credential cannot follow one to another host.</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="timeout" class="label">Timeout (seconds)</label>
|
|
<input type="number" id="timeout" name="timeout" value="{{.Target.Config.Timeout}}" min="0" max="{{.MaxTimeout}}" class="input">
|
|
<p class="text-xs text-gray-500 mt-1">Per-request timeout, at most {{.MaxTimeout}} seconds. Leave blank to use the default.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if eq .Target.Type "slack"}}
|
|
<div class="form-group">
|
|
<label for="url" class="label">Webhook URL</label>
|
|
<input type="url" id="url" name="url" value="{{.Target.Config.URL}}" required class="input">
|
|
<p class="text-xs text-gray-500 mt-1">Slack or Mattermost incoming webhook URL. Revalidated on save.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if eq .Target.Type "database"}}
|
|
<div class="form-group">
|
|
<label for="expiry" class="label">Archive Expiry</label>
|
|
<input type="text" id="expiry" name="expiry" value="{{.Target.Config.Expiry}}" placeholder="never" class="input">
|
|
<p class="text-xs text-gray-500 mt-1">"never" (the default when blank) keeps archived rows forever, or a Go duration like "720h" prunes older rows.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if or (eq .Target.Type "http") (eq .Target.Type "slack")}}
|
|
<div class="form-group">
|
|
<label for="max_retries" class="label">Max retries</label>
|
|
<input type="number" id="max_retries" name="max_retries" value="{{.Target.MaxRetries}}" min="0" max="20" class="input">
|
|
<p class="text-xs text-gray-500 mt-1">0 is fire-and-forget: one attempt, no circuit breaker.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="flex gap-3">
|
|
<button type="submit" class="btn-primary">Save Changes</button>
|
|
<a href="/source/{{.Webhook.ID}}" class="btn-secondary">Cancel</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{{end}}
|