mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-01 15:22:24 -05:00
add config option for scraper deterrence
This commit is contained in:
parent
db4a6e746c
commit
924280ac0b
4 changed files with 80 additions and 39 deletions
|
|
@ -175,6 +175,7 @@ type Configuration struct {
|
||||||
AdvancedSenderMultiplier int `name:"advanced-sender-multiplier" usage:"Multiplier to use per cpu for batching outgoing fedi messages. 0 or less turns batching off (not recommended)."`
|
AdvancedSenderMultiplier int `name:"advanced-sender-multiplier" usage:"Multiplier to use per cpu for batching outgoing fedi messages. 0 or less turns batching off (not recommended)."`
|
||||||
AdvancedCSPExtraURIs []string `name:"advanced-csp-extra-uris" usage:"Additional URIs to allow when building content-security-policy for media + images."`
|
AdvancedCSPExtraURIs []string `name:"advanced-csp-extra-uris" usage:"Additional URIs to allow when building content-security-policy for media + images."`
|
||||||
AdvancedHeaderFilterMode string `name:"advanced-header-filter-mode" usage:"Set incoming request header filtering mode."`
|
AdvancedHeaderFilterMode string `name:"advanced-header-filter-mode" usage:"Set incoming request header filtering mode."`
|
||||||
|
AdvancedScraperDeterrence bool `name:"advanced-scraper-deterrence" usage:"Enable proof-of-work based scraper deterrence on profile / status pages"`
|
||||||
|
|
||||||
// HTTPClient configuration vars.
|
// HTTPClient configuration vars.
|
||||||
HTTPClient HTTPClientConfiguration `name:"http-client"`
|
HTTPClient HTTPClientConfiguration `name:"http-client"`
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ var Defaults = Configuration{
|
||||||
AdvancedSenderMultiplier: 2, // 2 senders per CPU
|
AdvancedSenderMultiplier: 2, // 2 senders per CPU
|
||||||
AdvancedCSPExtraURIs: []string{},
|
AdvancedCSPExtraURIs: []string{},
|
||||||
AdvancedHeaderFilterMode: RequestHeaderFilterModeDisabled,
|
AdvancedHeaderFilterMode: RequestHeaderFilterModeDisabled,
|
||||||
|
AdvancedScraperDeterrence: false,
|
||||||
|
|
||||||
Cache: CacheConfiguration{
|
Cache: CacheConfiguration{
|
||||||
// Rough memory target that the total
|
// Rough memory target that the total
|
||||||
|
|
|
||||||
|
|
@ -2906,6 +2906,31 @@ func GetAdvancedHeaderFilterMode() string { return global.GetAdvancedHeaderFilte
|
||||||
// SetAdvancedHeaderFilterMode safely sets the value for global configuration 'AdvancedHeaderFilterMode' field
|
// SetAdvancedHeaderFilterMode safely sets the value for global configuration 'AdvancedHeaderFilterMode' field
|
||||||
func SetAdvancedHeaderFilterMode(v string) { global.SetAdvancedHeaderFilterMode(v) }
|
func SetAdvancedHeaderFilterMode(v string) { global.SetAdvancedHeaderFilterMode(v) }
|
||||||
|
|
||||||
|
// GetAdvancedScraperDeterrence safely fetches the Configuration value for state's 'AdvancedScraperDeterrence' field
|
||||||
|
func (st *ConfigState) GetAdvancedScraperDeterrence() (v bool) {
|
||||||
|
st.mutex.RLock()
|
||||||
|
v = st.config.AdvancedScraperDeterrence
|
||||||
|
st.mutex.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAdvancedScraperDeterrence safely sets the Configuration value for state's 'AdvancedScraperDeterrence' field
|
||||||
|
func (st *ConfigState) SetAdvancedScraperDeterrence(v bool) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
st.config.AdvancedScraperDeterrence = v
|
||||||
|
st.reloadToViper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdvancedScraperDeterrenceFlag returns the flag name for the 'AdvancedScraperDeterrence' field
|
||||||
|
func AdvancedScraperDeterrenceFlag() string { return "advanced-scraper-deterrence" }
|
||||||
|
|
||||||
|
// GetAdvancedScraperDeterrence safely fetches the value for global configuration 'AdvancedScraperDeterrence' field
|
||||||
|
func GetAdvancedScraperDeterrence() bool { return global.GetAdvancedScraperDeterrence() }
|
||||||
|
|
||||||
|
// SetAdvancedScraperDeterrence safely sets the value for global configuration 'AdvancedScraperDeterrence' field
|
||||||
|
func SetAdvancedScraperDeterrence(v bool) { global.SetAdvancedScraperDeterrence(v) }
|
||||||
|
|
||||||
// GetHTTPClientAllowIPs safely fetches the Configuration value for state's 'HTTPClient.AllowIPs' field
|
// GetHTTPClientAllowIPs safely fetches the Configuration value for state's 'HTTPClient.AllowIPs' field
|
||||||
func (st *ConfigState) GetHTTPClientAllowIPs() (v []string) {
|
func (st *ConfigState) GetHTTPClientAllowIPs() (v []string) {
|
||||||
st.mutex.RLock()
|
st.mutex.RLock()
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,11 @@ package middleware
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"crypto/x509"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"hash"
|
"hash"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -34,29 +32,31 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NoLLaMas(
|
func NoLLaMas(getInstanceV1 func(context.Context) (*apimodel.InstanceV1, gtserror.WithCode)) gin.HandlerFunc {
|
||||||
getInstance func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode),
|
if !config.GetAdvancedScraperDeterrence() {
|
||||||
) gin.HandlerFunc {
|
// NoLLaMas middleware disabled.
|
||||||
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
return func(*gin.Context) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
seed := make([]byte, 32)
|
||||||
|
|
||||||
|
// Read random data for the token seed.
|
||||||
|
_, err := io.ReadFull(rand.Reader, seed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate seed hash
|
|
||||||
// from this private key.
|
|
||||||
bpriv := x509.MarshalPKCS1PrivateKey(privKey)
|
|
||||||
seed := sha512.Sum512(bpriv)
|
|
||||||
|
|
||||||
// Configure nollamas.
|
// Configure nollamas.
|
||||||
var nollamas nollamas
|
var nollamas nollamas
|
||||||
nollamas.seed = seed[:]
|
nollamas.seed = seed
|
||||||
nollamas.ttl = time.Hour
|
nollamas.ttl = time.Hour
|
||||||
nollamas.diff = 4
|
nollamas.diff = 4
|
||||||
nollamas.getInstance = getInstance
|
nollamas.getInstanceV1 = getInstanceV1
|
||||||
return nollamas.Serve
|
return nollamas.Serve
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,13 +66,21 @@ const hashLen = sha256.BlockSize
|
||||||
// i.e. hex.EncodedLen(hashLen).
|
// i.e. hex.EncodedLen(hashLen).
|
||||||
const encodedHashLen = 2 * hashLen
|
const encodedHashLen = 2 * hashLen
|
||||||
|
|
||||||
func newHash() hash.Hash { return sha256.New() }
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
type nollamas struct {
|
type nollamas struct {
|
||||||
seed []byte // securely hashed private key
|
seed []byte // unique token seed
|
||||||
ttl time.Duration
|
ttl time.Duration
|
||||||
diff uint8
|
diff uint8
|
||||||
getInstance func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode)
|
|
||||||
|
getInstanceV1 func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *nollamas) Serve(c *gin.Context) {
|
func (m *nollamas) Serve(c *gin.Context) {
|
||||||
|
|
@ -90,16 +98,17 @@ func (m *nollamas) Serve(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get new hasher.
|
// Prepare hash + buffers.
|
||||||
hash := newHash()
|
hash := hashWithBufs{
|
||||||
|
hash: sha256.New(),
|
||||||
// Reset hash.
|
hbuf: make([]byte, 0, hashLen),
|
||||||
hash.Reset()
|
ebuf: make([]byte, encodedHashLen),
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a unique token for
|
// Generate a unique token for
|
||||||
// this request only valid for
|
// this request only valid for
|
||||||
// a period of now +- m.ttl.
|
// a period of now +- m.ttl.
|
||||||
token := m.token(c, hash)
|
token := m.token(c, &hash)
|
||||||
|
|
||||||
// For unique challenge string just use a
|
// For unique challenge string just use a
|
||||||
// portion of their unique 'success' token.
|
// portion of their unique 'success' token.
|
||||||
|
|
@ -144,14 +153,16 @@ func (m *nollamas) Serve(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset hash.
|
// Reset the hash.
|
||||||
hash.Reset()
|
hash.hash.Reset()
|
||||||
|
|
||||||
// Hash and encode input challenge with
|
// Hash and encode input challenge with
|
||||||
// proposed nonce as a possible solution.
|
// proposed nonce as a possible solution.
|
||||||
_, _ = hash.Write(byteutil.S2B(challenge))
|
hash.hash.Write(byteutil.S2B(challenge))
|
||||||
_, _ = hash.Write(byteutil.S2B(nonce))
|
hash.hash.Write(byteutil.S2B(nonce))
|
||||||
solution := hex.AppendEncode(nil, hash.Sum(nil))
|
hash.hbuf = hash.hash.Sum(hash.hbuf[:0])
|
||||||
|
hex.Encode(hash.ebuf, hash.hbuf)
|
||||||
|
solution := hash.ebuf
|
||||||
|
|
||||||
// Check that the first 'diff'
|
// Check that the first 'diff'
|
||||||
// many chars are indeed zeroes.
|
// many chars are indeed zeroes.
|
||||||
|
|
@ -182,9 +193,10 @@ func (m *nollamas) renderChallenge(c *gin.Context, challenge string) {
|
||||||
// our challenge page.
|
// our challenge page.
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
|
||||||
instance, errWithCode := m.getInstance(c.Request.Context())
|
// Fetch current instance information for templating vars.
|
||||||
|
instance, errWithCode := m.getInstanceV1(c.Request.Context())
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.getInstance)
|
apiutil.ErrorHandler(c, errWithCode, m.getInstanceV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,24 +217,24 @@ func (m *nollamas) renderChallenge(c *gin.Context, challenge string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *nollamas) token(c *gin.Context, hash hash.Hash) string {
|
func (m *nollamas) token(c *gin.Context, hash *hashWithBufs) string {
|
||||||
// Use our safe, unique input seed which
|
// Use our safe, unique input seed which
|
||||||
// is already hashed, but will get rehashed.
|
// is already hashed, but will get rehashed.
|
||||||
// This ensures we don't leak private keys,
|
// This ensures we don't leak private keys,
|
||||||
// but also we have cryptographically safe
|
// but also we have cryptographically safe
|
||||||
// deterministic tokens for comparisons.
|
// deterministic tokens for comparisons.
|
||||||
_, _ = hash.Write(m.seed)
|
hash.hash.Write(m.seed)
|
||||||
|
|
||||||
// Include difficulty level in
|
// Include difficulty level in
|
||||||
// hash input data so if config
|
// hash input data so if config
|
||||||
// changes then token invalidates.
|
// changes then token invalidates.
|
||||||
_, _ = hash.Write([]byte{m.diff})
|
hash.hash.Write([]byte{m.diff})
|
||||||
|
|
||||||
// Also seed the generated input with
|
// Also seed the generated input with
|
||||||
// current time rounded to TTL, so our
|
// current time rounded to TTL, so our
|
||||||
// single comparison handles expiries.
|
// single comparison handles expiries.
|
||||||
now := time.Now().Round(m.ttl).Unix()
|
now := time.Now().Round(m.ttl).Unix()
|
||||||
_, _ = hash.Write([]byte{
|
hash.hash.Write([]byte{
|
||||||
byte(now >> 56),
|
byte(now >> 56),
|
||||||
byte(now >> 48),
|
byte(now >> 48),
|
||||||
byte(now >> 40),
|
byte(now >> 40),
|
||||||
|
|
@ -235,10 +247,12 @@ func (m *nollamas) token(c *gin.Context, hash hash.Hash) string {
|
||||||
|
|
||||||
// Finally, append unique client request data.
|
// Finally, append unique client request data.
|
||||||
userAgent := c.Request.Header.Get("User-Agent")
|
userAgent := c.Request.Header.Get("User-Agent")
|
||||||
_, _ = hash.Write(byteutil.S2B(userAgent))
|
hash.hash.Write(byteutil.S2B(userAgent))
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
_, _ = hash.Write(byteutil.S2B(clientIP))
|
hash.hash.Write(byteutil.S2B(clientIP))
|
||||||
|
|
||||||
// Return hex encoded hash output.
|
// Return hex encoded hash output.
|
||||||
return hex.EncodeToString(hash.Sum(nil))
|
hash.hbuf = hash.hash.Sum(hash.hbuf[:0])
|
||||||
|
hex.Encode(hash.ebuf, hash.hbuf)
|
||||||
|
return string(hash.ebuf)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue