diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index 6bc27a7c4..bafb23a40 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -499,6 +499,7 @@ var Start action.GTSAction = func(ctx context.Context) error { s2sLimit := middleware.RateLimit(rlLimit, exceptions) // server-to-server (AP) fsMainLimit := middleware.RateLimit(rlLimit, exceptions) // fileserver / web templates fsEmojiLimit := middleware.RateLimit(rlLimit*2, exceptions) // fileserver (emojis only, use high limit) + nollamas := middleware.NoLLaMas(state.DB) // throttling cpuMultiplier := config.GetAdvancedThrottlingMultiplier() @@ -544,7 +545,7 @@ var Start action.GTSAction = func(ctx context.Context) error { nodeInfoModule.Route(route, s2sLimit, s2sThrottle, gzip) activityPubModule.Route(route, s2sLimit, s2sThrottle, robotsDisallowAll, gzip) activityPubModule.RoutePublicKey(route, s2sLimit, pkThrottle, robotsDisallowAll, gzip) - webModule.Route(route, fsMainLimit, fsThrottle, robotsDisallowAIOnly, gzip) + webModule.Route(route, fsMainLimit, fsThrottle, robotsDisallowAIOnly, nollamas, gzip) // Finally start the main http server! if err := route.Start(); err != nil { diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go index cca4ead22..e3a764790 100644 --- a/cmd/gotosocial/action/testrig/testrig.go +++ b/cmd/gotosocial/action/testrig/testrig.go @@ -257,6 +257,8 @@ var Start action.GTSAction = func(ctx context.Context) error { nodeInfoModule = api.NewNodeInfo(processor) // nodeinfo endpoint activityPubModule = api.NewActivityPub(state.DB, processor) // ActivityPub endpoints webModule = web.New(state.DB, processor) // web pages + user profiles + settings panels etc + + nollamas = middleware.NoLLaMas(state.DB) ) // these should be routed in order @@ -271,7 +273,7 @@ var Start action.GTSAction = func(ctx context.Context) error { nodeInfoModule.Route(route) activityPubModule.Route(route) activityPubModule.RoutePublicKey(route) - webModule.Route(route) + webModule.Route(route, nollamas) // Create background cleaner. cleaner := cleaner.New(state) diff --git a/internal/middleware/nollamas.go b/internal/middleware/nollamas.go index 87c42aae4..6728e2b7b 100644 --- a/internal/middleware/nollamas.go +++ b/internal/middleware/nollamas.go @@ -18,25 +18,39 @@ package middleware import ( + "context" "crypto/sha256" + "crypto/sha512" "crypto/subtle" + "crypto/x509" "encoding/hex" "hash" "net/http" - "strconv" "time" "codeberg.org/gruf/go-byteutil" "github.com/gin-gonic/gin" - apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -//go:embed challenge.html -var challengeHTML []byte +func NoLLaMas(db db.DB) gin.HandlerFunc { + instance, err := db.GetInstanceAccount(context.Background(), "") + if err != nil { + panic(err) + } -func NoLLaMas() gin.HandlerFunc { + // Generate seed hash from + // this instance private key. + priv := instance.PrivateKey + bpriv := x509.MarshalPKCS1PrivateKey(priv) + seed := sha512.Sum512(bpriv) + + // Configure nollamas. var nollamas nollamas + nollamas.seed = seed[:] + nollamas.ttl = time.Hour + nollamas.diff = 4 return nollamas.Serve } @@ -91,9 +105,8 @@ func (m *nollamas) Serve(c *gin.Context) { // Check for a provided success token. cookie, _ := c.Cookie("gts-nollamas") - if len(cookie) == 0 || len(cookie) > encodedHashLen { - // If they provide no cookie, or - // obviously wrong cookie, just + if len(cookie) > encodedHashLen { + // Clearly invalid cookie, just // present them with new challenge. m.renderChallenge(c, challenge) return @@ -112,9 +125,11 @@ func (m *nollamas) Serve(c *gin.Context) { return } - // Check headers to see if is in-progress challenge. - nonce := c.Request.Header.Get("X-NoLLaMas-Solution") - if nonce == "" { + // Check query to see if an in-progress + // challenge solution has been provided. + query := c.Request.URL.Query() + nonce := query.Get("nollamas_solution") + if nonce == "" || len(nonce) > 20 { // No attempted solution, just // present them with new challenge. @@ -134,15 +149,19 @@ func (m *nollamas) Serve(c *gin.Context) { // Check that the first 'diff' // many chars are indeed zeroes. for i := range m.diff { - if subtle.ConstantTimeByteEq(solution[i], '0') == 0 { + if solution[i] != '0' { // They failed challenge, - // present them fail page. - m.renderFail(c) + // re-present challenge page. + m.renderChallenge(c, challenge) return } } + // Drop the solution from query. + query.Del("nollamas_solution") + c.Request.URL.RawQuery = query.Encode() + // They passed the challenge! Set success // token cookie and allow them to continue. c.SetCookie("gts-nollamas", token, int(m.ttl/time.Second), @@ -156,21 +175,11 @@ func (m *nollamas) renderChallenge(c *gin.Context, challenge string) { // our challenge page. c.Abort() - // Set the challenge we expect them to use in header. - c.Request.Header.Set("X-NoLLaMas-Challenge", challenge) - c.Request.Header.Set("X-NoLLaMas-Difficulty", strconv.FormatUint(uint64(m.diff), 10)) - - // Write the challenge HTML response to client. - apiutil.Data(c, http.StatusOK, "text/html", challengeHTML) -} - -func (m *nollamas) renderFail(c *gin.Context) { - // Don't pass to further - // handlers, they only get - // our failure page. - c.Abort() - - apiutil.Data(c, http.StatusOK, apiutil.AppJSON, []byte(`{"error": "failed nollamas challenge"}`)) + // Write the templated challenge HTML response to client. + c.HTML(http.StatusOK, "nollamas.tmpl", map[string]any{ + "challenge": challenge, + "difficulty": m.diff, + }) } func (m *nollamas) token(c *gin.Context, hash hash.Hash) string { @@ -201,7 +210,7 @@ func (m *nollamas) token(c *gin.Context, hash hash.Hash) string { byte(now), }) - // Finally append unique client request data. + // Finally, append unique client request data. userAgent := c.Request.Header.Get("User-Agent") _, _ = hash.Write(byteutil.S2B(userAgent)) clientIP := c.ClientIP() @@ -210,18 +219,3 @@ func (m *nollamas) token(c *gin.Context, hash hash.Hash) string { // Return hex encoded hash output. return hex.EncodeToString(hash.Sum(nil)) } - -// appendTime will append time as seconds in binary. -// func appendTime(b []byte, t time.Time) []byte { -// sec := t.Unix() -// return append(b, -// byte(sec>>56), -// byte(sec>>48), -// byte(sec>>40), -// byte(sec>>32), -// byte(sec>>24), -// byte(sec>>16), -// byte(sec>>8), -// byte(sec), -// ) -// } diff --git a/internal/middleware/challenge.html b/web/template/nollamas.tmpl similarity index 62% rename from internal/middleware/challenge.html rename to web/template/nollamas.tmpl index e2ae3fe08..db4d66400 100644 --- a/internal/middleware/challenge.html +++ b/web/template/nollamas.tmpl @@ -5,43 +5,6 @@ Verifying... - @@ -56,13 +19,15 @@ // Define our worker task func. const workerTask = function() { onmessage = async function(e) { + console.log('worker started'); + const challenge = e.data.challenge; const textEncoder = new TextEncoder(); - // Get difficult and generate the expected + // Get difficulty and generate the expected // zero ASCII prefix to check for in hashes. const difficultyStr = e.data.difficulty; - const difficulty = parseInt(diffStr, 10); + const difficulty = parseInt(difficultyStr, 10); const zeroPrefix = '0'.repeat(difficulty); let nonce = 0; @@ -82,64 +47,38 @@ break; } - // Send status updates. - if (i % 1000 == 0) { - postMessage({nonce: nonce}); - continue; - } - // Iter. - i++; + nonce++; } } }; // Convert the worker task function to call-able base64 blob URL. - const workerTaskBlob = new Blob(['(',workerTask.toString(),')()'], + const workerTaskBlob = new Blob(['(',workerTask.toString(),')()'], { type: 'application/javascript' }); const workerTaskURL = URL.createObjectURL(workerTaskBlob); - const req = new XMLHttpRequest(); - req.open('GET', window.location.href, false); - req.send(null); - - // Read the incoming request headers for our challenge information. - const challenge = req.getResponseHeader('X-NoLLaMas-Challenge'); - const difficulty = req.getResponseHeader('X-NoLLaMas-Difficulty'); - console.log('received challenge:${challenge} difficulty:${difficulty}'); + const challenge = '{{ .challenge }}'; + const difficulty = '{{ .difficulty }}'; + console.log('challenge:', challenge); + console.log('difficulty:', difficulty); // Prepare the worker with task function. const worker = new Worker(workerTaskURL); - - // Set the main worker function. - worker.onmessage = function (e) { - if (e.data.done) { - console.log("solution found for: ${e.data.nonce}"); - - fetch(window.location.href, { - method: 'GET', - headers: { 'X-NoLLaMas-Solution': e.data.nonce }, - credentials: 'include' - }).then(response => { - console.log("Server response:", response.status); - return response.text().then(() => { - setTimeout(() => { - window.location.href = window.location.href; - }, 300); - }); - }).catch(error => { - console.error('Error on refresh:', error); - }); - } else if (e.data.progress) { - console.log("search progress: ${e.data.nonce}"); - } - }; - - // Post our challenge. worker.postMessage({ challenge: challenge, difficulty: difficulty, }); + + // Set the main worker function. + worker.onmessage = function (e) { + if (e.data.done) { + console.log('solution found for:', e.data.nonce); + let url = new URL(window.location.href); + url.searchParams.append('nollamas_solution', e.data.nonce); + window.location.href = url.toString(); + } + };