mirror of
https://github.com/LibreTranslate/LibreTranslate.git
synced 2025-05-15 22:42:54 +00:00
Add fingerprinting mechanism
This commit is contained in:
parent
da0890d60f
commit
f2268fe4d9
8 changed files with 65 additions and 20 deletions
25
README.md
25
README.md
|
@ -192,18 +192,19 @@ Arguments passed to the process or set via environment variables are split into
|
||||||
|
|
||||||
### Settings / Flags
|
### Settings / Flags
|
||||||
|
|
||||||
| Argument | Description | Default Setting | Env. name |
|
| Argument | Description | Default Setting | Env. name |
|
||||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- | ---------------------------- |
|
| ----------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------ |
|
||||||
| --debug | Enable debug environment | `Disabled` | LT_DEBUG |
|
| --debug | Enable debug environment | `Disabled` | LT_DEBUG |
|
||||||
| --ssl | Whether to enable SSL | `Disabled` | LT_SSL |
|
| --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 |
|
| --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-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-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 |
|
||||||
| --suggestions | Allow user suggestions | `Disabled` | LT_SUGGESTIONS |
|
| --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 |
|
||||||
| --disable-files-translation | Disable files translation | `File translation allowed` | LT_DISABLE_FILES_TRANSLATION |
|
| --suggestions | Allow user suggestions | `Disabled` | LT_SUGGESTIONS |
|
||||||
| --disable-web-ui | Disable web ui | `Web Ui enabled` | LT_DISABLE_WEB_UI |
|
| --disable-files-translation | Disable files translation | `File translation allowed` | LT_DISABLE_FILES_TRANSLATION |
|
||||||
| --update-models | Update language models at startup | `Only on if no models found` | LT_UPDATE_MODELS |
|
| --disable-web-ui | Disable web ui | `Web Ui enabled` | LT_DISABLE_WEB_UI |
|
||||||
| --metrics | Enable the /metrics endpoint for exporting [Prometheus](https://prometheus.io/) usage metrics | `Disabled` | LT_METRICS |
|
| --update-models | Update language models at startup | `Only on if no models found` | LT_UPDATE_MODELS |
|
||||||
|
| --metrics | Enable the /metrics endpoint for exporting [Prometheus](https://prometheus.io/) usage metrics | `Disabled` | LT_METRICS |
|
||||||
|
|
||||||
### Configuration Parameters
|
### Configuration Parameters
|
||||||
|
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
1.7.0
|
1.7.1
|
||||||
|
|
|
@ -102,6 +102,9 @@ def get_remote_address():
|
||||||
|
|
||||||
return ip
|
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):
|
def get_req_limits(default_limit, api_keys_db, db_multiplier=1, multiplier=1):
|
||||||
req_limit = default_limit
|
req_limit = default_limit
|
||||||
|
@ -348,6 +351,7 @@ def create_app(args):
|
||||||
and not secret.secret_match(req_secret)
|
and not secret.secret_match(req_secret)
|
||||||
):
|
):
|
||||||
need_key = True
|
need_key = True
|
||||||
|
|
||||||
if secret.secret_bogus_match(req_secret):
|
if secret.secret_bogus_match(req_secret):
|
||||||
abort(make_response(jsonify({
|
abort(make_response(jsonify({
|
||||||
'translatedText': secret.get_emoji(),
|
'translatedText': secret.get_emoji(),
|
||||||
|
@ -355,6 +359,10 @@ def create_app(args):
|
||||||
'detectedLanguage': { 'confidence': 100, 'language': 'en' }
|
'detectedLanguage': { 'confidence': 100, 'language': 'en' }
|
||||||
}), 200))
|
}), 200))
|
||||||
|
|
||||||
|
if args.require_api_key_fingerprint:
|
||||||
|
if flood.fingerprint_mismatch(ip, get_fingerprint()):
|
||||||
|
need_key = True
|
||||||
|
|
||||||
if need_key:
|
if need_key:
|
||||||
description = _("Please contact the server operator to get an API key")
|
description = _("Please contact the server operator to get an API key")
|
||||||
if args.get_api_key_link:
|
if args.get_api_key_link:
|
||||||
|
|
|
@ -151,6 +151,11 @@ _default_options_objects = [
|
||||||
'default_value': False,
|
'default_value': False,
|
||||||
'value_type': 'bool'
|
'value_type': 'bool'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'REQUIRE_API_KEY_FINGERPRINT',
|
||||||
|
'default_value': False,
|
||||||
|
'value_type': 'bool'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'SHARED_STORAGE',
|
'name': 'SHARED_STORAGE',
|
||||||
'default_value': 'memory://',
|
'default_value': 'memory://',
|
||||||
|
|
|
@ -45,3 +45,16 @@ def is_banned(request_ip):
|
||||||
|
|
||||||
# More than X offences?
|
# More than X offences?
|
||||||
return active and s.get_hash_int("banned", request_ip) >= threshold
|
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
|
|
@ -147,6 +147,12 @@ def get_args():
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Require use of an API key for programmatic access to the API, unless the client also sends a secret match",
|
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(
|
parser.add_argument(
|
||||||
"--shared-storage",
|
"--shared-storage",
|
||||||
type=str,
|
type=str,
|
||||||
|
|
|
@ -108,7 +108,7 @@ def get_emoji():
|
||||||
return random.choice(["😂", "🤪", "😜", "🤣", "😹", "🐒", "🙈", "🤡", "🥸", "😆", "🥴", "🐸", "🐤", "🐒🙊", "👀", "💩", "🤯", "😛", "🤥", "👻"])
|
return random.choice(["😂", "🤪", "😜", "🤣", "😹", "🐒", "🙈", "🤡", "🥸", "😆", "🥴", "🐸", "🐤", "🐒🙊", "👀", "💩", "🤯", "😛", "🤥", "👻"])
|
||||||
|
|
||||||
def setup(args):
|
def setup(args):
|
||||||
if args.api_keys and args.require_api_key_secret:
|
if args.require_api_key_secret:
|
||||||
s = get_storage()
|
s = get_storage()
|
||||||
|
|
||||||
if not s.exists("secret_0"):
|
if not s.exists("secret_0"):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import redis
|
import redis
|
||||||
|
import time
|
||||||
|
|
||||||
storage = None
|
storage = None
|
||||||
def get_storage():
|
def get_storage():
|
||||||
|
@ -18,7 +19,7 @@ class Storage:
|
||||||
def get_int(self, key):
|
def get_int(self, key):
|
||||||
raise Exception("not implemented")
|
raise Exception("not implemented")
|
||||||
|
|
||||||
def set_str(self, key, value):
|
def set_str(self, key, value, ex=None):
|
||||||
raise Exception("not implemented")
|
raise Exception("not implemented")
|
||||||
def get_str(self, key):
|
def get_str(self, key):
|
||||||
raise Exception("not implemented")
|
raise Exception("not implemented")
|
||||||
|
@ -56,11 +57,22 @@ class MemoryStorage(Storage):
|
||||||
def get_int(self, key):
|
def get_int(self, key):
|
||||||
return int(self.store.get(key, 0))
|
return int(self.store.get(key, 0))
|
||||||
|
|
||||||
def set_str(self, key, value):
|
def set_str(self, key, value, ex=None):
|
||||||
self.store[key] = value
|
self.store[key] = {
|
||||||
|
'value': value,
|
||||||
|
'ex': time.time() + ex
|
||||||
|
}
|
||||||
|
|
||||||
def get_str(self, key):
|
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):
|
def set_hash_int(self, ns, key, value):
|
||||||
if ns not in self.store:
|
if ns not in self.store:
|
||||||
|
@ -123,8 +135,8 @@ class RedisStorage(Storage):
|
||||||
else:
|
else:
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def set_str(self, key, value):
|
def set_str(self, key, value, ex=None):
|
||||||
self.conn.set(key, value)
|
self.conn.set(key, value, ex=ex)
|
||||||
|
|
||||||
def get_str(self, key):
|
def get_str(self, key):
|
||||||
v = self.conn.get(key)
|
v = self.conn.get(key)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue