Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f61b608b1c |
@@ -313,7 +313,7 @@ itself; a production deployment puts a reverse proxy in front of it
|
|||||||
[Deployment behind a reverse proxy](#deployment-behind-a-reverse-proxy)),
|
[Deployment behind a reverse proxy](#deployment-behind-a-reverse-proxy)),
|
||||||
and the proxy reaches it over loopback. A default that bound every
|
and the proxy reaches it over loopback. A default that bound every
|
||||||
interface would leave that cleartext port answering the internet
|
interface would leave that cleartext port answering the internet
|
||||||
alongside the proxy — the admin sign-in form and the receiver, in the
|
alongside the proxy — the admin login form and the receiver, in the
|
||||||
clear, on a port nobody chose to publish. Reaching webhooker from
|
clear, on a port nobody chose to publish. Reaching webhooker from
|
||||||
another host is therefore something you configure, not something you
|
another host is therefore something you configure, not something you
|
||||||
get by default.
|
get by default.
|
||||||
@@ -835,7 +835,7 @@ reports.
|
|||||||
`-p 127.0.0.1:8080:8080`. Either way the port must reach the proxy
|
`-p 127.0.0.1:8080:8080`. Either way the port must reach the proxy
|
||||||
and nothing else; widen it only with a firewall or a publish
|
and nothing else; widen it only with a firewall or a publish
|
||||||
address in front of it. A cleartext port answering the internet
|
address in front of it. A cleartext port answering the internet
|
||||||
serves the admin sign-in form and the unauthenticated receiver with
|
serves the admin login form and the unauthenticated receiver with
|
||||||
no TLS at all, and the proxy in front of it changes nothing about
|
no TLS at all, and the proxy in front of it changes nothing about
|
||||||
that.
|
that.
|
||||||
2. **Make sure the environment is not `dev` (leave
|
2. **Make sure the environment is not `dev` (leave
|
||||||
@@ -2591,7 +2591,7 @@ The query string is never logged; it is replaced by the fixed marker
|
|||||||
`/.well-known/healthcheck` and `/s/*` answer 200 to anyone with no rate
|
`/.well-known/healthcheck` and `/s/*` answer 200 to anyone with no rate
|
||||||
limiter in front of them, so a query on a fixed 200 URL would otherwise
|
limiter in front of them, so a query on a fixed 200 URL would otherwise
|
||||||
buy the same amplification as an invented path. Nothing debuggable is
|
buy the same amplification as an invented path. Nothing debuggable is
|
||||||
lost: the only query parameters this service reads are the sign-in page's
|
lost: the only query parameters this service reads are the login page's
|
||||||
`next`, the page to return to, and `notice`, which names the line a page
|
`next`, the page to return to, and `notice`, which names the line a page
|
||||||
shows after an action.
|
shows after an action.
|
||||||
|
|
||||||
@@ -2746,9 +2746,9 @@ wider than it:
|
|||||||
| `... rate limit exceeded` (429) | `WARN` | path | yes, on the receiver |
|
| `... rate limit exceeded` (429) | `WARN` | path | yes, on the receiver |
|
||||||
| `auth middleware: unauthenticated request` | `DEBUG` | path, method | yes, by definition |
|
| `auth middleware: unauthenticated request` | `DEBUG` | path, method | yes, by definition |
|
||||||
| `entrypoint not found` | `DEBUG` | entrypoint UUID | yes, on the receiver |
|
| `entrypoint not found` | `DEBUG` | entrypoint UUID | yes, on the receiver |
|
||||||
| `user not found` / `invalid password` | `DEBUG` | username | yes, on the sign-in form |
|
| `user not found` / `invalid password` | `DEBUG` | username | yes, on the login form |
|
||||||
| `login failure limit exceeded` (429) | `WARN` | path | yes, on the sign-in form |
|
| `login failure limit exceeded` (429) | `WARN` | path | yes, on the login form |
|
||||||
| `password verification capacity exhausted` | `WARN` | path | yes, on the sign-in form |
|
| `password verification capacity exhausted` | `WARN` | path | yes, on the login form |
|
||||||
|
|
||||||
`DEBUG` being off by default is not a bound. An operator turning it on
|
`DEBUG` being off by default is not a bound. An operator turning it on
|
||||||
to diagnose a flood must not thereby hand the flood an unbounded write,
|
to diagnose a flood must not thereby hand the flood an unbounded write,
|
||||||
@@ -2796,7 +2796,7 @@ standard output on every statement that returned an error, including a
|
|||||||
plain record-not-found, at a level no operator setting reached. Two of
|
plain record-not-found, at a level no operator setting reached. Two of
|
||||||
this service's lookups miss by design on unauthenticated routes: the
|
this service's lookups miss by design on unauthenticated routes: the
|
||||||
entrypoint lookup behind `/h/{uuid}` and the user lookup behind
|
entrypoint lookup behind `/h/{uuid}` and the user lookup behind
|
||||||
the sign-in form, whose path segment and submitted username the client
|
the login form, whose path segment and submitted username the client
|
||||||
picks outright. Every
|
picks outright. Every
|
||||||
`gorm.Open` in the service now installs the adapter in
|
`gorm.Open` in the service now installs the adapter in
|
||||||
`internal/gormlog` instead. It writes through the same `slog` logger as
|
`internal/gormlog` instead. It writes through the same `slog` logger as
|
||||||
@@ -3090,8 +3090,8 @@ abuse limit later; they are tracked as future work.
|
|||||||
|
|
||||||
#### Authenticated Endpoints
|
#### Authenticated Endpoints
|
||||||
|
|
||||||
A signed-out `GET` of any of these is redirected to `/pages/login` with
|
A logged-out `GET` of any of these is redirected to `/pages/login` with
|
||||||
its path and query as `next` when they fit in 2048 bytes, so signing in
|
its path and query as `next` when they fit in 2048 bytes, so logging in
|
||||||
returns to the page that was asked for.
|
returns to the page that was asked for.
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
@@ -3440,7 +3440,7 @@ check, see [The login endpoint](#the-login-endpoint).
|
|||||||
still evaluated, so roughly 27 guesses a second get through and the
|
still evaluated, so roughly 27 guesses a second get through and the
|
||||||
admin password has to carry that load (see
|
admin password has to carry that load (see
|
||||||
[The login endpoint](#the-login-endpoint)). `GET` requests to the
|
[The login endpoint](#the-login-endpoint)). `GET` requests to the
|
||||||
sign-in page are not limited
|
login page are not limited
|
||||||
- **Password-change rate limiting** via [go-chi/httprate](https://github.com/go-chi/httprate):
|
- **Password-change rate limiting** via [go-chi/httprate](https://github.com/go-chi/httprate):
|
||||||
sliding-window rate limiter, 5 POST attempts per minute per bucket.
|
sliding-window rate limiter, 5 POST attempts per minute per bucket.
|
||||||
It runs behind session auth, so only a client already holding a
|
It runs behind session auth, so only a client already holding a
|
||||||
|
|||||||
@@ -177,17 +177,16 @@ func httpConfigFields(t *database.Target) []ConfigField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maxRetriesField describes a target's retry count, which lives
|
// maxRetriesField describes a target's retry count, which lives
|
||||||
// on the target row rather than in its configuration blob. A
|
// on the target row rather than in its configuration blob.
|
||||||
// stored 0 makes a single attempt, so it is shown as 1.
|
|
||||||
func maxRetriesField(t *database.Target) ConfigField {
|
func maxRetriesField(t *database.Target) ConfigField {
|
||||||
attempts := strconv.Itoa(t.MaxRetries)
|
retries := strconv.Itoa(t.MaxRetries)
|
||||||
if t.MaxRetries == 0 {
|
if t.MaxRetries == 0 {
|
||||||
attempts = "1 (fire-and-forget: no retries, no circuit breaker)"
|
retries += " (fire-and-forget)"
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigField{
|
return ConfigField{
|
||||||
Label: "Delivery attempts",
|
Label: "Delivery attempts",
|
||||||
Value: attempts,
|
Value: retries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func TestNewTargetViews_Slack(t *testing.T) {
|
|||||||
t,
|
t,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Webhook URL": slackMaskedURL,
|
"Webhook URL": slackMaskedURL,
|
||||||
viewMaxRetries: "1 (fire-and-forget: no retries, no circuit breaker)",
|
viewMaxRetries: "0 (fire-and-forget)",
|
||||||
},
|
},
|
||||||
fieldMap(view.Config),
|
fieldMap(view.Config),
|
||||||
)
|
)
|
||||||
@@ -258,7 +258,7 @@ func TestNewTargetViews_HTTPFireAndForget(t *testing.T) {
|
|||||||
t,
|
t,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Destination URL": viewMaskedOrigin,
|
"Destination URL": viewMaskedOrigin,
|
||||||
viewMaxRetries: "1 (fire-and-forget: no retries, no circuit breaker)",
|
viewMaxRetries: "0 (fire-and-forget)",
|
||||||
},
|
},
|
||||||
fieldMap(view.Config),
|
fieldMap(view.Config),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ func TestArchiveFileView_Rotated(t *testing.T) {
|
|||||||
assert.Equal(t, "2.0 kB", view.Size)
|
assert.Equal(t, "2.0 kB", view.Size)
|
||||||
|
|
||||||
page := targetList(t, renderedPage(t, env, webhook.ID))
|
page := targetList(t, renderedPage(t, env, webhook.ID))
|
||||||
assert.Contains(t, page, "Archive size: 2.0 kB in 2 files")
|
assert.Contains(t, page, "Archive Size: 2.0 kB in 2 files")
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderedPage returns the webhook page.
|
// renderedPage returns the webhook page.
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ func TestHandleSourceDetail_ShowsArchiveFile(t *testing.T) {
|
|||||||
path := delivery.ArchivePath(dbMgr, wh, archive)
|
path := delivery.ArchivePath(dbMgr, wh, archive)
|
||||||
|
|
||||||
body := renderSourceDetailPage(t, h, sess, wh.ID)
|
body := renderSourceDetailPage(t, h, sess, wh.ID)
|
||||||
assert.Equal(t, 1, strings.Count(body, "Archive file:"))
|
assert.Equal(t, 1, strings.Count(body, "Archive File:"))
|
||||||
assert.Contains(t, body, filepath.Base(path))
|
assert.Contains(t, body, filepath.Base(path))
|
||||||
assert.Contains(t, body, "not created yet")
|
assert.Contains(t, body, "not created yet")
|
||||||
assert.NotContains(t, body, "Archive size:")
|
assert.NotContains(t, body, "Archive Size:")
|
||||||
|
|
||||||
seedArchive(t, path, 1, 100)
|
seedArchive(t, path, 1, 100)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func TestHandleSourceDetail_ShowsArchiveFile(t *testing.T) {
|
|||||||
assert.Contains(t, body, filepath.Base(path))
|
assert.Contains(t, body, filepath.Base(path))
|
||||||
assert.NotContains(t, body, "not created yet")
|
assert.NotContains(t, body, "not created yet")
|
||||||
assert.Regexp(t,
|
assert.Regexp(t,
|
||||||
`Archive size:</span>\s*<span>[1-9][0-9.]* [kM]?B</span>`, body,
|
`Archive Size:</span>\s*<span>[1-9][0-9.]* [kM]?B</span>`, body,
|
||||||
)
|
)
|
||||||
assert.Contains(t, body,
|
assert.Contains(t, body,
|
||||||
`title="`+file.ModTime().UTC().Format(time.DateTime)+` UTC"`,
|
`title="`+file.ModTime().UTC().Format(time.DateTime)+` UTC"`,
|
||||||
@@ -69,7 +69,7 @@ func TestHandleSourceDetail_ShowsArchiveFile(t *testing.T) {
|
|||||||
body = renderSourceDetailPage(t, h, sess, wh.ID)
|
body = renderSourceDetailPage(t, h, sess, wh.ID)
|
||||||
assert.Contains(t, body, filepath.Base(path))
|
assert.Contains(t, body, filepath.Base(path))
|
||||||
assert.Contains(t, body, "not created yet")
|
assert.Contains(t, body, "not created yet")
|
||||||
assert.NotContains(t, body, "Archive size:")
|
assert.NotContains(t, body, "Archive Size:")
|
||||||
}
|
}
|
||||||
|
|
||||||
// targetList returns the text of the targets section in a rendered
|
// targetList returns the text of the targets section in a rendered
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ func TestPausedTarget_ShownUntilBreakerCloses(t *testing.T) {
|
|||||||
|
|
||||||
list := targetList(t, renderSourceDetailPage(t, h, sess, wh.ID))
|
list := targetList(t, renderSourceDetailPage(t, h, sess, wh.ID))
|
||||||
assert.Regexp(t, "t-http http Active Edit Deactivate Delete "+
|
assert.Regexp(t, "t-http http Active Edit Deactivate Delete "+
|
||||||
"Deliveries paused: after repeated failures, until "+cooldownEnds+
|
"Deliveries Paused: after repeated failures, until "+cooldownEnds+
|
||||||
", then one waiting delivery is sent to test the target while "+
|
", then one waiting delivery is sent to test the target while "+
|
||||||
"the others wait at least one more cooldown", list)
|
"the others wait at least one more cooldown", list)
|
||||||
assert.Equal(t, 1, strings.Count(list, "Deliveries paused"))
|
assert.Equal(t, 1, strings.Count(list, "Paused"))
|
||||||
|
|
||||||
log := renderSourceLogsPage(t, h, sess, wh.ID)
|
log := renderSourceLogsPage(t, h, sess, wh.ID)
|
||||||
assert.Equal(t, 2, strings.Count(log, "t-http: waiting"))
|
assert.Equal(t, 2, strings.Count(log, "t-http: waiting"))
|
||||||
@@ -105,7 +105,7 @@ func TestPausedTarget_ShownUntilBreakerCloses(t *testing.T) {
|
|||||||
|
|
||||||
list = targetList(t, renderSourceDetailPage(t, h, sess, wh.ID))
|
list = targetList(t, renderSourceDetailPage(t, h, sess, wh.ID))
|
||||||
assert.Contains(t, list, "t-http http Active Edit Deactivate Delete "+
|
assert.Contains(t, list, "t-http http Active Edit Deactivate Delete "+
|
||||||
"Deliveries paused: held while one delivery tests whether the "+
|
"Deliveries Paused: held while one delivery tests whether the "+
|
||||||
"target has recovered")
|
"target has recovered")
|
||||||
// Not the whole list: the add target form above the rows says UTC.
|
// Not the whole list: the add target form above the rows says UTC.
|
||||||
assert.NotContains(t, targetRow(list, "t-http", "t-log"), "UTC")
|
assert.NotContains(t, targetRow(list, "t-http", "t-log"), "UTC")
|
||||||
@@ -115,7 +115,7 @@ func TestPausedTarget_ShownUntilBreakerCloses(t *testing.T) {
|
|||||||
breakers.Set(target.ID, delivery.CircuitClosed, 0)
|
breakers.Set(target.ID, delivery.CircuitClosed, 0)
|
||||||
|
|
||||||
list = targetList(t, renderSourceDetailPage(t, h, sess, wh.ID))
|
list = targetList(t, renderSourceDetailPage(t, h, sess, wh.ID))
|
||||||
assert.NotContains(t, list, "Deliveries paused")
|
assert.NotContains(t, list, "Paused")
|
||||||
|
|
||||||
assertRetryingNotWaiting(t, h, sess, wh.ID, retrying, backedOff)
|
assertRetryingNotWaiting(t, h, sess, wh.ID, retrying, backedOff)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,7 +275,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{with .Paused}}
|
{{with .Paused}}
|
||||||
<div class="text-xs text-yellow-600 mt-1">
|
<div class="text-xs text-yellow-600 mt-1">
|
||||||
<span class="font-medium">Deliveries paused:</span>
|
<span class="font-medium">Deliveries Paused:</span>
|
||||||
<span>{{if .Until}}after repeated failures, until {{.Until}} ({{.Relative}}), then one waiting delivery is sent to test the target while the others wait at least one more cooldown{{else}}held while one delivery tests whether the target has recovered{{end}}</span>
|
<span>{{if .Until}}after repeated failures, until {{.Until}} ({{.Relative}}), then one waiting delivery is sent to test the target while the others wait at least one more cooldown{{else}}held while one delivery tests whether the target has recovered{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -287,17 +287,17 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{with .Archive}}
|
{{with .Archive}}
|
||||||
<div class="text-xs text-gray-500 mt-1">
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
<span class="font-medium text-gray-700">Archive file:</span>
|
<span class="font-medium text-gray-700">Archive File:</span>
|
||||||
<span class="break-all">{{.Name}}</span>
|
<span class="break-all">{{.Name}}</span>
|
||||||
{{with .Note}}<span>({{.}})</span>{{end}}
|
{{with .Note}}<span>({{.}})</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .Files}}
|
{{if .Files}}
|
||||||
<div class="text-xs text-gray-500 mt-1">
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
<span class="font-medium text-gray-700">Archive size:</span>
|
<span class="font-medium text-gray-700">Archive Size:</span>
|
||||||
<span>{{.Size}}{{if gt .Files 1}} in {{.Files}} files{{end}}</span>
|
<span>{{.Size}}{{if gt .Files 1}} in {{.Files}} files{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
<span class="font-medium text-gray-700">Last written:</span>
|
<span class="font-medium text-gray-700">Last Written:</span>
|
||||||
<span title="{{.WrittenUTC}}">{{.Written}}</span>
|
<span title="{{.WrittenUTC}}">{{.Written}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user