feat: publish Gamertan Web Foundations preview source
verify / verify (push) Successful in 2m55s

Export the reviewed application-neutral package set through the exact public allowlist. Development history and private application evidence remain outside this canonical source root.

Developed with material AI assistance under maintainer review.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
2026-08-16 15:05:40 -04:00
commit 3a4b6db9b8
54 changed files with 4523 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MPL-2.0
package requestlog
import (
"bufio"
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
)
// JSONL is a synchronous append-only sink. The caller owns rotation.
type JSONL struct {
mu sync.Mutex
file *os.File
writer *bufio.Writer
err error
}
func OpenJSONL(path string) (*JSONL, error) {
if !filepath.IsAbs(path) || filepath.Clean(path) != path {
return nil, errors.New("requestlog: JSONL path must be clean and absolute")
}
if info, err := os.Lstat(path); err == nil && (info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular()) {
return nil, errors.New("requestlog: JSONL destination must be a regular file")
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return nil, err
}
if err = file.Chmod(0o600); err != nil {
file.Close()
return nil, err
}
return &JSONL{file: file, writer: bufio.NewWriterSize(file, 64*1024)}, nil
}
func (sink *JSONL) WriteRecord(ctx context.Context, record Record) error {
if err := ctx.Err(); err != nil {
return err
}
if err := record.Validate(); err != nil {
return err
}
body, err := json.Marshal(record)
if err != nil {
return err
}
sink.mu.Lock()
defer sink.mu.Unlock()
if sink.err != nil {
return sink.err
}
if _, err = sink.writer.Write(append(body, '\n')); err == nil {
err = sink.writer.Flush()
}
if err != nil {
sink.err = err
}
return err
}
func (sink *JSONL) Err() error { sink.mu.Lock(); defer sink.mu.Unlock(); return sink.err }
func (sink *JSONL) Close() error {
sink.mu.Lock()
defer sink.mu.Unlock()
if sink.file == nil {
return sink.err
}
flushErr := sink.writer.Flush()
closeErr := sink.file.Close()
sink.file = nil
return errors.Join(sink.err, flushErr, closeErr)
}
+205
View File
@@ -0,0 +1,205 @@
// SPDX-License-Identifier: MPL-2.0
// Package requestlog records bounded, versioned HTTP request observations.
package requestlog
import (
"context"
"errors"
"net/http"
"strings"
"time"
"unicode/utf8"
"gamertan.com/web/requestmeta"
)
const RecordVersion = 1
// Record is deliberately stable and append-log friendly. Sensitive fields are
// populated only when explicitly enabled by Policy.
type Record struct {
Version int `json:"version"`
Timestamp time.Time `json:"timestamp"`
RequestID string `json:"request_id,omitempty"`
Method string `json:"method"`
Route string `json:"route"`
Status int `json:"status"`
Bytes int64 `json:"bytes"`
DurationMicros int64 `json:"duration_micros"`
ClientIP string `json:"client_ip,omitempty"`
Path string `json:"path,omitempty"`
Query string `json:"query,omitempty"`
Referer string `json:"referer,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
SessionID string `json:"session_id,omitempty"`
}
// Validate rejects records that cannot have been produced by this package's
// bounded middleware contract.
func (record Record) Validate() error {
if record.Version != RecordVersion || record.Timestamp.IsZero() || !boundedField(record.Method, 16, false) || !boundedField(record.Route, 256, false) || record.Status < 100 || record.Status > 999 || record.Bytes < 0 || record.DurationMicros < 0 {
return errors.New("requestlog: invalid record")
}
fields := []struct {
value string
limit int
}{
{record.RequestID, 64}, {record.ClientIP, 64}, {record.Path, 2048},
{record.Query, 4096}, {record.Referer, 2048}, {record.UserAgent, 1024},
{record.SessionID, 256},
}
for _, field := range fields {
if !boundedField(field.value, field.limit, true) {
return errors.New("requestlog: invalid record")
}
}
return nil
}
// Sink receives complete records after a handler returns.
type Sink interface {
WriteRecord(context.Context, Record) error
}
// SensitiveFields must be opted into field by field.
type SensitiveFields struct {
ClientIP bool
Path bool
Query bool
Referer bool
UserAgent bool
SessionID bool
}
// Policy controls classification and collection. Route must return a low-cardinality
// route label; nil produces "unclassified" rather than recording a raw path.
type Policy struct {
Route func(*http.Request) string
SessionID func(*http.Request) string
Sensitive SensitiveFields
OnSinkError func(error)
Now func() time.Time
}
func Middleware(sink Sink, policy Policy) func(http.Handler) http.Handler {
if policy.Now == nil {
policy.Now = time.Now
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
started := policy.Now()
capture := &responseCapture{ResponseWriter: response, status: http.StatusOK}
defer func() {
recovered := recover()
if recovered != nil && !capture.wroteHeader {
capture.status = http.StatusInternalServerError
}
record := makeRecord(request, capture, policy, started, policy.Now())
if sink != nil {
if err := sink.WriteRecord(context.WithoutCancel(request.Context()), record); err != nil && policy.OnSinkError != nil {
policy.OnSinkError(errors.New("requestlog: sink write failed"))
}
}
if recovered != nil {
panic(recovered)
}
}()
next.ServeHTTP(capture, request)
})
}
}
func makeRecord(request *http.Request, capture *responseCapture, policy Policy, started, finished time.Time) Record {
route := "unclassified"
if policy.Route != nil {
route = bounded(policy.Route(request), 256)
if route == "" {
route = "unclassified"
}
}
duration := finished.Sub(started).Microseconds()
if duration < 0 {
duration = 0
}
record := Record{Version: RecordVersion, Timestamp: finished.UTC(), Method: bounded(request.Method, 16), Route: route, Status: capture.status, Bytes: capture.bytes, DurationMicros: duration}
if metadata, ok := requestmeta.FromContext(request.Context()); ok {
record.RequestID = metadata.RequestID
if policy.Sensitive.ClientIP && metadata.ClientIP.IsValid() {
record.ClientIP = metadata.ClientIP.String()
}
}
if policy.Sensitive.Path {
record.Path = bounded(request.URL.EscapedPath(), 2048)
}
if policy.Sensitive.Query {
record.Query = bounded(request.URL.RawQuery, 4096)
}
if policy.Sensitive.Referer {
record.Referer = bounded(request.Referer(), 2048)
}
if policy.Sensitive.UserAgent {
record.UserAgent = bounded(request.UserAgent(), 1024)
}
if policy.Sensitive.SessionID && policy.SessionID != nil {
record.SessionID = bounded(policy.SessionID(request), 256)
}
return record
}
func boundedField(value string, limit int, emptyOK bool) bool {
if (!emptyOK && value == "") || len(value) > limit || !utf8.ValidString(value) {
return false
}
for _, character := range value {
if character == 0 || character == '\r' || character == '\n' {
return false
}
}
return true
}
func bounded(value string, limit int) string {
value = strings.ToValidUTF8(value, "")
value = strings.Map(func(r rune) rune {
if r == 0 || r == '\r' || r == '\n' {
return -1
}
return r
}, value)
if len(value) <= limit {
return value
}
value = value[:limit]
for !utf8.ValidString(value) {
value = value[:len(value)-1]
}
return value
}
type responseCapture struct {
http.ResponseWriter
status int
bytes int64
wroteHeader bool
}
func (capture *responseCapture) WriteHeader(status int) {
if capture.wroteHeader {
return
}
capture.wroteHeader = true
capture.status = status
capture.ResponseWriter.WriteHeader(status)
}
func (capture *responseCapture) Write(body []byte) (int, error) {
if !capture.wroteHeader {
capture.WriteHeader(http.StatusOK)
}
written, err := capture.ResponseWriter.Write(body)
capture.bytes += int64(written)
return written, err
}
func (capture *responseCapture) Unwrap() http.ResponseWriter { return capture.ResponseWriter }
+169
View File
@@ -0,0 +1,169 @@
// SPDX-License-Identifier: MPL-2.0
package requestlog
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/netip"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"gamertan.com/web/requestmeta"
)
type memorySink struct {
records []Record
err error
ctxErr error
}
func (sink *memorySink) WriteRecord(ctx context.Context, record Record) error {
sink.records = append(sink.records, record)
sink.ctxErr = ctx.Err()
return sink.err
}
func TestSafePolicyOmitsSensitiveFields(t *testing.T) {
resolver, _ := requestmeta.New(requestmeta.Config{TrustedProxies: []netip.Prefix{netip.MustParsePrefix("127.0.0.0/8")}, Random: strings.NewReader(strings.Repeat("a", 16))})
sink := &memorySink{}
now := time.Unix(100, 0)
handler := resolver.Middleware(Middleware(sink, Policy{Route: func(*http.Request) string { return "item.show" }, Now: func() time.Time { now = now.Add(time.Millisecond); return now }})(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
response.WriteHeader(http.StatusCreated)
_, _ = response.Write([]byte("ok"))
})))
request := httptest.NewRequest(http.MethodGet, "http://example.test/private?id=secret", nil)
request.RemoteAddr = "127.0.0.1:1000"
request.Header.Set("X-Forwarded-For", "203.0.113.9")
request.Header.Set("User-Agent", "private-agent")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if len(sink.records) != 1 {
t.Fatalf("records=%d", len(sink.records))
}
record := sink.records[0]
if record.Route != "item.show" || record.Status != 201 || record.Bytes != 2 || record.RequestID == "" {
t.Fatalf("record=%+v", record)
}
if record.ClientIP != "" || record.Path != "" || record.Query != "" || record.UserAgent != "" {
t.Fatalf("sensitive leak: %+v", record)
}
}
func TestSensitivePolicyIsExplicitAndBounded(t *testing.T) {
sink := &memorySink{}
handler := Middleware(sink, Policy{Sensitive: SensitiveFields{Path: true, Query: true, Referer: true, UserAgent: true, SessionID: true}, SessionID: func(*http.Request) string { return "session" }})(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
request := httptest.NewRequest(http.MethodGet, "http://example.test/path?q=value", nil)
request.Header.Set("Referer", "https://ref.example/")
request.Header.Set("User-Agent", "browser")
handler.ServeHTTP(httptest.NewRecorder(), request)
record := sink.records[0]
if record.Path != "/path" || record.Query != "q=value" || record.Referer == "" || record.UserAgent != "browser" || record.SessionID != "session" {
t.Fatalf("record=%+v", record)
}
}
func TestJSONLRoundTripAndMode(t *testing.T) {
path := filepath.Join(t.TempDir(), "access.jsonl")
sink, err := OpenJSONL(path)
if err != nil {
t.Fatal(err)
}
if err = sink.WriteRecord(context.Background(), Record{Version: 1, Timestamp: time.Unix(100, 0), Method: "GET", Route: "home", Status: 200}); err != nil {
t.Fatal(err)
}
if err = sink.Close(); err != nil {
t.Fatal(err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
t.Fatalf("mode=%o", info.Mode().Perm())
}
body, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var record Record
if err = json.Unmarshal([]byte(strings.TrimSpace(string(body))), &record); err != nil {
t.Fatal(err)
}
if record.Route != "home" || record.Version != 1 {
t.Fatalf("record=%+v", record)
}
}
func TestPanicIsRecordedAndRepanicked(t *testing.T) {
sink := &memorySink{}
handler := Middleware(sink, Policy{})(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { panic("expected") }))
defer func() {
if recover() == nil {
t.Fatal("panic was swallowed")
}
if len(sink.records) != 1 || sink.records[0].Status != http.StatusInternalServerError {
t.Fatalf("records=%+v", sink.records)
}
}()
handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "http://example.test/", nil))
}
func TestCanceledRequestStillRecordsEvidence(t *testing.T) {
sink := &memorySink{}
handler := Middleware(sink, Policy{})(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
request := httptest.NewRequest(http.MethodGet, "http://example.test/", nil)
ctx, cancel := context.WithCancel(request.Context())
cancel()
handler.ServeHTTP(httptest.NewRecorder(), request.WithContext(ctx))
if len(sink.records) != 1 || sink.ctxErr != nil {
t.Fatalf("records=%d ctxErr=%v", len(sink.records), sink.ctxErr)
}
}
func TestJSONLRejectsInvalidRecord(t *testing.T) {
sink, err := OpenJSONL(filepath.Join(t.TempDir(), "access.jsonl"))
if err != nil {
t.Fatal(err)
}
defer sink.Close()
if err = sink.WriteRecord(context.Background(), Record{Version: 99}); err == nil {
t.Fatal("invalid record accepted")
}
}
func TestJSONLRejectsSymlinkDestination(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation is privilege-dependent on Windows")
}
directory := t.TempDir()
target := filepath.Join(directory, "target.jsonl")
if err := os.WriteFile(target, nil, 0o600); err != nil {
t.Fatal(err)
}
link := filepath.Join(directory, "access.jsonl")
if err := os.Symlink(target, link); err != nil {
t.Fatal(err)
}
if _, err := OpenJSONL(link); err == nil {
t.Fatal("symlink destination accepted")
}
}
func TestResponseStatusUsesFirstHeader(t *testing.T) {
sink := &memorySink{}
handler := Middleware(sink, Policy{})(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
response.WriteHeader(http.StatusNoContent)
response.WriteHeader(http.StatusInternalServerError)
}))
handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "http://example.test/", nil))
if sink.records[0].Status != http.StatusNoContent {
t.Fatalf("status=%d", sink.records[0].Status)
}
}