| 
									
										
										
										
											2025-04-22 16:06:51 +01: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 ( | 
					
						
							| 
									
										
										
										
											2025-04-23 12:17:47 +02:00
										 |  |  | 	"context" | 
					
						
							|  |  |  | 	"crypto/rand" | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	"crypto/sha256" | 
					
						
							|  |  |  | 	"crypto/subtle" | 
					
						
							|  |  |  | 	"encoding/hex" | 
					
						
							|  |  |  | 	"hash" | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-byteutil" | 
					
						
							|  |  |  | 	"github.com/gin-gonic/gin" | 
					
						
							| 
									
										
										
										
											2025-04-23 12:17:47 +02:00
										 |  |  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 
					
						
							|  |  |  | 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | 
					
						
							| 
									
										
										
										
											2025-04-23 12:17:47 +02:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 
					
						
							| 
									
										
										
										
											2025-04-23 12:38:26 +01:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | func NoLLaMas(getInstanceV1 func(context.Context) (*apimodel.InstanceV1, gtserror.WithCode)) gin.HandlerFunc { | 
					
						
							|  |  |  | 	if !config.GetAdvancedScraperDeterrence() { | 
					
						
							|  |  |  | 		// NoLLaMas middleware disabled. | 
					
						
							|  |  |  | 		return func(*gin.Context) {} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	seed := make([]byte, 32) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Read random data for the token seed. | 
					
						
							|  |  |  | 	_, err := io.ReadFull(rand.Reader, seed) | 
					
						
							| 
									
										
										
										
											2025-04-23 12:17:47 +02:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		panic(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 	// Configure nollamas. | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	var nollamas nollamas | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	nollamas.seed = seed | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 	nollamas.ttl = time.Hour | 
					
						
							|  |  |  | 	nollamas.diff = 4 | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	nollamas.getInstanceV1 = getInstanceV1 | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	return nollamas.Serve | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // i.e. outputted hash slice length. | 
					
						
							| 
									
										
										
										
											2025-04-23 12:46:30 +01:00
										 |  |  | const hashLen = sha256.Size | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | // i.e. hex.EncodedLen(hashLen). | 
					
						
							|  |  |  | const encodedHashLen = 2 * hashLen | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01: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 | 
					
						
							|  |  |  | 	hbuf []byte | 
					
						
							|  |  |  | 	ebuf []byte | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | type nollamas struct { | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	seed []byte // unique token seed | 
					
						
							|  |  |  | 	ttl  time.Duration | 
					
						
							|  |  |  | 	diff uint8 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	getInstanceV1 func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (m *nollamas) Serve(c *gin.Context) { | 
					
						
							|  |  |  | 	if c.Request.Method != http.MethodGet { | 
					
						
							|  |  |  | 		// Only interested in protecting | 
					
						
							|  |  |  | 		// crawlable 'GET' endpoints. | 
					
						
							|  |  |  | 		c.Next() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if _, ok := c.Get(oauth.SessionAuthorizedToken); ok { | 
					
						
							|  |  |  | 		// Don't guard against requests | 
					
						
							|  |  |  | 		// providing valid OAuth tokens. | 
					
						
							|  |  |  | 		c.Next() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	// Prepare hash + buffers. | 
					
						
							|  |  |  | 	hash := hashWithBufs{ | 
					
						
							|  |  |  | 		hash: sha256.New(), | 
					
						
							|  |  |  | 		hbuf: make([]byte, 0, hashLen), | 
					
						
							|  |  |  | 		ebuf: make([]byte, encodedHashLen), | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Generate a unique token for | 
					
						
							|  |  |  | 	// this request only valid for | 
					
						
							|  |  |  | 	// a period of now +- m.ttl. | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	token := m.token(c, &hash) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// For unique challenge string just use a | 
					
						
							|  |  |  | 	// portion of their unique '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. | 
					
						
							|  |  |  | 	challenge := token[:len(token)/2] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 12:38:26 +01:00
										 |  |  | 	// Prepare new log entry with challenge. | 
					
						
							|  |  |  | 	l := log.WithContext(c.Request.Context()) | 
					
						
							|  |  |  | 	l = l.WithField("challenge", challenge) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01: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(token), | 
					
						
							|  |  |  | 		byteutil.S2B(cookie), | 
					
						
							|  |  |  | 	) == 1 { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// They passed us a valid, expected | 
					
						
							|  |  |  | 		// token. They already passed checks. | 
					
						
							|  |  |  | 		c.Next() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 	// 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 { | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-22 19:57:38 +01:00
										 |  |  | 		// An invalid solution string, just | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 		// present them with new challenge. | 
					
						
							| 
									
										
										
										
											2025-04-23 12:38:26 +01:00
										 |  |  | 		l.Info("posing new challenge") | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 		m.renderChallenge(c, challenge) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	// Reset the hash. | 
					
						
							|  |  |  | 	hash.hash.Reset() | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Hash and encode input challenge with | 
					
						
							|  |  |  | 	// proposed nonce as a possible solution. | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	hash.hash.Write(byteutil.S2B(challenge)) | 
					
						
							|  |  |  | 	hash.hash.Write(byteutil.S2B(nonce)) | 
					
						
							|  |  |  | 	hash.hbuf = hash.hash.Sum(hash.hbuf[:0]) | 
					
						
							|  |  |  | 	hex.Encode(hash.ebuf, hash.hbuf) | 
					
						
							|  |  |  | 	solution := hash.ebuf | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Check that the first 'diff' | 
					
						
							|  |  |  | 	// many chars are indeed zeroes. | 
					
						
							|  |  |  | 	for i := range m.diff { | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 		if solution[i] != '0' { | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// They failed challenge, | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 			// re-present challenge page. | 
					
						
							| 
									
										
										
										
											2025-04-23 12:38:26 +01:00
										 |  |  | 			l.Warn("invalid solution provided") | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 			m.renderChallenge(c, challenge) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 			return | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 12:38:26 +01:00
										 |  |  | 	l.Infof("challenge passed: %s", nonce) | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 12:38:26 +01:00
										 |  |  | 	// They passed the challenge! Set success token | 
					
						
							|  |  |  | 	// cookie and allow them to continue to next handlers. | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	c.SetCookie("gts-nollamas", token, int(m.ttl/time.Second), | 
					
						
							|  |  |  | 		"", "", false, false) | 
					
						
							|  |  |  | 	c.Next() | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (m *nollamas) renderChallenge(c *gin.Context, challenge string) { | 
					
						
							|  |  |  | 	// Don't pass to further | 
					
						
							|  |  |  | 	// handlers, they only get | 
					
						
							|  |  |  | 	// our challenge page. | 
					
						
							|  |  |  | 	c.Abort() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	// Fetch current instance information for templating vars. | 
					
						
							|  |  |  | 	instance, errWithCode := m.getInstanceV1(c.Request.Context()) | 
					
						
							| 
									
										
										
										
											2025-04-23 12:17:47 +02:00
										 |  |  | 	if errWithCode != nil { | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 		apiutil.ErrorHandler(c, errWithCode, m.getInstanceV1) | 
					
						
							| 
									
										
										
										
											2025-04-23 12:17:47 +02:00
										 |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 18:01:01 +01:00
										 |  |  | 	// Write templated challenge response to client. | 
					
						
							| 
									
										
										
										
											2025-04-23 12:17:47 +02:00
										 |  |  | 	apiutil.TemplateWebPage(c, apiutil.WebPage{ | 
					
						
							|  |  |  | 		Template: "nollamas.tmpl", | 
					
						
							|  |  |  | 		Instance: instance, | 
					
						
							|  |  |  | 		Extra: map[string]any{ | 
					
						
							|  |  |  | 			"challenge":  challenge, | 
					
						
							|  |  |  | 			"difficulty": m.diff, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		Javascript: []apiutil.JavascriptEntry{ | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				Src:   "/assets/dist/nollamas.js", | 
					
						
							|  |  |  | 				Defer: true, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-04-22 19:26:41 +01:00
										 |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | func (m *nollamas) token(c *gin.Context, hash *hashWithBufs) string { | 
					
						
							| 
									
										
										
										
											2025-04-23 12:38:26 +01:00
										 |  |  | 	// Use our unique seed to seed hash, | 
					
						
							|  |  |  | 	// to ensure we have cryptographically | 
					
						
							|  |  |  | 	// unique, yet deterministic, tokens | 
					
						
							|  |  |  | 	// generated for a given http client. | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	hash.hash.Write(m.seed) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Include difficulty level in | 
					
						
							|  |  |  | 	// hash input data so if config | 
					
						
							|  |  |  | 	// changes then token invalidates. | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	hash.hash.Write([]byte{m.diff}) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Also seed the generated input with | 
					
						
							| 
									
										
										
										
											2025-04-22 19:57:38 +01:00
										 |  |  | 	// current time rounded to TTL, so our | 
					
						
							|  |  |  | 	// single comparison handles expiries. | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	now := time.Now().Round(m.ttl).Unix() | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	hash.hash.Write([]byte{ | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01: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-04-22 19:26:41 +01:00
										 |  |  | 	// Finally, append unique client request data. | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	userAgent := c.Request.Header.Get("User-Agent") | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	hash.hash.Write(byteutil.S2B(userAgent)) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 	clientIP := c.ClientIP() | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	hash.hash.Write(byteutil.S2B(clientIP)) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Return hex encoded hash output. | 
					
						
							| 
									
										
										
										
											2025-04-23 11:53:19 +01:00
										 |  |  | 	hash.hbuf = hash.hash.Sum(hash.hbuf[:0]) | 
					
						
							|  |  |  | 	hex.Encode(hash.ebuf, hash.hbuf) | 
					
						
							|  |  |  | 	return string(hash.ebuf) | 
					
						
							| 
									
										
										
										
											2025-04-22 16:06:51 +01:00
										 |  |  | } |