Add fingerprinting mechanism

This commit is contained in:
Piero Toffanin 2025-04-18 12:21:16 -04:00
parent da0890d60f
commit f2268fe4d9
8 changed files with 65 additions and 20 deletions

View file

@ -193,12 +193,13 @@ Arguments passed to the process or set via environment variables are split into
### Settings / Flags
| Argument | Description | Default Setting | Env. name |
| --------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- | ---------------------------- |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------ |
| --debug | Enable debug environment | `Disabled` | LT_DEBUG |
| --ssl | Whether to enable SSL | `Disabled` | LT_SSL |
| --api-keys | Enable API keys database for per-client rate limits when --req-limit is reached | `Don't use API keys` | LT_API_KEYS |
| --require-api-key-origin | Require use of an API key for programmatic access to the API, unless the request origin matches this domain | `No restrictions on domain origin` | LT_REQUIRE_API_KEY_ORIGIN |
| --require-api-key-secret | Require use of an API key for programmatic access to the API, unless the client also sends a secret match | `No secrets required` | LT_REQUIRE_API_KEY_SECRET |
| --require-api-key-fingerprint | Require use of an API key for programmatic access to the API, unless the client also matches a fingerprint | `No fingerprinting required` | LT_REQUIRE_API_KEY_FINGERPRINT |
| --suggestions | Allow user suggestions | `Disabled` | LT_SUGGESTIONS |
| --disable-files-translation | Disable files translation | `File translation allowed` | LT_DISABLE_FILES_TRANSLATION |
| --disable-web-ui | Disable web ui | `Web Ui enabled` | LT_DISABLE_WEB_UI |

View file

@ -1 +1 @@
1.7.0
1.7.1

View file

@ -102,6 +102,9 @@ def get_remote_address():
return ip
def get_fingerprint():
return request.headers.get("User-Agent", "") + "|" + request.headers.get("Cookie", "")
def get_req_limits(default_limit, api_keys_db, db_multiplier=1, multiplier=1):
req_limit = default_limit
@ -348,6 +351,7 @@ def create_app(args):
and not secret.secret_match(req_secret)
):
need_key = True
if secret.secret_bogus_match(req_secret):
abort(make_response(jsonify({
'translatedText': secret.get_emoji(),
@ -355,6 +359,10 @@ def create_app(args):
'detectedLanguage': { 'confidence': 100, 'language': 'en' }
}), 200))
if args.require_api_key_fingerprint:
if flood.fingerprint_mismatch(ip, get_fingerprint()):
need_key = True
if need_key:
description = _("Please contact the server operator to get an API key")
if args.get_api_key_link:

View file

@ -151,6 +151,11 @@ _default_options_objects = [
'default_value': False,
'value_type': 'bool'
},
{
'name': 'REQUIRE_API_KEY_FINGERPRINT',
'default_value': False,
'value_type': 'bool'
},
{
'name': 'SHARED_STORAGE',
'default_value': 'memory://',

View file

@ -45,3 +45,16 @@ def is_banned(request_ip):
# More than X offences?
return active and s.get_hash_int("banned", request_ip) >= threshold
def fingerprint_mismatch(request_ip, fingerprint):
if not isinstance(fingerprint, str) or fingerprint == "":
return True
s = get_storage()
k = f"fingerprint:{request_ip}"
expected = s.get_str(k)
if expected == "":
s.set_str(k, fingerprint, ex=300)
return False
else:
return fingerprint != expected

View file

@ -147,6 +147,12 @@ def get_args():
action="store_true",
help="Require use of an API key for programmatic access to the API, unless the client also sends a secret match",
)
parser.add_argument(
"--require-api-key-fingerprint",
default=DEFARGS['REQUIRE_API_KEY_FINGERPRINT'],
action="store_true",
help="Require use of an API key for programmatic access to the API, unless the client also matches a fingerprint",
)
parser.add_argument(
"--shared-storage",
type=str,

View file

@ -108,7 +108,7 @@ def get_emoji():
return random.choice(["😂", "🤪", "😜", "🤣", "😹", "🐒", "🙈", "🤡", "🥸", "😆", "🥴", "🐸", "🐤", "🐒🙊", "👀", "💩", "🤯", "😛", "🤥", "👻"])
def setup(args):
if args.api_keys and args.require_api_key_secret:
if args.require_api_key_secret:
s = get_storage()
if not s.exists("secret_0"):

View file

@ -1,4 +1,5 @@
import redis
import time
storage = None
def get_storage():
@ -18,7 +19,7 @@ class Storage:
def get_int(self, key):
raise Exception("not implemented")
def set_str(self, key, value):
def set_str(self, key, value, ex=None):
raise Exception("not implemented")
def get_str(self, key):
raise Exception("not implemented")
@ -56,11 +57,22 @@ class MemoryStorage(Storage):
def get_int(self, key):
return int(self.store.get(key, 0))
def set_str(self, key, value):
self.store[key] = value
def set_str(self, key, value, ex=None):
self.store[key] = {
'value': value,
'ex': time.time() + ex
}
def get_str(self, key):
return str(self.store.get(key, ""))
d = self.store.get(key, {'value': '', 'ex': None})
if d['ex'] is None:
return d['value']
else:
if d['ex'] <= time.time():
del self.store[key]
return ''
else:
return d['value']
def set_hash_int(self, ns, key, value):
if ns not in self.store:
@ -123,8 +135,8 @@ class RedisStorage(Storage):
else:
return v
def set_str(self, key, value):
self.conn.set(key, value)
def set_str(self, key, value, ex=None):
self.conn.set(key, value, ex=ex)
def get_str(self, key):
v = self.conn.get(key)