added glob matching for ignored paths in hws

This commit is contained in:
2026-02-01 19:42:50 +11:00
parent 7ed40c7afe
commit cd29f11296
13 changed files with 833 additions and 11 deletions

21
cookies/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 haelnorr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

61
cookies/README.md Normal file
View File

@@ -0,0 +1,61 @@
# cookies v1.0.0
HTTP cookie utilities for Go web applications with security best practices.
## Features
- Secure cookie setting with HttpOnly flag
- Cookie deletion with proper expiration
- Pagefrom tracking for post-login redirects
- Host validation for referer-based redirects
- Full test coverage
## Installation
```bash
go get git.haelnorr.com/h/golib/cookies
```
## Quick Start
```go
package main
import (
"net/http"
"git.haelnorr.com/h/golib/cookies"
)
func handler(w http.ResponseWriter, r *http.Request) {
// Set a secure cookie
cookies.SetCookie(w, "session", "/", "abc123", 3600)
// Delete a cookie
cookies.DeleteCookie(w, "old_session", "/")
// Handle pagefrom for redirects
if r.URL.Path == "/login" {
cookies.SetPageFrom(w, r, "example.com")
}
// Check pagefrom after login
redirectTo := cookies.CheckPageFrom(w, r)
http.Redirect(w, r, redirectTo, http.StatusFound)
}
```
## Documentation
See the [wiki documentation](../golib/wiki/cookies.md) for detailed usage information and examples.
## License
MIT License
## Contributing
Please see the main golib repository for contributing guidelines.
## Related Projects
This package is part of the golib collection of utilities for Go applications and integrates well with other golib packages.

405
cookies/cookies_test.go Normal file
View File

