Compare commits
1
Commits
next
..
48a7e16b19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48a7e16b19 |
No files matched your search
@@ -168,10 +168,8 @@ either.
|
|||||||
- Request bodies for `POST /api/v1/session`, `POST /api/v1/login` and
|
- Request bodies for `POST /api/v1/session`, `POST /api/v1/login` and
|
||||||
`POST /api/v1/messages` are limited to `MAX_MESSAGE_SIZE` bytes (default
|
`POST /api/v1/messages` are limited to `MAX_MESSAGE_SIZE` bytes (default
|
||||||
`4096`). A larger body gets `400` `invalid request body`.
|
`4096`). A larger body gets `400` `invalid request body`.
|
||||||
- `GET /api/v1/server` and `GET /.well-known/healthcheck.json` answer a request
|
- The server allows cross-origin requests from any origin, with credentials. The
|
||||||
from any origin with `Access-Control-Allow-Origin: *`. Every other route sends
|
cookie is `SameSite=Strict`, so a browser sends it only from the same site.
|
||||||
no `Access-Control-Allow-*` header, so a page on another origin cannot read
|
|
||||||
its responses.
|
|
||||||
|
|
||||||
### The auth cookie
|
### The auth cookie
|
||||||
|
|
||||||
@@ -1895,14 +1893,14 @@ The IRC listener has no TLS.
|
|||||||
seconds. A request still running after 60 seconds is cancelled, and the
|
seconds. A request still running after 60 seconds is cancelled, and the
|
||||||
connection closes without a response. Headers may be up to 1 MiB.
|
connection closes without a response. Headers may be up to 1 MiB.
|
||||||
- A long poll waits at most 30 seconds.
|
- A long poll waits at most 30 seconds.
|
||||||
- Every response to a well-formed request carries a `Content-Security-Policy`
|
- Every response to a well-formed request, except to a cross-origin preflight,
|
||||||
header:
|
carries a `Content-Security-Policy` header:
|
||||||
`default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`.
|
`default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`.
|
||||||
The web client works within it.
|
The web client works within it.
|
||||||
- `GET /api/v1/server` and `GET /.well-known/healthcheck.json` answer a request
|
- Cross-origin requests are allowed from any origin, with credentials. The
|
||||||
from any origin with `Access-Control-Allow-Origin: *`. Every other route sends
|
allowed methods are `GET`, `POST`, `PUT`, `DELETE` and `OPTIONS`. The allowed
|
||||||
no `Access-Control-Allow-*` header, so a page on another origin cannot read
|
request headers are `Accept`, `Content-Type` and `X-CSRF-Token`. The cookie's
|
||||||
its responses.
|
`SameSite=Strict` still keeps it to the same site.
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ const (
|
|||||||
(SELECT 1 UNION ALL SELECT x + 1 FROM n)
|
(SELECT 1 UNION ALL SELECT x + 1 FROM n)
|
||||||
SELECT COUNT(*) FROM n`
|
SELECT COUNT(*) FROM n`
|
||||||
|
|
||||||
// endlessQueryTimeout is how long the endless query runs
|
endlessQueryTimeout = 100 * time.Millisecond
|
||||||
// before its context ends. A query whose context ends before
|
|
||||||
// it starts is never run, and the pool keeps its connection,
|
|
||||||
// so the timeout leaves a loaded host time to start it.
|
|
||||||
endlessQueryTimeout = 2 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestDefaultDBURLSetsWALAndBusyTimeout opens a file database
|
// TestDefaultDBURLSetsWALAndBusyTimeout opens a file database
|
||||||
|
|||||||
@@ -31,10 +31,7 @@ import (
|
|||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testTimeout is how long a test waits for something before it
|
const testTimeout = 5 * time.Second
|
||||||
// fails. A passing test never waits this long, so it is generous
|
|
||||||
// enough for a heavily loaded host.
|
|
||||||
const testTimeout = 30 * time.Second
|
|
||||||
|
|
||||||
// testPollInterval is how often the relay of a test server
|
// testPollInterval is how often the relay of a test server
|
||||||
// checks the client output queue without a broker
|
// checks the client output queue without a broker
|
||||||
@@ -568,49 +565,27 @@ func TestQuietClientAnsweringPingStaysConnected(t *testing.T) {
|
|||||||
func TestQuietClientNotAnsweringPingIsClosed(t *testing.T) {
|
func TestQuietClientNotAnsweringPingIsClosed(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// A read deadline of two ping intervals would close the
|
// latest leaves room for a loaded host, but is sooner than
|
||||||
// connection a second after latest. The ping interval is
|
// a read deadline of two ping intervals would close it.
|
||||||
// longer than testPingInterval, so that the time between
|
|
||||||
// the expected close and latest leaves room for a loaded
|
|
||||||
// host.
|
|
||||||
const (
|
const (
|
||||||
interval = 4 * time.Second
|
earliest = testPingInterval + testPongDeadline
|
||||||
earliest = interval + testPongDeadline
|
latest = 2 * testPingInterval
|
||||||
latest = 2*interval - time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
env := newTestEnvWithPingTimes(
|
env := newTestEnvWithShortPings(t)
|
||||||
t,
|
|
||||||
&config.Config{ServerName: testServerName},
|
|
||||||
interval, testPongDeadline,
|
|
||||||
)
|
|
||||||
client := env.dial(t)
|
client := env.dial(t)
|
||||||
|
|
||||||
lastSent := time.Now()
|
lastSent := time.Now()
|
||||||
|
|
||||||
client.register("silent")
|
client.register("silent")
|
||||||
|
client.readServerPing(lastSent)
|
||||||
// The server starts both waits once it has handled the
|
|
||||||
// registration, so the time registration took does not
|
|
||||||
// count against latest.
|
|
||||||
registered := time.Now()
|
|
||||||
|
|
||||||
client.readUntil(isServerPing)
|
|
||||||
client.readUntilClosed()
|
client.readUntilClosed()
|
||||||
|
|
||||||
waited := time.Since(lastSent)
|
waited := time.Since(lastSent)
|
||||||
if waited < earliest {
|
if waited < earliest || waited >= latest {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"closed %v after the client's last line, want at least %v",
|
"closed %v after the client's last line, want %v to %v",
|
||||||
waited, earliest,
|
waited, earliest, latest,
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sinceRegistered := time.Since(registered)
|
|
||||||
if sinceRegistered >= latest {
|
|
||||||
t.Errorf(
|
|
||||||
"closed %v after registration, want less than %v",
|
|
||||||
sinceRegistered, latest,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,19 +200,24 @@ func (mware *Middleware) Logging() func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORS returns middleware that lets a page on any origin read the
|
// CORS returns middleware that handles Cross-Origin Resource Sharing.
|
||||||
// response. Use it only on public, read-only routes.
|
// AllowCredentials is true so browsers include cookies in
|
||||||
|
// cross-origin API requests.
|
||||||
func (mware *Middleware) CORS() func(http.Handler) http.Handler {
|
func (mware *Middleware) CORS() func(http.Handler) http.Handler {
|
||||||
return cors.Handler(cors.Options{ //nolint:exhaustruct // optional fields
|
return cors.Handler(cors.Options{ //nolint:exhaustruct // optional fields
|
||||||
AllowedOrigins: []string{"*"},
|
AllowOriginFunc: func(
|
||||||
|
_ *http.Request, _ string,
|
||||||
|
) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
AllowedMethods: []string{
|
AllowedMethods: []string{
|
||||||
"GET", "POST", "PUT", "DELETE", "OPTIONS",
|
"GET", "POST", "PUT", "DELETE", "OPTIONS",
|
||||||
},
|
},
|
||||||
AllowedHeaders: []string{
|
AllowedHeaders: []string{
|
||||||
"Accept", "Authorization", "Content-Type", "X-CSRF-Token",
|
"Accept", "Content-Type", "X-CSRF-Token",
|
||||||
},
|
},
|
||||||
ExposedHeaders: []string{"Link"},
|
ExposedHeaders: []string{"Link"},
|
||||||
AllowCredentials: false,
|
AllowCredentials: true,
|
||||||
MaxAge: corsMaxAge,
|
MaxAge: corsMaxAge,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func (srv *Server) SetupRoutes() {
|
|||||||
srv.router.Use(srv.mw.Metrics())
|
srv.router.Use(srv.mw.Metrics())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
srv.router.Use(srv.mw.CORS())
|
||||||
srv.router.Use(srv.mw.CSP())
|
srv.router.Use(srv.mw.CSP())
|
||||||
srv.router.Use(middleware.Timeout(routeTimeout))
|
srv.router.Use(middleware.Timeout(routeTimeout))
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ func (srv *Server) SetupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Health check.
|
// Health check.
|
||||||
srv.router.With(srv.mw.CORS()).Get(
|
srv.router.Get(
|
||||||
"/.well-known/healthcheck.json",
|
"/.well-known/healthcheck.json",
|
||||||
srv.handlers.HandleHealthCheck(),
|
srv.handlers.HandleHealthCheck(),
|
||||||
)
|
)
|
||||||
@@ -68,7 +69,7 @@ func (srv *Server) SetupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Server) setupAPIv1(router chi.Router) {
|
func (srv *Server) setupAPIv1(router chi.Router) {
|
||||||
router.With(srv.mw.CORS()).Get(
|
router.Get(
|
||||||
"/server",
|
"/server",
|
||||||
srv.handlers.HandleServerInfo(),
|
srv.handlers.HandleServerInfo(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ package server_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -190,88 +188,6 @@ func TestServeAfterStopDoesNotStopApp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCORSHeaders checks the Access-Control-Allow-* headers on the
|
|
||||||
// answers to requests from another origin. The public, read-only
|
|
||||||
// routes answer with Access-Control-Allow-Origin: * alone. Routes
|
|
||||||
// that need the cookie, and a preflight to one, answer with none.
|
|
||||||
func TestCORSHeaders(t *testing.T) {
|
|
||||||
var srv *server.Server
|
|
||||||
|
|
||||||
startApp(t, fx.Populate(&srv))
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
method string
|
|
||||||
path string
|
|
||||||
requestMethod string
|
|
||||||
wantHeaders map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "server information",
|
|
||||||
method: http.MethodGet,
|
|
||||||
path: "/api/v1/server",
|
|
||||||
wantHeaders: map[string]string{"Access-Control-Allow-Origin": "*"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "health check",
|
|
||||||
method: http.MethodGet,
|
|
||||||
path: "/.well-known/healthcheck.json",
|
|
||||||
wantHeaders: map[string]string{"Access-Control-Allow-Origin": "*"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "send",
|
|
||||||
method: http.MethodPost,
|
|
||||||
path: "/api/v1/messages",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "state",
|
|
||||||
method: http.MethodGet,
|
|
||||||
path: "/api/v1/state",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preflight for send",
|
|
||||||
method: http.MethodOptions,
|
|
||||||
path: "/api/v1/messages",
|
|
||||||
requestMethod: http.MethodPost,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
request := httptest.NewRequestWithContext(
|
|
||||||
t.Context(), test.method,
|
|
||||||
"http://chat.example.com"+test.path, nil,
|
|
||||||
)
|
|
||||||
request.Header.Set("Origin", "http://blog.example.com")
|
|
||||||
|
|
||||||
if test.requestMethod != "" {
|
|
||||||
request.Header.Set(
|
|
||||||
"Access-Control-Request-Method", test.requestMethod,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
srv.ServeHTTP(recorder, request)
|
|
||||||
|
|
||||||
gotHeaders := map[string]string{}
|
|
||||||
|
|
||||||
for name := range recorder.Header() {
|
|
||||||
if strings.HasPrefix(name, "Access-Control-Allow-") {
|
|
||||||
gotHeaders[name] = recorder.Header().Get(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !maps.Equal(gotHeaders, test.wantHeaders) {
|
|
||||||
t.Errorf(
|
|
||||||
"Access-Control-Allow-* headers are %v, want %v",
|
|
||||||
gotHeaders, test.wantHeaders,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startApp starts the app newApp builds, with the HTTP port
|
// startApp starts the app newApp builds, with the HTTP port
|
||||||
// set to 0, so the system gives the server a free port.
|
// set to 0, so the system gives the server a free port.
|
||||||
func startApp(
|
func startApp(
|
||||||
|
|||||||
Reference in new issue
Block a user