diff --git a/README.md b/README.md index fdf58bd..d70c1c5 100644 --- a/README.md +++ b/README.md @@ -192,18 +192,19 @@ 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 | -| --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 | -| --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 | +| 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 | +| --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 diff --git a/VERSION b/VERSION index bd8bf88..943f9cb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.7.0 +1.7.1 diff --git a/libretranslate/app.py b/libretranslate/app.py index fd3ec8c..3dae777 100644 --- a/libretranslate/app.py +++ b/libretranslate/app.py @@ -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,12 +351,17 @@ 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(), 'alternatives': [], '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") diff --git a/libretranslate/default_values.py b/libretranslate/default_values.py index 2c79611..4da8e29 100644 --- a/libretranslate/default_values.py +++ b/libretranslate/default_values.py @@ -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://', diff --git a/libretranslate/flood.py b/libretranslate/flood.py index 63cd392..3f87c9b 100644 --- a/libretranslate/flood.py +++ b/libretranslate/flood.py @@ -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 \ No newline at end of file diff --git a/libretranslate/main.py b/libretranslate/main.py index b6b6a03..61d7585 100644 --- a/libretranslate/main.py +++ b/libretranslate/main.py @@ -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, diff --git a/libretranslate/secret.py b/libretranslate/secret.py index c8f278a..fdf818d 100644 --- a/libretranslate/secret.py +++ b/libretranslate/secret.py @@ -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"): diff --git a/libretranslate/storage.py b/libretranslate/storage.py index 46bd9c5..696f097 100644 --- a/libretranslate/storage.py +++ b/libretranslate/storage.py @@ -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)