| 
									
										
										
										
											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_test | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"crypto/sha256" | 
					
						
							|  |  |  | 	"encoding/hex" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/http/httptest" | 
					
						
							|  |  |  | 	"slices" | 
					
						
							|  |  |  | 	"strconv" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 	"testing" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/api/model" | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/config" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/middleware" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/router" | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-byteutil" | 
					
						
							|  |  |  | 	"github.com/gin-gonic/gin" | 
					
						
							|  |  |  | 	"github.com/stretchr/testify/assert" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestNoLLaMasMiddleware(t *testing.T) { | 
					
						
							|  |  |  | 	// Gin test engine. | 
					
						
							|  |  |  | 	e := gin.New() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Setup necessary configuration variables. | 
					
						
							|  |  |  | 	config.SetAdvancedScraperDeterrence(true) | 
					
						
							|  |  |  | 	config.SetWebTemplateBaseDir("../../web/template") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Load templates into engine. | 
					
						
							|  |  |  | 	err := router.LoadTemplates(e) | 
					
						
							|  |  |  | 	assert.NoError(t, err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Add middleware to the gin engine handler stack. | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	middleware := middleware.NoLLaMas(apiutil.CookiePolicy{}, getInstanceV1) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	e.Use(middleware) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Set test handler we can | 
					
						
							|  |  |  | 	// easily check if was used. | 
					
						
							|  |  |  | 	e.Handle("GET", "/", testHandler) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Test with differing user-agents. | 
					
						
							|  |  |  | 	for _, userAgent := range []string{ | 
					
						
							|  |  |  | 		"CURL", | 
					
						
							|  |  |  | 		"Mozilla FireSox", | 
					
						
							|  |  |  | 		"Google Gnome", | 
					
						
							|  |  |  | 	} { | 
					
						
							|  |  |  | 		testNoLLaMasMiddleware(t, e, userAgent) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) { | 
					
						
							|  |  |  | 	// Prepare a test request for gin engine. | 
					
						
							|  |  |  | 	r := httptest.NewRequest("GET", "/", nil) | 
					
						
							|  |  |  | 	r.Header.Set("User-Agent", userAgent) | 
					
						
							|  |  |  | 	rw := httptest.NewRecorder() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Pass req through | 
					
						
							|  |  |  | 	// engine handler. | 
					
						
							|  |  |  | 	e.ServeHTTP(rw, r) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Get http result. | 
					
						
							|  |  |  | 	res := rw.Result() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// It should have been stopped | 
					
						
							|  |  |  | 	// by middleware and NOT used | 
					
						
							|  |  |  | 	// the expected test handler. | 
					
						
							|  |  |  | 	ok := usedTestHandler(res) | 
					
						
							|  |  |  | 	assert.False(t, ok) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Read entire response body. | 
					
						
							|  |  |  | 	b, err := io.ReadAll(res.Body) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		panic(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var challenge string | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 	var difficulty uint64 | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Parse output body and find the challenge / difficulty. | 
					
						
							|  |  |  | 	for _, line := range strings.Split(string(b), "\n") { | 
					
						
							|  |  |  | 		line = strings.TrimSpace(line) | 
					
						
							|  |  |  | 		switch { | 
					
						
							|  |  |  | 		case strings.HasPrefix(line, "data-nollamas-challenge=\""): | 
					
						
							|  |  |  | 			line = line[25:] | 
					
						
							|  |  |  | 			line = line[:len(line)-1] | 
					
						
							|  |  |  | 			challenge = line | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 		case strings.HasPrefix(line, "data-nollamas-difficulty=\""): | 
					
						
							|  |  |  | 			line = line[26:] | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 			line = line[:len(line)-1] | 
					
						
							|  |  |  | 			var err error | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 			difficulty, err = strconv.ParseUint(line, 10, 8) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 			assert.NoError(t, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Ensure valid posed challenge. | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 	assert.NotZero(t, difficulty) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	assert.NotEmpty(t, challenge) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Prepare a test request for gin engine. | 
					
						
							|  |  |  | 	r = httptest.NewRequest("GET", "/", nil) | 
					
						
							|  |  |  | 	r.Header.Set("User-Agent", userAgent) | 
					
						
							|  |  |  | 	rw = httptest.NewRecorder() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Now compute and set solution query paramater. | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 	solution := computeSolution(challenge, difficulty) | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	r.URL.RawQuery = "nollamas_solution=" + solution | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	t.Logf("challenge=%s", challenge) | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 	t.Logf("difficulty=%d", difficulty) | 
					
						
							| 
									
										
										
										
											2025-04-29 13:57:26 +00:00
										 |  |  | 	t.Logf("solution=%s", solution) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 	// Pass req through | 
					
						
							|  |  |  | 	// engine handler. | 
					
						
							|  |  |  | 	e.ServeHTTP(rw, r) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Get http result. | 
					
						
							|  |  |  | 	res = rw.Result() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Should have received redirect. | 
					
						
							|  |  |  | 	uri, err := res.Location() | 
					
						
							|  |  |  | 	assert.NoError(t, err) | 
					
						
							|  |  |  | 	assert.Equal(t, uri.String(), "/") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Ensure our expected solution cookie (to bypass challenge) was set. | 
					
						
							|  |  |  | 	ok = slices.ContainsFunc(res.Cookies(), func(c *http.Cookie) bool { | 
					
						
							|  |  |  | 		return c.Name == "gts-nollamas" | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	assert.True(t, ok) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // computeSolution does the functional equivalent of our nollamas workerTask.js. | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | func computeSolution(challenge string, diff uint64) string { | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | outer: | 
					
						
							|  |  |  | 	for i := 0; ; i++ { | 
					
						
							|  |  |  | 		solution := strconv.Itoa(i) | 
					
						
							|  |  |  | 		combined := challenge + solution | 
					
						
							|  |  |  | 		hash := sha256.Sum256(byteutil.S2B(combined)) | 
					
						
							|  |  |  | 		encoded := hex.EncodeToString(hash[:]) | 
					
						
							| 
									
										
										
										
											2025-05-03 16:45:25 +00:00
										 |  |  | 		for i := range diff { | 
					
						
							| 
									
										
										
										
											2025-04-28 20:12:27 +00:00
										 |  |  | 			if encoded[i] != '0' { | 
					
						
							|  |  |  | 				continue outer | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return solution | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // usedTestHandler returns whether testHandler() was used. | 
					
						
							|  |  |  | func usedTestHandler(res *http.Response) bool { | 
					
						
							|  |  |  | 	return res.Header.Get("test-handler") == "ok" | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func testHandler(c *gin.Context) { | 
					
						
							|  |  |  | 	c.Writer.Header().Set("test-handler", "ok") | 
					
						
							|  |  |  | 	c.Writer.WriteHeader(http.StatusOK) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func getInstanceV1(context.Context) (*model.InstanceV1, gtserror.WithCode) { | 
					
						
							|  |  |  | 	return &model.InstanceV1{}, nil | 
					
						
							|  |  |  | } |