@@ -0,0 +1,405 @@
package cookies
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestSetCookie(t *testing.T) {
tests := []struct {
name string
cookie string
path string
value string
maxAge int
expected string
}{
{
name: "basic cookie",
cookie: "test",
path: "/",
value: "value",
maxAge: 3600,
expected: "test=value; Path=/; Max-Age=3600; HttpOnly",
},
{
name: "zero max age",
cookie: "session",
path: "/api",
value: "abc123",
maxAge: 0,
expected: "session=abc123; Path=/api; HttpOnly",
},
{
name: "negative max age",
cookie: "temp",
path: "/",
value: "temp",
maxAge: -1,
expected: "temp=temp; Path=/; Max-Age=0; HttpOnly",
},
{
name: "empty value",
cookie: "empty",
path: "/",
value: "",
maxAge: 3600,
expected: "empty=; Path=/; Max-Age=3600; HttpOnly",
},
{
name: "special characters in value",
cookie: "data",
path: "/",
value: "test@123!#$%",
maxAge: 7200,
expected: "data=test@123!#$%; Path=/; Max-Age=7200; HttpOnly",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
SetCookie(w, tt.cookie, tt.path, tt.value, tt.maxAge)
headers := w.Header()["Set-Cookie"]
if len(headers) != 1 {
t.Errorf("Expected 1 Set-Cookie header, got %d", len(headers))
return
}
// Parse the cookie header to check individual components
cookieHeader := headers[0]
// Check that all expected components are present
if !strings.Contains(cookieHeader, tt.cookie+"="+tt.value) {
t.Errorf("Expected cookie name/value not found in: %s", cookieHeader)
}
if !strings.Contains(cookieHeader, "Path="+tt.path) {
t.Errorf("Expected path not found in: %s", cookieHeader)
}
if !strings.Contains(cookieHeader, "HttpOnly") {
t.Errorf("Expected HttpOnly not found in: %s", cookieHeader)
}
if tt.maxAge != 0 {
expectedMaxAge := fmt.Sprintf("Max-Age=%d", tt.maxAge)
if tt.maxAge < 0 {
expectedMaxAge = "Max-Age=0" // Go normalizes negative Max-Age to 0
}
if !strings.Contains(cookieHeader, expectedMaxAge) {
t.Errorf("Expected Max-Age not found in: %s", cookieHeader)
}
}
})
}
}
func TestDeleteCookie(t *testing.T) {
tests := []struct {
name string
cookie string
path string
expected string
}{
{
name: "basic deletion",
cookie: "test",
path: "/",
expected: "test=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; HttpOnly",
},
{
name: "delete with specific path",
cookie: "session",
path: "/api",
expected: "session=; Path=/api; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; HttpOnly",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
DeleteCookie(w, tt.cookie, tt.path)
headers := w.Header()["Set-Cookie"]
if len(headers) != 1 {
t.Errorf("Expected 1 Set-Cookie header, got %d", len(headers))
return
}
cookieHeader := headers[0]
// Check deletion-specific components
if !strings.Contains(cookieHeader, tt.cookie+"=") {
t.Errorf("Expected cookie name not found in: %s", cookieHeader)
}
if !strings.Contains(cookieHeader, "Path="+tt.path) {
t.Errorf("Expected path not found in: %s", cookieHeader)
}
if !strings.Contains(cookieHeader, "Max-Age=0") {
t.Errorf("Expected Max-Age=0 not found in: %s", cookieHeader)
}
if !strings.Contains(cookieHeader, "Expires=") {
t.Errorf("Expected Expires not found in: %s", cookieHeader)
}
if !strings.Contains(cookieHeader, "HttpOnly") {
t.Errorf("Expected HttpOnly not found in: %s", cookieHeader)
}
})
}
}
func TestCheckPageFrom(t *testing.T) {
tests := []struct {
name string
cookieValue string
cookiePath string
expectedResult string
shouldSet bool
}{
{
name: "valid pagefrom cookie",
cookieValue: "/dashboard",
cookiePath: "/",
expectedResult: "/dashboard",
shouldSet: true,
},
{
name: "no pagefrom cookie",
cookieValue: "",
cookiePath: "",
expectedResult: "/",
shouldSet: false,
},
{
name: "empty pagefrom cookie",
cookieValue: "",
cookiePath: "/",
expectedResult: "",
shouldSet: true,
},
{
name: "pagefrom with query params",
cookieValue: "/search?q=test",
cookiePath: "/",
expectedResult: "/search?q=test",
shouldSet: true,
},
{
name: "pagefrom with special path",
cookieValue: "/api/v1/users",
cookiePath: "/api",
expectedResult: "/api/v1/users",
shouldSet: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := &http.Request{
Header: make(http.Header),
}
if tt.shouldSet {
cookie := &http.Cookie{
Name: "pagefrom",
Value: tt.cookieValue,
Path: tt.cookiePath,
}
r.AddCookie(cookie)
}
result := CheckPageFrom(w, r)
if result != tt.expectedResult {
t.Errorf("CheckPageFrom() = %v, want %v", result, tt.expectedResult)
}
// Verify that the cookie was deleted
if tt.shouldSet {
headers := w.Header()["Set-Cookie"]
if len(headers) != 1 {
t.Errorf("Expected 1 Set-Cookie header for deletion, got %d", len(headers))
return
}
cookieHeader := headers[0]
if !strings.Contains(cookieHeader, "pagefrom=") {
t.Errorf("Expected pagefrom cookie deletion not found in: %s", cookieHeader)
}
if !strings.Contains(cookieHeader, "Max-Age=0") {
t.Errorf("Expected Max-Age=0 for deletion not found in: %s", cookieHeader)
}
}
})
}
}
func TestSetPageFrom(t *testing.T) {
tests := []struct {
name string
referer string
trustedHost string
expectedSet bool
expectedValue string
}{
{
name: "valid trusted host referer",
referer: "http://example.com/dashboard",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/dashboard",
},
{
name: "valid trusted host with https",
referer: "https://example.com/profile",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/profile",
},
{
name: "untrusted host",
referer: "http://evil.com/dashboard",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/",
},
{
name: "empty path",
referer: "http://example.com",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/",
},
{
name: "login path - should not set",
referer: "http://example.com/login",
trustedHost: "example.com",
expectedSet: false,
expectedValue: "",
},
{
name: "register path - should not set",
referer: "http://example.com/register",
trustedHost: "example.com",
expectedSet: false,
expectedValue: "",
},
{
name: "invalid referer URL",
referer: "not-a-url",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/",
},
{
name: "empty referer",
referer: "",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/",
},
{
name: "root path",
referer: "http://example.com/",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/",
},
{
name: "path with query string",
referer: "http://example.com/search?q=test",
trustedHost: "example.com",
expectedSet: true,
expectedValue: "/search",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := &http.Request{
Header: make(http.Header),
}
if tt.referer != "" {
r.Header.Set("Referer", tt.referer)
}
SetPageFrom(w, r, tt.trustedHost)
headers := w.Header()["Set-Cookie"]
if tt.expectedSet {
if len(headers) != 1 {
t.Errorf("Expected 1 Set-Cookie header, got %d", len(headers))
return
}
cookieHeader := headers[0]
if !strings.Contains(cookieHeader, "pagefrom="+tt.expectedValue) {
t.Errorf("Expected pagefrom=%s not found in: %s", tt.expectedValue, cookieHeader)
}
} else {
if len(headers) != 0 {
t.Errorf("Expected no Set-Cookie header, got %d", len(headers))
}
}
})
}
}
func TestIntegration(t *testing.T) {
// Test the complete flow: SetPageFrom -> CheckPageFrom
t.Run("complete flow", func(t *testing.T) {
// Step 1: Set pagefrom cookie
w1 := httptest.NewRecorder()
r1 := &http.Request{
Header: make(http.Header),
}
r1.Header.Set("Referer", "http://example.com/dashboard")
SetPageFrom(w1, r1, "example.com")
// Extract the cookie from the response
headers1 := w1.Header()["Set-Cookie"]
if len(headers1) != 1 {
t.Errorf("Expected 1 Set-Cookie header, got %d", len(headers1))
return
}
// Verify the cookie was set correctly
cookieHeader := headers1[0]
if !strings.Contains(cookieHeader, "pagefrom=/dashboard") {
t.Errorf("Expected pagefrom=/dashboard not found in: %s", cookieHeader)
}
// Step 2: Check pagefrom cookie (should delete it)
w2 := httptest.NewRecorder()
r2 := &http.Request{
Header: make(http.Header),
}
r2.AddCookie(&http.Cookie{
Name: "pagefrom",
Value: "/dashboard",
Path: "/",
})
result := CheckPageFrom(w2, r2)
if result != "/dashboard" {
t.Errorf("Expected result /dashboard, got %s", result)
}
// Verify the cookie was deleted
headers2 := w2.Header()["Set-Cookie"]
if len(headers2) != 1 {
t.Errorf("Expected 1 Set-Cookie header for deletion, got %d", len(headers2))
return
}
cookieHeader2 := headers2[0]
// Check for deletion indicators (Max-Age=0 with Expires in the past)
if !(strings.Contains(cookieHeader2, "Max-Age=0") && strings.Contains(cookieHeader2, "Expires=Thu, 01 Jan 1970")) {
t.Errorf("Expected cookie deletion, got: %s", cookieHeader2)
}
})
}

26
cookies/doc.go Normal file
View File

@@ -0,0 +1,26 @@
// Package cookies provides utilities for handling HTTP cookies in Go web applications.
// It includes functions for setting secure cookies, deleting cookies, and managing
// pagefrom tracking for post-login redirects.
//
// The package follows security best practices by setting the HttpOnly flag on all
// cookies to prevent XSS attacks. The SetCookie function allows you to specify the
// name, path, value, and max-age for cookies.
//
// The pagefrom functionality helps with user experience by remembering where a user
// was before being redirected to login/register pages, then redirecting them back
// after successful authentication.
//
// Example usage:
//
// // Set a session cookie
// cookies.SetCookie(w, "session", "/", "abc123", 3600)
//
// // Delete a cookie
// cookies.DeleteCookie(w, "old_session", "/")
//
// // Handle pagefrom tracking
// cookies.SetPageFrom(w, r, "example.com")
// redirectTo := cookies.CheckPageFrom(w, r)
//
// All functions are designed to be safe and handle edge cases gracefully.
package cookies