Compare commits
3
Commits
28b813ba64
...
3e4ccc9720
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e4ccc9720 | ||
|
|
e5947489e4 | ||
|
|
0a7a10aeed |
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -79,17 +80,24 @@ func RateLimitPublicStatus() gin.HandlerFunc {
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 429 {object} ErrorResponse
|
||||
func getPublicStatusPage(c *gin.Context) {
|
||||
pageID := c.Param("pageId")
|
||||
inst, ok := publicStatusInstance(c)
|
||||
if !ok {
|
||||
// Every 404 on this route is indistinguishable to the caller by
|
||||
// design, so the log is the only place the three reasons are told
|
||||
// apart. It carries no monitor data and no page contents.
|
||||
log.Printf("public status: 404 page=%q reason=no_instance", pageID)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
snap, err := services.PublicStatusSnapshot(inst.InstanceID, c.Param("pageId"))
|
||||
snap, err := services.PublicStatusSnapshot(inst.InstanceID, pageID)
|
||||
if errors.Is(err, services.ErrPageNotFound) {
|
||||
log.Printf("public status: 404 page=%q instance=%s reason=page_missing_or_unpublished", pageID, inst.InstanceID)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("public status: 500 page=%q instance=%s: %v", pageID, inst.InstanceID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
@@ -114,21 +122,34 @@ func getPublicStatusPage(c *gin.Context) {
|
||||
// without this every self-hosted status page 404s forever. More than one is a
|
||||
// refusal rather than a guess.
|
||||
func publicStatusInstance(c *gin.Context) (*models.Instance, bool) {
|
||||
// Host resolution is where this route fails silently: an untrusted peer
|
||||
// means X-Forwarded-Host is ignored and the request host is the Go
|
||||
// service's own name, which names no slug. Log the inputs and the branch
|
||||
// taken, so the 404 says which of the four it was.
|
||||
host := c.Request.Host
|
||||
if trustedPeer(c) {
|
||||
if h := firstForwarded(c.GetHeader("X-Forwarded-Host")); h != "" {
|
||||
host = h
|
||||
}
|
||||
xfh := firstForwarded(c.GetHeader("X-Forwarded-Host"))
|
||||
trusted := trustedPeer(c)
|
||||
if trusted && xfh != "" {
|
||||
host = xfh
|
||||
}
|
||||
log.Printf("public status: resolve peer=%s trusted=%t request_host=%q x_forwarded_host=%q using_host=%q slug=%q",
|
||||
c.RemoteIP(), trusted, c.Request.Host, xfh, host, auth.HostSlug(host))
|
||||
|
||||
if inst, ok := auth.InstanceForHost(host); ok {
|
||||
return inst, true
|
||||
}
|
||||
if auth.HostSlug(host) != "" {
|
||||
if slug := auth.HostSlug(host); slug != "" {
|
||||
// The host named an instance and that instance does not exist.
|
||||
log.Printf("public status: no instance for slug=%q (host=%q)", slug, host)
|
||||
return nil, false
|
||||
}
|
||||
if services.DeploymentMode() == license.DeploymentCloud {
|
||||
log.Printf("public status: host %q names no slug and deployment is cloud, refusing to guess", host)
|
||||
return nil, false
|
||||
}
|
||||
return auth.SoleInstance()
|
||||
inst, ok := auth.SoleInstance()
|
||||
if !ok {
|
||||
log.Printf("public status: host %q names no slug and this deployment has no single instance", host)
|
||||
}
|
||||
return inst, ok
|
||||
}
|
||||
|
||||
@@ -405,6 +405,7 @@ func unavailableSnapshot(reason, title string) *StatusSnapshot {
|
||||
// unpublished" would confirm it exists.
|
||||
func PublicStatusSnapshot(instanceID, pageID string) (*StatusSnapshot, error) {
|
||||
if err := ValidatePageID(pageID); err != nil {
|
||||
log.Printf("public status: page id %q is not a valid id: %v", pageID, err)
|
||||
return nil, ErrPageNotFound
|
||||
}
|
||||
if snap := cachedSnapshot(instanceID, pageID); snap != nil {
|
||||
@@ -413,9 +414,11 @@ func PublicStatusSnapshot(instanceID, pageID string) (*StatusSnapshot, error) {
|
||||
|
||||
page, err := GetStatusPage(instanceID, pageID)
|
||||
if err != nil {
|
||||
log.Printf("public status: instance=%s page=%q lookup: %v", instanceID, pageID, err)
|
||||
return nil, err
|
||||
}
|
||||
if !page.Published {
|
||||
log.Printf("public status: instance=%s page=%q exists but published=false", instanceID, pageID)
|
||||
return nil, ErrPageNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,11 @@ function RecordPanel({ license }: { license: LicenseInfo }) {
|
||||
<Keyed label="Instance ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="truncate font-mono text-xs text-text-primary">{license.instance_id}</code>
|
||||
<button type="button" onClick={copyId} className="flex-shrink-0 rounded-sm border border-border px-1.5 py-0.5 font-mono text-[0.6rem] uppercase tracking-[0.1em] text-text-secondary transition-colors hover:border-text-tertiary hover:text-text-primary">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyId}
|
||||
className="flex-shrink-0 rounded-sm border border-border px-1.5 py-0.5 font-mono text-[0.6rem] uppercase tracking-[0.1em] text-text-secondary transition-colors hover:border-text-tertiary hover:text-text-primary"
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -257,6 +261,7 @@ export default function LicensePage() {
|
||||
<Feature label="Browser console" included={Boolean(license.features.console)} />
|
||||
<Feature label="Single sign-on" included={Boolean(license.features.oidc)} />
|
||||
<Feature label="Vulnerability Scanning" included={Boolean(license.features.vuln_scanning)} />
|
||||
<Feature label="Status Pages" included={Boolean(license.features.status_pages)} />
|
||||
</div>
|
||||
</Card>
|
||||
</Group>
|
||||
|
||||
@@ -107,10 +107,10 @@ function Switch({ checked, onChange, label }: { checked: boolean; onChange: (v:
|
||||
aria-checked={checked}
|
||||
aria-label={label}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`relative h-[21px] w-[38px] flex-shrink-0 rounded-full transition-colors ${checked ? "bg-success" : "bg-border"}`}
|
||||
className={`relative h-[21px] w-[38px] flex-shrink-0 rounded-full border-0 p-0 transition-colors ${checked ? "bg-success" : "bg-border"}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-[2px] h-[17px] w-[17px] rounded-full transition-transform ${
|
||||
className={`absolute left-0 top-[2px] h-[17px] w-[17px] rounded-full transition-transform ${
|
||||
checked ? "translate-x-[19px] bg-accent-ink" : "translate-x-[2px] bg-text-tertiary"
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -32,17 +32,25 @@ async function fetchSnapshot(host: string, forwardedFor: string, pageId: string)
|
||||
// intact.
|
||||
if (forwardedFor) outbound["X-Forwarded-For"] = forwardedFor;
|
||||
|
||||
const url = `${base}/public/status/${encodeURIComponent(pageId)}`;
|
||||
// This call is container-to-container and never appears in the front
|
||||
// proxy's access log, which is why a 404 here reads as "Next 404'd it".
|
||||
// Log both halves so the upstream status is visible in the web logs.
|
||||
console.log(`[status] GET ${url} host=${host} xff=${forwardedFor || "-"}`);
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${base}/public/status/${encodeURIComponent(pageId)}`, {
|
||||
res = await fetch(url, {
|
||||
headers: outbound,
|
||||
cache: "no-store",
|
||||
});
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// The control plane is unreachable. That is not "no such page".
|
||||
console.error(`[status] upstream unreachable ${url}:`, e);
|
||||
return { kind: "unavailable" };
|
||||
}
|
||||
|
||||
console.log(`[status] upstream ${res.status} for ${url}`);
|
||||
if (res.status === 404) return { kind: "not-found" };
|
||||
// A 429, a 500 or anything else is a page that exists and cannot be read
|
||||
// right now. Telling a customer mid-outage that their status page does not
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user