-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/cookie #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/cookie #36
Conversation
Reviewer's Guide by SourceryThis PR introduces a new ext/cookie package to securely manage HTTP cookies using base64 encoding and HMAC signatures, while also refactoring the Context API to use public fields (Response and Request) instead of private ones, and updating associated tests and documentation accordingly. Updated Context Class DiagramclassDiagram
class Context {
+Routing Routing
+app *App
+Response http.ResponseWriter
+Request *http.Request
-writtenStatus bool
-values map[string]any
+WriteStatus(code int)
+WriteHeader(key string, value string)
+View(data any, options ...string) error
+Redirect(url string, statusCode ...int)
+AcceptLanguage() []string
+Accept() []MimeType
+RequestReferer() string
}
note for Context "Refactored to expose Request and Response as public fields"
ext/cookie Package Functions DiagramclassDiagram
class Cookie {
<<module>> cookie
+Set(ctx *xun.Context, v http.Cookie) error
+Get(ctx *xun.Context, name string) (string, error)
+Delete(ctx *xun.Context, v http.Cookie)
+SetSigned(ctx *xun.Context, v http.Cookie, secretKey []byte) (time.Time, error)
+GetSigned(ctx *xun.Context, name string, secretKey []byte) (string, *time.Time, error)
}
class Errors {
<<constant>>
ErrValueTooLong
ErrInvalidValue
}
class Helper {
signValue(secretKey []byte, name string, value string, ts time.Time) (string, string)
}
Cookie ..> Errors : uses
Cookie ..> Helper : calls
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Here's the code health analysis summary for commits Analysis Summary
|
@sourcery-ai review |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #36 +/- ##
==========================================
+ Coverage 91.01% 91.89% +0.88%
==========================================
Files 37 38 +1
Lines 1335 1407 +72
==========================================
+ Hits 1215 1293 +78
+ Misses 83 78 -5
+ Partials 37 36 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @cnlangzi - I've reviewed your changes - here's some feedback:
Overall Comments:
- Ensure Delete sets Max-Age to 0 (or update the test expectation) as the test checks for 'Max-Age=0' while the code uses -1.
- Consider using a fixed-length timestamp format when signing cookies to avoid hard-coded length assumptions in GetSigned.
Here's what I looked at during the review
- 🟡 General issues: 1 issue found
- 🟡 Security: 1 issue found
- 🟡 Testing: 1 issue found
- 🟡 Complexity: 1 issue found
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
// | ||
// If the length of the resulting cookie value is longer than 4096 bytes, an | ||
// ErrValueTooLong error is returned. | ||
func GetSigned(ctx *xun.Context, name string, secretKey []byte) (string, *time.Time, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Insufficient length check before slicing the signed cookie value
In GetSigned, the check only ensures that signedValue has at least sha256.Size bytes. However, you later slice 20 additional bytes to extract the timestamp (tv). This could potentially cause an index out of range panic. Consider verifying that len(signedValue) >= sha256.Size + 20 before performing the slice.
@sourcery-ai review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @cnlangzi - I've reviewed your changes - here's some feedback:
Overall Comments:
- Consider adding inline comments explaining the byte concatenation and splitting logic in signValue and GetSigned to clarify the expected fixed lengths.
- Now that Context exposes Request and Response as public fields, remove the old accessor methods to simplify the API and reduce redundancy.
Here's what I looked at during the review
- 🟡 General issues: 1 issue found
- 🟢 Security: all looks good
- 🟡 Testing: 1 issue found
- 🟡 Complexity: 1 issue found
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
||
// Split apart the signature and original cookie value. | ||
signature := signedValue[:sha256.Size] | ||
tv := signedValue[sha256.Size : sha256.Size+20] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Assumption of fixed-length RFC3339 timestamp.
The code assumes that a UTC-formatted RFC3339 timestamp always yields exactly 20 characters. While this may hold true now, a more robust approach would be to derive the expected length dynamically, guarding against potential format changes in the future.
Suggested implementation:
timestampLength := len(time.Now().UTC().Format(time.RFC3339))
if len(signedValue) < sha256.Size+timestampLength {
return "", nil, ErrInvalidValue
}
// Split apart the signature and original cookie value.
signature := signedValue[:sha256.Size]
tv := signedValue[sha256.Size : sha256.Size+timestampLength]
value := signedValue[sha256.Size+timestampLength:]
Ensure that any code referencing the timestamp portion (tv) is updated to reflect the dynamically computed timestampLength, and that this approach works correctly with your timestamp format configuration.
@@ -0,0 +1,235 @@ | |||
package cookie |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Test cases for cookie with different options
It would be beneficial to include test cases that explore various cookie options like HttpOnly, Secure, SameSite, Domain, Expires, MaxAge, and Path. This ensures that the cookie handling functions correctly manage these attributes and produce the expected behavior under different scenarios.
Suggested implementation:
func TestCookieOptions(t *testing.T) {
// Create a dummy response recorder and a dummy request.
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "http://example.com", nil)
// Define the cookie attributes.
cookieName := "testCookie"
cookieValue := "testValue"
opts := &http.Cookie{
Name: cookieName,
Value: cookieValue,
HttpOnly: true,
Secure: true,
Domain: "example.com",
Path: "/",
Expires: time.Now().Add(24 * time.Hour),
MaxAge: 3600,
SameSite: http.SameSiteStrictMode,
}
// Set the cookie using SetSigned.
// Adjust the SetSigned function call if its signature differs.
err := SetSigned(rr, opts)
require.NoError(t, err)
// Simulate the client sending back the cookie:
// Add all Set-Cookie headers from response to the request header.
for _, cookieStr := range rr.Header()["Set-Cookie"] {
req.Header.Add("Cookie", cookieStr)
}
// Retrieve the cookie using GetSigned.
retrievedValue, err := GetSigned(req, cookieName)
require.NoError(t, err)
require.Equal(t, cookieValue, retrievedValue, "retrieved cookie value should match the original value")
// Verify that the cookie attributes are correctly set.
cookies := rr.Result().Cookies()
require.NotEmpty(t, cookies)
var c *http.Cookie
for _, cookie := range cookies {
if cookie.Name == cookieName {
c = cookie
break
}
}
require.NotNil(t, c)
require.Equal(t, true, c.HttpOnly)
require.Equal(t, true, c.Secure)
require.Equal(t, "example.com", c.Domain)
require.Equal(t, "/", c.Path)
require.Equal(t, http.SameSiteStrictMode, c.SameSite)
// Checking Expires (should be in the future) and MaxAge.
require.True(t, c.Expires.After(time.Now()), "cookie Expires should be in the future")
require.Equal(t, 3600, c.MaxAge)
}
Ensure that the SetSigned and GetSigned functions in your cookie package are designed to handle the provided *http.Cookie options.
Also, adapt the test if your implementation of SetSigned/ GetSigned requires different parameters or returns a different format.
@@ -0,0 +1,159 @@ | |||
// Package cookie provides functions for securely setting and retrieving HTTP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider refactoring the signing logic to use a delimiter to separate the signature, timestamp, and cookie value, which simplifies parsing and improves code resilience.
Consider refactoring the signing logic so that the signature, timestamp, and cookie value are clearly separated rather than being concatenated by fixed lengths. This will remove the brittle parsing in GetSigned. For example, you can use a delimiter (or a structured type) to encapsulate the three parts.
Actionable Steps:
-
Refactor signValue
Make it return a single string in the format"{signature}|{timestamp}|{value}"
and encode the signature in a URL‐safe format. For example:import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "time" ) func signValue(secretKey []byte, name, value string, ts time.Time) string { tsStr := ts.UTC().Format(time.RFC3339) mac := hmac.New(sha256.New, secretKey) mac.Write([]byte(name)) mac.Write([]byte(tsStr)) mac.Write([]byte(value)) signature := base64.URLEncoding.EncodeToString(mac.Sum(nil)) return fmt.Sprintf("%s|%s|%s", signature, tsStr, value) }
-
Refactor GetSigned
Update GetSigned to split the cookie value using the delimiter rather than slicing by fixed lengths:import ( "errors" "strings" "time" ) func GetSigned(ctx *xun.Context, name string, secretKey []byte) (string, *time.Time, error) { signedValue, err := Get(ctx, name) if err != nil { return "", nil, err } parts := strings.SplitN(signedValue, "|", 3) if len(parts) != 3 { return "", nil, ErrInvalidValue } signature, tsStr, value := parts[0], parts[1], parts[2] ts, err := time.Parse(time.RFC3339, tsStr) if err != nil { return "", nil, ErrInvalidValue } // Recalculate expected signature expectedMac := hmac.New(sha256.New, secretKey) expectedMac.Write([]byte(name)) expectedMac.Write([]byte(tsStr)) expectedMac.Write([]byte(value)) expectedSignature := base64.URLEncoding.EncodeToString(expectedMac.Sum(nil)) if !hmac.Equal([]byte(signature), []byte(expectedSignature)) { return "", nil, ErrInvalidValue } return value, &ts, nil }
These changes remove the dependency on fixed string slicing and make the code more readable and resilient.
Changed
Fixed
Added
ext/cookie
Tests
Tasks to complete before merging PR:
make unit-test
to check for any regressions 📋make lint
to check for any issuesSummary by Sourcery
New Features:
ext/cookie
extension to provide functions for setting, getting, and deleting cookies, including support for signed cookies.