Compare commits
3
Commits
1cd2e4e97c
...
b30d41b87d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b30d41b87d | ||
|
|
ca77250cce | ||
|
|
521b6f8011 |
@@ -0,0 +1,67 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
// #138: the vwr viewer cookie must carry the Secure attribute. Tests inspect
|
||||||
|
// the Set-Cookie header directly rather than relying on cookie round-tripping,
|
||||||
|
// because Go's HTTP client (and browsers) drop Secure cookies over plain HTTP,
|
||||||
|
// which is how tests and local dev run.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"palette/internal/store"
|
||||||
|
"palette/internal/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestServer138(t *testing.T) *httptest.ResponseRecorder {
|
||||||
|
t.Helper()
|
||||||
|
st, err := store.OpenStore(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ui, err := web.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg := Config{MaxTextBytes: 5 * 1024 * 1024}
|
||||||
|
ss := NewTestSettingsStore(t, cfg)
|
||||||
|
globalSettingsFn = ss.get
|
||||||
|
t.Cleanup(func() { globalSettingsFn = nil })
|
||||||
|
a := &apiServer{store: st, cfg: cfg, ui: ui, settings: ss, adminKey: "test-admin-key"}
|
||||||
|
req := httptest.NewRequest("GET", "/history", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
a.routes().ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestViewerCookieAttributes(t *testing.T) {
|
||||||
|
rec := newTestServer138(t)
|
||||||
|
var vwr *string
|
||||||
|
for _, c := range rec.Result().Cookies() {
|
||||||
|
if c.Name == "vwr" {
|
||||||
|
cc := c
|
||||||
|
vwr = &cc.Value
|
||||||
|
if !c.Secure {
|
||||||
|
t.Error("vwr cookie missing Secure attribute (#138)")
|
||||||
|
}
|
||||||
|
if !c.HttpOnly {
|
||||||
|
t.Error("vwr cookie missing HttpOnly attribute")
|
||||||
|
}
|
||||||
|
if c.Path != "/" {
|
||||||
|
t.Errorf("vwr cookie Path = %q, want /", c.Path)
|
||||||
|
}
|
||||||
|
if c.SameSite != 2 { // http.SameSiteLaxMode
|
||||||
|
t.Errorf("vwr cookie SameSite = %v, want Lax", c.SameSite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vwr == nil {
|
||||||
|
t.Fatal("no vwr cookie set")
|
||||||
|
}
|
||||||
|
// also confirm the raw header form spells out Secure
|
||||||
|
sc := rec.Header().Get("Set-Cookie")
|
||||||
|
if !strings.Contains(sc, "Secure") {
|
||||||
|
t.Errorf("Set-Cookie header %q lacks Secure", sc)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,9 +141,12 @@ func viewerCookieMiddleware(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if c, err := r.Cookie("vwr"); err != nil || c.Value == "" {
|
if c, err := r.Cookie("vwr"); err != nil || c.Value == "" {
|
||||||
id := store.GenSlug(16)
|
id := store.GenSlug(16)
|
||||||
|
// #138: Secure keeps the viewer id off plain-HTTP requests
|
||||||
|
// (all deployments are HTTPS-only behind traefik).
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "vwr", Value: id, Path: "/",
|
Name: "vwr", Value: id, Path: "/",
|
||||||
MaxAge: 31536000, HttpOnly: true, SameSite: http.SameSiteLaxMode,
|
MaxAge: 31536000, HttpOnly: true, Secure: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
r.AddCookie(&http.Cookie{Name: "vwr", Value: id})
|
r.AddCookie(&http.Cookie{Name: "vwr", Value: id})
|
||||||
// remember that this cookie was minted here, not sent by the client
|
// remember that this cookie was minted here, not sent by the client
|
||||||
|
|||||||
@@ -263,8 +263,12 @@ body {
|
|||||||
/* #130: line wrap toggle, accent when active like other toggles */
|
/* #130: line wrap toggle, accent when active like other toggles */
|
||||||
.iconbtn.wrap-toggle[aria-pressed="true"],
|
.iconbtn.wrap-toggle[aria-pressed="true"],
|
||||||
.btn-icon.wrap-toggle[aria-pressed="true"] { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
.btn-icon.wrap-toggle[aria-pressed="true"] { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
||||||
html[data-wrap] .codebody { white-space: pre-wrap; }
|
/* #152: wrap ON must break long unbroken tokens mid-word and allow no horizontal scrolling */
|
||||||
html[data-wrap] .editor { white-space: pre-wrap; }
|
html[data-wrap] .codebody { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all; overflow-x: hidden; }
|
||||||
|
html[data-wrap] .editor { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all; overflow-x: hidden; }
|
||||||
|
/* #152: with wrap on nothing may scroll horizontally, including the code container and mobile floats */
|
||||||
|
html[data-wrap] .code { overflow-x: hidden; }
|
||||||
|
html[data-wrap] .float { overflow-x: hidden; }
|
||||||
.iconbtn.danger:hover { color: #ff8fa3; border-color: #ff8fa3; }
|
.iconbtn.danger:hover { color: #ff8fa3; border-color: #ff8fa3; }
|
||||||
.code-head {
|
.code-head {
|
||||||
display: flex; align-items: center; gap: 10px; padding: 8px 16px;
|
display: flex; align-items: center; gap: 10px; padding: 8px 16px;
|
||||||
@@ -520,11 +524,11 @@ a.admin-link:hover { color: var(--fg); text-decoration: underline; }
|
|||||||
body { font-size: 16px; }
|
body { font-size: 16px; }
|
||||||
|
|
||||||
/* topbar: tighten so logo + nav + gear fit */
|
/* topbar: tighten so logo + nav + gear fit */
|
||||||
.topbar { gap: 10px; padding: 0 12px; height: 56px; }
|
.topbar { gap: 6px; padding: 0 10px; height: 56px; }
|
||||||
.logo { font-size: 17px; white-space: nowrap; }
|
.logo { font-size: 17px; white-space: nowrap; }
|
||||||
.logo em { display: none; }
|
.logo em { display: none; }
|
||||||
.topbar nav { gap: 2px; flex-shrink: 0; }
|
.topbar nav { gap: 2px; flex-shrink: 0; }
|
||||||
.topbar nav a { padding: 5px 8px; font-size: 15px; }
|
.topbar nav a { padding: 4px 6px; font-size: 15px; }
|
||||||
.iconbtn.gear { padding: 4px 7px; flex-shrink: 0; }
|
.iconbtn.gear { padding: 4px 7px; flex-shrink: 0; }
|
||||||
.iconbtn.gear svg { width: 18px; height: 18px; }
|
.iconbtn.gear svg { width: 18px; height: 18px; }
|
||||||
.iconbtn.dark-toggle { padding: 4px 7px; flex-shrink: 0; }
|
.iconbtn.dark-toggle { padding: 4px 7px; flex-shrink: 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user