diff options
Diffstat (limited to 'internal/middleware')
| -rw-r--r-- | internal/middleware/nollamas.go | 78 | ||||
| -rw-r--r-- | internal/middleware/nollamas_test.go | 33 | ||||
| -rw-r--r-- | internal/middleware/session.go | 39 |
3 files changed, 102 insertions, 48 deletions
diff --git a/internal/middleware/nollamas.go b/internal/middleware/nollamas.go index 7f01c5afc..e5be014f5 100644 --- a/internal/middleware/nollamas.go +++ b/internal/middleware/nollamas.go @@ -50,7 +50,10 @@ import ( // requires javascript to be enabled on the client to pass the middleware check. // // Heavily inspired by: https://github.com/TecharoHQ/anubis -func NoLLaMas(getInstanceV1 func(context.Context) (*apimodel.InstanceV1, gtserror.WithCode)) gin.HandlerFunc { +func NoLLaMas( + cookiePolicy apiutil.CookiePolicy, + getInstanceV1 func(context.Context) (*apimodel.InstanceV1, gtserror.WithCode), +) gin.HandlerFunc { if !config.GetAdvancedScraperDeterrence() { // NoLLaMas middleware disabled. @@ -69,8 +72,10 @@ func NoLLaMas(getInstanceV1 func(context.Context) (*apimodel.InstanceV1, gtserro var nollamas nollamas nollamas.seed = seed nollamas.ttl = time.Hour - nollamas.diff = 4 + nollamas.diff1 = 4 + nollamas.diff2 = '4' nollamas.getInstanceV1 = getInstanceV1 + nollamas.policy = cookiePolicy return nollamas.Serve } @@ -84,9 +89,28 @@ type hashWithBufs struct { } type nollamas struct { - seed []byte // unique token seed - ttl time.Duration - diff uint8 + // our instance cookie policy. + policy apiutil.CookiePolicy + + // unique token seed + // to prevent hashes + // being guessable + seed []byte + + // success cookie TTL + ttl time.Duration + + // algorithm difficulty knobs. + // diff1 determines the number of + // leading zeroes required, while + // diff2 checks the next byte at + // index is less than it. + // + // e.g. you look for say: + // - b[0:3] must be '0' + // - b[4] can be < '5' + diff1 uint8 + diff2 uint8 // extra fields required for // our template rendering. @@ -211,7 +235,7 @@ func (m *nollamas) Serve(c *gin.Context) { // They passed the challenge! Set success token // cookie and allow them to continue to next handlers. - c.SetCookie("gts-nollamas", token, int(m.ttl/time.Second), "", "", false, false) + m.policy.SetCookie(c, "gts-nollamas", token, int(m.ttl/time.Second), "/") c.Redirect(http.StatusTemporaryRedirect, c.Request.URL.RequestURI()) } @@ -239,8 +263,12 @@ func (m *nollamas) renderChallenge(c *gin.Context, challenge string) { "/assets/Fork-Awesome/css/fork-awesome.min.css", }, Extra: map[string]any{ - "challenge": challenge, - "difficulty": m.diff, + "challenge": challenge, + "difficulty1": m.diff1, + + // must be a str otherwise template + // renders uint8 as int, not char + "difficulty2": hexStrs[m.diff2], }, Javascript: []apiutil.JavascriptEntry{ { @@ -261,7 +289,8 @@ func (m *nollamas) token(hash *hashWithBufs, userAgent, clientIP string) string // Include difficulty level in // hash input data so if config // changes then token invalidates. - hash.hash.Write([]byte{m.diff}) + hash.hash.Write([]byte{m.diff1}) + hash.hash.Write([]byte{m.diff2}) // Also seed the generated input with // current time rounded to TTL, so our @@ -297,13 +326,40 @@ func (m *nollamas) checkChallenge(hash *hashWithBufs, challenge, nonce string) b hex.Encode(hash.ebuf, hash.hbuf) solution := hash.ebuf + // Compiler bound-check-elimination hint. + if len(solution) < int(m.diff1+1) { + panic(gtserror.New("BCE")) + } + // Check that the first 'diff' // many chars are indeed zeroes. - for i := range m.diff { + for i := range m.diff1 { if solution[i] != '0' { return false } } - return true + // Check that next char is < 'diff2'. + return solution[m.diff1] < m.diff2 +} + +// hexStrs is a quick lookup of ASCII hex +// bytes to their string equivalent. +var hexStrs = [...]string{ + '0': "0", + '1': "1", + '2': "2", + '3': "3", + '4': "4", + '5': "5", + '6': "6", + '7': "7", + '8': "8", + '9': "9", + 'a': "a", + 'b': "b", + 'c': "c", + 'd': "d", + 'e': "e", + 'f': "f", } diff --git a/internal/middleware/nollamas_test.go b/internal/middleware/nollamas_test.go index 92a044d32..d6fdb5ff6 100644 --- a/internal/middleware/nollamas_test.go +++ b/internal/middleware/nollamas_test.go @@ -30,6 +30,7 @@ import ( "testing" "code.superseriousbusiness.org/gotosocial/internal/api/model" + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" "code.superseriousbusiness.org/gotosocial/internal/config" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/middleware" @@ -52,7 +53,7 @@ func TestNoLLaMasMiddleware(t *testing.T) { assert.NoError(t, err) // Add middleware to the gin engine handler stack. - middleware := middleware.NoLLaMas(getInstanceV1) + middleware := middleware.NoLLaMas(apiutil.CookiePolicy{}, getInstanceV1) e.Use(middleware) // Set test handler we can @@ -94,8 +95,9 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) { panic(err) } - var difficulty uint64 var challenge string + var diff1 uint64 + var diff2 uint8 // Parse output body and find the challenge / difficulty. for _, line := range strings.Split(string(b), "\n") { @@ -105,17 +107,22 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) { line = line[25:] line = line[:len(line)-1] challenge = line - case strings.HasPrefix(line, "data-nollamas-difficulty=\""): - line = line[26:] + case strings.HasPrefix(line, "data-nollamas-difficulty1=\""): + line = line[27:] line = line[:len(line)-1] var err error - difficulty, err = strconv.ParseUint(line, 10, 8) + diff1, err = strconv.ParseUint(line, 10, 8) assert.NoError(t, err) + case strings.HasPrefix(line, "data-nollamas-difficulty2=\""): + line = line[27:] + line = line[:len(line)-1] + diff2 = line[0] } } // Ensure valid posed challenge. - assert.NotZero(t, difficulty) + assert.NotZero(t, diff1) + assert.NotZero(t, diff2) assert.NotEmpty(t, challenge) // Prepare a test request for gin engine. @@ -124,9 +131,14 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) { rw = httptest.NewRecorder() // Now compute and set solution query paramater. - solution := computeSolution(challenge, difficulty) + solution := computeSolution(challenge, diff1, diff2) r.URL.RawQuery = "nollamas_solution=" + solution + t.Logf("challenge=%s", challenge) + t.Logf("diff1=%d", diff1) + t.Logf("diff2='%c'", diff2) + t.Logf("solution=%s", solution) + // Pass req through // engine handler. e.ServeHTTP(rw, r) @@ -147,18 +159,21 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) { } // computeSolution does the functional equivalent of our nollamas workerTask.js. -func computeSolution(challenge string, difficulty uint64) string { +func computeSolution(challenge string, diff1 uint64, diff2 uint8) string { outer: for i := 0; ; i++ { solution := strconv.Itoa(i) combined := challenge + solution hash := sha256.Sum256(byteutil.S2B(combined)) encoded := hex.EncodeToString(hash[:]) - for i := range difficulty { + for i := range diff1 { if encoded[i] != '0' { continue outer } } + if encoded[diff1] >= diff2 { + continue outer + } return solution } } diff --git a/internal/middleware/session.go b/internal/middleware/session.go index 50433002a..83b38ef35 100644 --- a/internal/middleware/session.go +++ b/internal/middleware/session.go @@ -19,12 +19,10 @@ package middleware import ( "fmt" - "net/http" "net/url" - "strings" + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" "code.superseriousbusiness.org/gotosocial/internal/config" - "code.superseriousbusiness.org/gotosocial/internal/log" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" @@ -32,29 +30,15 @@ import ( ) // SessionOptions returns the standard set of options to use for each session. -func SessionOptions() sessions.Options { - var samesite http.SameSite - switch strings.TrimSpace(strings.ToLower(config.GetAdvancedCookiesSamesite())) { - case "lax": - samesite = http.SameSiteLaxMode - case "strict": - samesite = http.SameSiteStrictMode - default: - log.Warnf(nil, "%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag(), config.GetAdvancedCookiesSamesite()) - samesite = http.SameSiteLaxMode - } - +func SessionOptions(cookiePolicy apiutil.CookiePolicy) sessions.Options { return sessions.Options{ Path: "/", - Domain: config.GetHost(), + Domain: cookiePolicy.Domain, // 2 minutes - MaxAge: 120, - // only set secure over https - Secure: config.GetProtocol() == "https", - // forbid javascript from inspecting cookie - HttpOnly: true, - // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 - SameSite: samesite, + MaxAge: 120, + Secure: cookiePolicy.Secure, + HttpOnly: cookiePolicy.HTTPOnly, + SameSite: cookiePolicy.SameSite, } } @@ -84,11 +68,10 @@ func SessionName() (string, error) { return fmt.Sprintf("gotosocial-%s", punyHostname), nil } -// Session returns a new gin middleware that implements session cookies using the given -// sessionName, authentication key, and encryption key. Session name can be derived from the -// SessionName utility function in this package. -func Session(sessionName string, auth []byte, crypt []byte) gin.HandlerFunc { +// Session returns a new gin middleware that implements session cookies using the given sessionName, authentication +// key, and encryption key. Session name can be derived from the SessionName utility function in this package. +func Session(sessionName string, auth []byte, crypt []byte, cookiePolicy apiutil.CookiePolicy) gin.HandlerFunc { store := memstore.NewStore(auth, crypt) - store.Options(SessionOptions()) + store.Options(SessionOptions(cookiePolicy)) return sessions.Sessions(sessionName, store) } |
