| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | // 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 <http://www.gnu.org/licenses/>. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package middleware | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"crypto/rand" | 
					
						
							|  |  |  | 	"crypto/sha256" | 
					
						
							|  |  |  | 	"crypto/subtle" | 
					
						
							|  |  |  | 	"encoding/hex" | 
					
						
							|  |  |  | 	"hash" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	"strconv" | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	apimodel "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/gtscontext" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/log" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/oauth" | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	"codeberg.org/gruf/go-bitutil" | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	"codeberg.org/gruf/go-byteutil" | 
					
						
							|  |  |  | 	"github.com/gin-gonic/gin" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // NoLLaMas returns a piece of HTTP middleware that provides a deterrence | 
					
						
							|  |  |  | // on routes it is applied to, against bots and scrapers. It generates a | 
					
						
							|  |  |  | // unique but deterministic challenge for each HTTP client within an hour | 
					
						
							|  |  |  | // TTL that requires a proof-of-work solution to pass onto the next handler. | 
					
						
							|  |  |  | // On successful solution, the client is provided a cookie that allows them | 
					
						
							|  |  |  | // to bypass this check within that hour TTL. The outcome of this is that it | 
					
						
							|  |  |  | // should make scraping of these endpoints economically unfeasible, when enabled, | 
					
						
							|  |  |  | // and with an absurdly minimal performance impact. The downside is that it | 
					
						
							|  |  |  | // requires javascript to be enabled on the client to pass the middleware check. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Heavily inspired by: https://github.com/TecharoHQ/anubis | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | func NoLLaMas( | 
					
						
							|  |  |  | 	cookiePolicy apiutil.CookiePolicy, | 
					
						
							|  |  |  | 	getInstanceV1 func(context.Context) (*apimodel.InstanceV1, gtserror.WithCode), | 
					
						
							|  |  |  | ) gin.HandlerFunc { | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-07 11:59:39 +00:00
										 |  |  | 	if !config.GetAdvancedScraperDeterrenceEnabled() { | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 		// NoLLaMas middleware disabled. | 
					
						
							|  |  |  | 		return func(*gin.Context) {} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	var seed [32]byte | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Read random data for the token seed. | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	_, err := io.ReadFull(rand.Reader, seed[:]) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		panic(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Configure nollamas. | 
					
						
							|  |  |  | 	var nollamas nollamas | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	nollamas.entropy = seed | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	nollamas.ttl = time.Hour | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	nollamas.rounds = config.GetAdvancedScraperDeterrenceDifficulty() | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	nollamas.getInstanceV1 = getInstanceV1 | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	nollamas.policy = cookiePolicy | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	return nollamas.Serve | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | // i.e. hash slice length. | 
					
						
							|  |  |  | const hashLen = sha256.Size | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // i.e. hex.EncodedLen(hashLen). | 
					
						
							|  |  |  | const encodedHashLen = 2 * hashLen | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | // hashWithBufs encompasses a hash along | 
					
						
							|  |  |  | // with the necessary buffers to generate | 
					
						
							|  |  |  | // a hashsum and then encode that sum. | 
					
						
							|  |  |  | type hashWithBufs struct { | 
					
						
							|  |  |  | 	hash hash.Hash | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	hbuf [hashLen]byte | 
					
						
							|  |  |  | 	ebuf [encodedHashLen]byte | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // write is a passthrough to hash.Hash{}.Write(). | 
					
						
							|  |  |  | func (h *hashWithBufs) write(b []byte) { | 
					
						
							|  |  |  | 	_, _ = h.hash.Write(b) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // writeString is a passthrough to hash.Hash{}.Write([]byte(s)). | 
					
						
							|  |  |  | func (h *hashWithBufs) writeString(s string) { | 
					
						
							|  |  |  | 	_, _ = h.hash.Write(byteutil.S2B(s)) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // EncodedSum returns the hex encoded sum of hash.Sum(). | 
					
						
							|  |  |  | func (h *hashWithBufs) EncodedSum() string { | 
					
						
							|  |  |  | 	_ = h.hash.Sum(h.hbuf[:0]) | 
					
						
							|  |  |  | 	hex.Encode(h.ebuf[:], h.hbuf[:]) | 
					
						
							|  |  |  | 	return string(h.ebuf[:]) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Reset will reset hash and buffers. | 
					
						
							|  |  |  | func (h *hashWithBufs) Reset() { | 
					
						
							|  |  |  | 	h.ebuf = [encodedHashLen]byte{} | 
					
						
							|  |  |  | 	h.hbuf = [hashLen]byte{} | 
					
						
							|  |  |  | 	h.hash.Reset() | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type nollamas struct { | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	// our instance cookie policy. | 
					
						
							|  |  |  | 	policy apiutil.CookiePolicy | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// unique entropy | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	// to prevent hashes | 
					
						
							|  |  |  | 	// being guessable | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	entropy [32]byte | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// success cookie TTL | 
					
						
							|  |  |  | 	ttl time.Duration | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// rounds determines roughly how | 
					
						
							|  |  |  | 	// many hash-encode rounds each | 
					
						
							|  |  |  | 	// client is required to complete. | 
					
						
							|  |  |  | 	rounds uint32 | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// extra fields required for | 
					
						
							|  |  |  | 	// our template rendering. | 
					
						
							|  |  |  | 	getInstanceV1 func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (m *nollamas) Serve(c *gin.Context) { | 
					
						
							|  |  |  | 	if c.Request.Method != http.MethodGet { | 
					
						
							|  |  |  | 		// Only interested in protecting | 
					
						
							|  |  |  | 		// crawlable 'GET' endpoints. | 
					
						
							|  |  |  | 		c.Next() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Extract request context. | 
					
						
							|  |  |  | 	ctx := c.Request.Context() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if ctx.Value(oauth.SessionAuthorizedToken) != nil { | 
					
						
							|  |  |  | 		// Don't guard against requests | 
					
						
							|  |  |  | 		// providing valid OAuth tokens. | 
					
						
							|  |  |  | 		c.Next() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if gtscontext.HTTPSignature(ctx) != "" { | 
					
						
							|  |  |  | 		// Don't guard against requests | 
					
						
							|  |  |  | 		// providing HTTP signatures. | 
					
						
							|  |  |  | 		c.Next() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// Prepare new hash with buffers. | 
					
						
							|  |  |  | 	hash := hashWithBufs{hash: sha256.New()} | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Extract client fingerprint data. | 
					
						
							|  |  |  | 	userAgent := c.GetHeader("User-Agent") | 
					
						
							|  |  |  | 	clientIP := c.ClientIP() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Generate a unique token for this request, | 
					
						
							|  |  |  | 	// only valid for a period of now +- m.ttl. | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	token := m.getToken(&hash, userAgent, clientIP) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Check for a provided success token. | 
					
						
							|  |  |  | 	cookie, _ := c.Cookie("gts-nollamas") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check whether passed cookie | 
					
						
							|  |  |  | 	// is the expected success token. | 
					
						
							|  |  |  | 	if subtle.ConstantTimeCompare( | 
					
						
							|  |  |  | 		byteutil.S2B(cookie), | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 		byteutil.S2B(token), | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	) == 1 { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// They passed us a valid, expected | 
					
						
							|  |  |  | 		// token. They already passed checks. | 
					
						
							|  |  |  | 		c.Next() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 	// From here-on out, all | 
					
						
							|  |  |  | 	// possibilities are handled | 
					
						
							|  |  |  | 	// by us. Prevent further http | 
					
						
							|  |  |  | 	// handlers from being called. | 
					
						
							|  |  |  | 	c.Abort() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// Generate challenge for this unique (yet deterministic) token, | 
					
						
							|  |  |  | 	// returning seed, wanted 'challenge' result and expected solution. | 
					
						
							|  |  |  | 	seed, challenge, solution := m.getChallenge(&hash, token) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	// Prepare new log entry. | 
					
						
							|  |  |  | 	l := log.WithContext(ctx). | 
					
						
							|  |  |  | 		WithField("userAgent", userAgent). | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 		WithField("seed", seed). | 
					
						
							|  |  |  | 		WithField("rounds", solution) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Extract and parse query. | 
					
						
							|  |  |  | 	query := c.Request.URL.Query() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check query to see if an in-progress | 
					
						
							|  |  |  | 	// challenge solution has been provided. | 
					
						
							|  |  |  | 	nonce := query.Get("nollamas_solution") | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	if nonce == "" { | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 		// No solution given, likely new client! | 
					
						
							|  |  |  | 		// Simply present them with challenge. | 
					
						
							|  |  |  | 		m.renderChallenge(c, seed, challenge) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 		l.Info("posing new challenge") | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// Check nonce matches expected. | 
					
						
							|  |  |  | 	if subtle.ConstantTimeCompare( | 
					
						
							|  |  |  | 		byteutil.S2B(solution), | 
					
						
							|  |  |  | 		byteutil.S2B(nonce), | 
					
						
							|  |  |  | 	) != 1 { | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 		// Their nonce failed, re-challenge them. | 
					
						
							|  |  |  | 		m.renderChallenge(c, challenge, solution) | 
					
						
							|  |  |  | 		l.Infof("invalid solution provided: %s", nonce) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	l.Info("challenge passed") | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Drop solution query and encode. | 
					
						
							|  |  |  | 	query.Del("nollamas_solution") | 
					
						
							|  |  |  | 	c.Request.URL.RawQuery = query.Encode() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// They passed the challenge! Set success token | 
					
						
							|  |  |  | 	// cookie and allow them to continue to next handlers. | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	m.policy.SetCookie(c, "gts-nollamas", token, int(m.ttl/time.Second), "/") | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	c.Redirect(http.StatusTemporaryRedirect, c.Request.URL.RequestURI()) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | func (m *nollamas) renderChallenge(c *gin.Context, seed, challenge string) { | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	// Fetch current instance information for templating vars. | 
					
						
							|  |  |  | 	instance, errWithCode := m.getInstanceV1(c.Request.Context()) | 
					
						
							|  |  |  | 	if errWithCode != nil { | 
					
						
							|  |  |  | 		apiutil.ErrorHandler(c, errWithCode, m.getInstanceV1) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Write templated challenge response to client. | 
					
						
							|  |  |  | 	apiutil.TemplateWebPage(c, apiutil.WebPage{ | 
					
						
							|  |  |  | 		Template: "nollamas.tmpl", | 
					
						
							|  |  |  | 		Instance: instance, | 
					
						
							|  |  |  | 		Stylesheets: []string{ | 
					
						
							|  |  |  | 			"/assets/dist/nollamas.css", | 
					
						
							|  |  |  | 			// Include fork-awesome stylesheet | 
					
						
							|  |  |  | 			// to get nice loading spinner. | 
					
						
							|  |  |  | 			"/assets/Fork-Awesome/css/fork-awesome.min.css", | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		Extra: map[string]any{ | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 			"seed":      seed, | 
					
						
							|  |  |  | 			"challenge": challenge, | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 		Javascript: []apiutil.JavascriptEntry{ | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				Src:   "/assets/dist/nollamas.js", | 
					
						
							|  |  |  | 				Defer: true, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | // getToken generates a unique yet deterministic token for given HTTP request | 
					
						
							|  |  |  | // details, seeded by runtime generated entropy data and ttl rounded timestamp. | 
					
						
							|  |  |  | func (m *nollamas) getToken(hash *hashWithBufs, userAgent, clientIP string) string { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Reset before | 
					
						
							|  |  |  | 	// using hash. | 
					
						
							|  |  |  | 	hash.Reset() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Use our unique entropy to seed hash, | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	// to ensure we have cryptographically | 
					
						
							|  |  |  | 	// unique, yet deterministic, tokens | 
					
						
							|  |  |  | 	// generated for a given http client. | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	hash.write(m.entropy[:]) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Also seed the generated input with | 
					
						
							|  |  |  | 	// current time rounded to TTL, so our | 
					
						
							|  |  |  | 	// single comparison handles expiries. | 
					
						
							|  |  |  | 	now := time.Now().Round(m.ttl).Unix() | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	hash.write([]byte{ | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 		byte(now >> 56), | 
					
						
							|  |  |  | 		byte(now >> 48), | 
					
						
							|  |  |  | 		byte(now >> 40), | 
					
						
							|  |  |  | 		byte(now >> 32), | 
					
						
							|  |  |  | 		byte(now >> 24), | 
					
						
							|  |  |  | 		byte(now >> 16), | 
					
						
							|  |  |  | 		byte(now >> 8), | 
					
						
							|  |  |  | 		byte(now), | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// Append client request data. | 
					
						
							|  |  |  | 	hash.writeString(userAgent) | 
					
						
							|  |  |  | 	hash.writeString(clientIP) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// Return hex encoded hash. | 
					
						
							|  |  |  | 	return hash.EncodedSum() | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | // getChallenge prepares a new challenge given the deterministic input token for this request. | 
					
						
							|  |  |  | // it will return an input seed string, a challenge string which is the end result the client | 
					
						
							|  |  |  | // should be looking for, and the solution for this such that challenge = hex(sha256(seed + solution)). | 
					
						
							|  |  |  | // the solution will always be a string-encoded 64bit integer calculated from m.rounds + random jitter. | 
					
						
							|  |  |  | func (m *nollamas) getChallenge(hash *hashWithBufs, token string) (seed, challenge, solution string) { | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// For their unique seed string just use a | 
					
						
							|  |  |  | 	// single portion of their 'success' token. | 
					
						
							|  |  |  | 	// SHA256 is not yet cracked, this is not an | 
					
						
							|  |  |  | 	// application of a hash requiring serious | 
					
						
							|  |  |  | 	// cryptographic security and it rotates on | 
					
						
							|  |  |  | 	// a TTL basis, so it should be fine. | 
					
						
							|  |  |  | 	seed = token[:len(token)/4] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// BEFORE resetting the hash, get the last | 
					
						
							|  |  |  | 	// two bytes of NON-hex-encoded data from | 
					
						
							|  |  |  | 	// token generation to use for random jitter. | 
					
						
							|  |  |  | 	// This is taken from the end of the hash as | 
					
						
							|  |  |  | 	// this is the "unseen" end part of token. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// (if we used hex-encoded data it would | 
					
						
							|  |  |  | 	// only ever be '0-9' or 'a-z' ASCII chars). | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// Security-wise, same applies as-above. | 
					
						
							|  |  |  | 	jitter := int16(hash.hbuf[len(hash.hbuf)-2]) | | 
					
						
							|  |  |  | 		int16(hash.hbuf[len(hash.hbuf)-1])<<8 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var rounds int64 | 
					
						
							|  |  |  | 	switch { | 
					
						
							|  |  |  | 	// For some small percentage of | 
					
						
							|  |  |  | 	// clients we purposely low-ball | 
					
						
							|  |  |  | 	// their rounds required, to make | 
					
						
							|  |  |  | 	// it so gaming it with a starting | 
					
						
							|  |  |  | 	// nonce value may suddenly fail. | 
					
						
							|  |  |  | 	case jitter%37 == 0: | 
					
						
							|  |  |  | 		rounds = int64(m.rounds/10) + int64(jitter/10) | 
					
						
							|  |  |  | 	case jitter%31 == 0: | 
					
						
							|  |  |  | 		rounds = int64(m.rounds/5) + int64(jitter/5) | 
					
						
							|  |  |  | 	case jitter%29 == 0: | 
					
						
							|  |  |  | 		rounds = int64(m.rounds/3) + int64(jitter/3) | 
					
						
							|  |  |  | 	case jitter%13 == 0: | 
					
						
							|  |  |  | 		rounds = int64(m.rounds/2) + int64(jitter/2) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Determine an appropriate number of hash rounds | 
					
						
							|  |  |  | 	// we want the client to perform on input seed. This | 
					
						
							|  |  |  | 	// is determined as configured m.rounds +- jitter. | 
					
						
							|  |  |  | 	// This will be the 'solution' to create 'challenge'. | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 		rounds = int64(m.rounds) + int64(jitter) //nolint:gosec | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-26 11:57:50 +02:00
										 |  |  | 	// Encode (positive) determined hash rounds as string. | 
					
						
							|  |  |  | 	solution = strconv.FormatInt(bitutil.Abs64(rounds), 10) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Reset before | 
					
						
							|  |  |  | 	// using hash. | 
					
						
							|  |  |  | 	hash.Reset() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Calculate the expected result | 
					
						
							|  |  |  | 	// of hex(sha256(seed + solution)), | 
					
						
							|  |  |  | 	// i.e. the proposed 'challenge'. | 
					
						
							|  |  |  | 	hash.writeString(seed) | 
					
						
							|  |  |  | 	hash.writeString(solution) | 
					
						
							|  |  |  | 	challenge = hash.EncodedSum() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | } |