Skip to content

Conversation

cnlangzi
Copy link
Member

@cnlangzi cnlangzi commented Feb 7, 2025

Changed

Fixed

Added

  • added ext/cookie

Tests

Tasks to complete before merging PR:

  • Ensure unit tests are passing. If not run make unit-test to check for any regressions 📋
  • Ensure lint tests are passing. if not run make lint to check for any issues
  • Ensure codecov/patch is passing for changes.

Summary by Sourcery

New Features:

  • Added ext/cookie extension to provide functions for setting, getting, and deleting cookies, including support for signed cookies.

Copy link

sourcery-ai bot commented Feb 7, 2025

Reviewer's Guide by Sourcery

This 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 Diagram

classDiagram
    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"
Loading

ext/cookie Package Functions Diagram

classDiagram
    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
Loading

File-Level Changes

Change Details Files
Refactored Context API to use public fields for Request and Response
  • Replaced private context fields (rw, req) with public fields (Response, Request) in Context struct
  • Removed redundant getter methods in Context related to request and response handling
  • Updated Context method implementations (WriteStatus, WriteHeader, View, etc.) to use the new public fields
  • Modified usage in app handlers, routing logic, and tests to align with the new field names
context.go
app.go
context_test.go
app_test.go
binder_test.go
group_test.go
middleware_test.go
README.md
ext/hsts/hsts.go
ext/htmx/interceptor.go
Introduced a new ext/cookie package for handling cookies securely
  • Added the ext/cookie package with functions for setting, retrieving, and deleting cookies
  • Implemented secure cookie operations using base64 encoding and HMAC signatures
  • Added functionality for handling signed cookies with timestamp verification
  • Included comprehensive unit tests for the cookie functionality
ext/cookie/cookie.go
ext/cookie/cookie_test.go
Updated documentation to reflect API changes and new cookie features
  • Updated examples in README to use the new Context API and cookie handling functions
  • Aligned documentation with the new public fields and methods in Context
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

deepsource-io bot commented Feb 7, 2025

Here's the code health analysis summary for commits b5c1aac..a30fac1. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource Go LogoGo✅ SuccessView Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

@cnlangzi
Copy link
Member Author

cnlangzi commented Feb 8, 2025

@sourcery-ai review

Copy link

codecov bot commented Feb 8, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 91.89%. Comparing base (b5c1aac) to head (a30fac1).
Report is 1 commits behind head on main.

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     
Flag Coverage Δ
Unit-Tests 91.89% <100.00%> (+0.88%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

@sourcery-ai sourcery-ai bot left a 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

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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) {
Copy link

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.

@cnlangzi
Copy link
Member Author

cnlangzi commented Feb 9, 2025

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a 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

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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]
Copy link

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
Copy link

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
Copy link

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:

  1. 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)
    }
  2. 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.

@cnlangzi cnlangzi merged commit 470a0dc into main Feb 9, 2025
7 checks passed
@cnlangzi cnlangzi deleted the feat/cookie branch February 9, 2025 00:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant