diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index ad9ef851e..aa04f1f12 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -478,17 +478,20 @@ var Start action.GTSAction = func(ctx context.Context) error { return fmt.Errorf("error generating session name for session middleware: %w", err) } + // Configure our instance cookie policy. + cookiePolicy := apiutil.NewCookiePolicy() + var ( - authModule = api.NewAuth(state, process, idp, routerSession, sessionName) // auth/oauth paths - clientModule = api.NewClient(state, process) // api client endpoints - metricsModule = api.NewMetrics() // Metrics endpoints - healthModule = api.NewHealth(dbService.Ready) // Health check endpoints - fileserverModule = api.NewFileserver(process) // fileserver endpoints - robotsModule = api.NewRobots() // robots.txt endpoint - wellKnownModule = api.NewWellKnown(process) // .well-known endpoints - nodeInfoModule = api.NewNodeInfo(process) // nodeinfo endpoint - activityPubModule = api.NewActivityPub(dbService, process) // ActivityPub endpoints - webModule = web.New(dbService, process) // web pages + user profiles + settings panels etc + authModule = api.NewAuth(state, process, idp, routerSession, sessionName, cookiePolicy) // auth/oauth paths + clientModule = api.NewClient(state, process) // api client endpoints + metricsModule = api.NewMetrics() // Metrics endpoints + healthModule = api.NewHealth(dbService.Ready) // Health check endpoints + fileserverModule = api.NewFileserver(process) // fileserver endpoints + robotsModule = api.NewRobots() // robots.txt endpoint + wellKnownModule = api.NewWellKnown(process) // .well-known endpoints + nodeInfoModule = api.NewNodeInfo(process) // nodeinfo endpoint + activityPubModule = api.NewActivityPub(dbService, process) // ActivityPub endpoints + webModule = web.New(dbService, process, cookiePolicy) // web pages + user profiles + settings panels etc ) // Create per-route / per-grouping middlewares. diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go index e96e57b97..89e00521f 100644 --- a/cmd/gotosocial/action/testrig/testrig.go +++ b/cmd/gotosocial/action/testrig/testrig.go @@ -244,17 +244,20 @@ var Start action.GTSAction = func(ctx context.Context) error { return fmt.Errorf("error generating session name for session middleware: %w", err) } + // Configure our instance cookie policy. + cookiePolicy := apiutil.NewCookiePolicy() + var ( - authModule = api.NewAuth(state, processor, idp, routerSession, sessionName) // auth/oauth paths - clientModule = api.NewClient(state, processor) // api client endpoints - metricsModule = api.NewMetrics() // Metrics endpoints - healthModule = api.NewHealth(state.DB.Ready) // Health check endpoints - fileserverModule = api.NewFileserver(processor) // fileserver endpoints - robotsModule = api.NewRobots() // robots.txt endpoint - wellKnownModule = api.NewWellKnown(processor) // .well-known endpoints - 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 + authModule = api.NewAuth(state, processor, idp, routerSession, sessionName, cookiePolicy) // auth/oauth paths + clientModule = api.NewClient(state, processor) // api client endpoints + metricsModule = api.NewMetrics() // Metrics endpoints + healthModule = api.NewHealth(state.DB.Ready) // Health check endpoints + fileserverModule = api.NewFileserver(processor) // fileserver endpoints + robotsModule = api.NewRobots() // robots.txt endpoint + wellKnownModule = api.NewWellKnown(processor) // .well-known endpoints + nodeInfoModule = api.NewNodeInfo(processor) // nodeinfo endpoint + activityPubModule = api.NewActivityPub(state.DB, processor) // ActivityPub endpoints + webModule = web.New(state.DB, processor, cookiePolicy) // web pages + user profiles + settings panels etc ) // these should be routed in order diff --git a/internal/api/auth.go b/internal/api/auth.go index 2504acb30..fb92f3ef1 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -19,6 +19,7 @@ package api import ( "code.superseriousbusiness.org/gotosocial/internal/api/auth" + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/middleware" "code.superseriousbusiness.org/gotosocial/internal/oidc" @@ -31,6 +32,7 @@ import ( type Auth struct { routerSession *gtsmodel.RouterSession sessionName string + cookiePolicy apiutil.CookiePolicy auth *auth.Module } @@ -47,7 +49,12 @@ func (a *Auth) Route(r *router.Router, m ...gin.HandlerFunc) { Directives: []string{"private", "max-age=120"}, Vary: []string{"Accept", "Accept-Encoding"}, }) - sessionMiddleware = middleware.Session(a.sessionName, a.routerSession.Auth, a.routerSession.Crypt) + sessionMiddleware = middleware.Session( + a.sessionName, + a.routerSession.Auth, + a.routerSession.Crypt, + a.cookiePolicy, + ) ) authGroup.Use(m...) oauthGroup.Use(m...) @@ -64,10 +71,12 @@ func NewAuth( idp oidc.IDP, routerSession *gtsmodel.RouterSession, sessionName string, + cookiePolicy apiutil.CookiePolicy, ) *Auth { return &Auth{ routerSession: routerSession, sessionName: sessionName, + cookiePolicy: cookiePolicy, auth: auth.New(state, p, idp), } } diff --git a/internal/api/auth/auth_test.go b/internal/api/auth/auth_test.go index 8082f4aed..18261eea0 100644 --- a/internal/api/auth/auth_test.go +++ b/internal/api/auth/auth_test.go @@ -24,6 +24,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/admin" "code.superseriousbusiness.org/gotosocial/internal/api/auth" + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" "code.superseriousbusiness.org/gotosocial/internal/config" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/email" @@ -142,7 +143,7 @@ func (suite *AuthStandardTestSuite) newContext( // Trigger the session middleware on the context. store := memstore.NewStore(make([]byte, 32), make([]byte, 32)) - store.Options(middleware.SessionOptions()) + store.Options(middleware.SessionOptions(apiutil.NewCookiePolicy())) sessionMiddleware := sessions.Sessions("gotosocial-localhost", store) sessionMiddleware(ctx) diff --git a/internal/api/util/cookie.go b/internal/api/util/cookie.go new file mode 100644 index 000000000..29160f0af --- /dev/null +++ b/internal/api/util/cookie.go @@ -0,0 +1,83 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package util + +import ( + "net/http" + "net/url" + + "code.superseriousbusiness.org/gotosocial/internal/config" + "code.superseriousbusiness.org/gotosocial/internal/log" + "github.com/gin-gonic/gin" +) + +// CookiePolicy encompasses a number +// of security related cookie directives +// of which we want to be set consistently +// on all cookies administered by us. +type CookiePolicy struct { + Domain string + SameSite http.SameSite + HTTPOnly bool + Secure bool +} + +// NewCookiePolicy will return a new CookiePolicy{} +// object setup according to current instance config. +func NewCookiePolicy() CookiePolicy { + var sameSite http.SameSite + switch s := config.GetAdvancedCookiesSamesite(); s { + case "strict": + sameSite = http.SameSiteStrictMode + case "lax": + sameSite = http.SameSiteLaxMode + default: + log.Warnf(nil, "%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag(), s) + sameSite = http.SameSiteLaxMode + } + return CookiePolicy{ + Domain: config.GetHost(), + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 + SameSite: sameSite, + + // forbid javascript from + // inspecting cookie + HTTPOnly: true, + + // only set secure cookie directive over https + Secure: (config.GetProtocol() == "https"), + } +} + +// SetCookie will set the given cookie details according to currently configured CookiePolicy{}. +func (p *CookiePolicy) SetCookie(c *gin.Context, name, value string, maxAge int, path string) { + if path == "" { + path = "/" + } + http.SetCookie(c.Writer, &http.Cookie{ + Name: name, + Value: url.QueryEscape(value), + MaxAge: maxAge, + Path: path, + Domain: p.Domain, + SameSite: p.SameSite, + Secure: p.Secure, + HttpOnly: p.HTTPOnly, + }) +} 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) } diff --git a/internal/web/web.go b/internal/web/web.go index e42dc16c3..4494f8ff9 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -75,13 +75,15 @@ const ( type Module struct { processor *processing.Processor eTagCache cache.Cache[string, eTagCacheEntry] + cookiePolicy apiutil.CookiePolicy isURIBlocked func(context.Context, *url.URL) (bool, error) } -func New(db db.DB, processor *processing.Processor) *Module { +func New(db db.DB, processor *processing.Processor, cookiePolicy apiutil.CookiePolicy) *Module { return &Module{ processor: processor, eTagCache: newETagCache(), + cookiePolicy: cookiePolicy, isURIBlocked: db.IsURIBlocked, } } @@ -107,7 +109,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) { profileGroup.Use(middleware.SignatureCheck(m.isURIBlocked), middleware.CacheControl(middleware.CacheControlConfig{ Directives: []string{"no-store"}, })) - nollamas := middleware.NoLLaMas(m.processor.InstanceGetV1) + nollamas := middleware.NoLLaMas(m.cookiePolicy, m.processor.InstanceGetV1) profileGroup.Use(nollamas) profileGroup.Handle(http.MethodGet, "", m.profileGETHandler) // use empty path here since it's the base of the group profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler) diff --git a/testrig/config.go b/testrig/config.go index ec7b72faa..b2a073a1f 100644 --- a/testrig/config.go +++ b/testrig/config.go @@ -170,6 +170,7 @@ func testDefaults() config.Configuration { AdvancedRateLimitRequests: 0, // disabled AdvancedThrottlingMultiplier: 0, // disabled AdvancedSenderMultiplier: 0, // 1 sender only, regardless of CPU + AdvancedScraperDeterrence: envBool("GTS_ADVANCED_SCRAPER_DETERRENCE", false), SoftwareVersion: "0.0.0-testrig", @@ -178,6 +179,13 @@ func testDefaults() config.Configuration { } } +func envBool(key string, _default bool) bool { + return env(key, _default, func(value string) bool { + b, _ := strconv.ParseBool(value) + return b + }) +} + func envInt(key string, _default int) int { return env(key, _default, func(value string) int { i, _ := strconv.Atoi(value) diff --git a/web/source/nollamas/index.js b/web/source/nollamas/index.js index b32cae423..94bac4fc7 100644 --- a/web/source/nollamas/index.js +++ b/web/source/nollamas/index.js @@ -44,17 +44,20 @@ document.addEventListener('DOMContentLoaded', function() { // Read the challenge and difficulty from // data attributes on the nollamas section. const challenge = nollamas.dataset.nollamasChallenge; - const difficulty = nollamas.dataset.nollamasDifficulty; + const difficulty1 = nollamas.dataset.nollamasDifficulty1; + const difficulty2 = nollamas.dataset.nollamasDifficulty2; - console.log('challenge:', challenge); // eslint-disable-line no-console - console.log('difficulty:', difficulty); // eslint-disable-line no-console + console.log('challenge:', challenge); // eslint-disable-line no-console + console.log('difficulty1:', difficulty1); // eslint-disable-line no-console + console.log('difficulty2:', difficulty2); // eslint-disable-line no-console // Prepare the worker with task function. const worker = new Worker("/assets/dist/nollamasworker.js"); const startTime = performance.now(); worker.postMessage({ challenge: challenge, - difficulty: difficulty, + difficulty1: difficulty1, + difficulty2: difficulty2, }); // Set the main worker function. diff --git a/web/source/nollamasworker/index.js b/web/source/nollamasworker/index.js index b95ec0917..37a54349a 100644 --- a/web/source/nollamasworker/index.js +++ b/web/source/nollamasworker/index.js @@ -25,9 +25,10 @@ onmessage = async function(e) { // Get difficulty and generate the expected // zero ASCII prefix to check for in hashes. - const difficultyStr = e.data.difficulty; - const difficulty = parseInt(difficultyStr, 10); - const zeroPrefix = '0'.repeat(difficulty); + const difficulty1Str = e.data.difficulty1; + const difficulty2Str = e.data.difficulty2; + const difficulty1 = parseInt(difficulty1Str, 10); + const zeroPrefix = '0'.repeat(difficulty1); let nonce = 0; while (true) { // eslint-disable-line no-constant-condition @@ -43,8 +44,13 @@ onmessage = async function(e) { // Check if the hex encoded hash has // difficulty defined zeroes prefix. if (hashHex.startsWith(zeroPrefix)) { - postMessage({ nonce: nonce, done: true }); - break; + + // Check if the next char after zero prefix + // is specifically less than difficulty2 char. + if (hashHex.charAt(difficulty1) < difficulty2Str) { + postMessage({ nonce: nonce, done: true }); + break; + } } // Iter. diff --git a/web/template/nollamas.tmpl b/web/template/nollamas.tmpl index a02fd92f7..cc0f0f617 100644 --- a/web/template/nollamas.tmpl +++ b/web/template/nollamas.tmpl @@ -21,7 +21,8 @@

Checking you're not a creepy crawler...