added glob matching for ignored paths in hws
This commit is contained in:
21
cookies/LICENSE
Normal file
21
cookies/LICENSE
Normal 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
61
cookies/README.md
Normal 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
405
cookies/cookies_test.go
Normal 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
26
cookies/doc.go
Normal 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
|
||||
Reference in New Issue
Block a user