Add require-api-key-secret

This commit is contained in:
Piero Toffanin 2023-03-09 13:59:25 -05:00
parent b3e9105d21
commit f2792e5001
7 changed files with 88 additions and 13 deletions

View file

@ -191,6 +191,7 @@ docker-compose -f docker-compose.cuda.yml up -d --build
| --api-keys-remote | Use this remote endpoint to query for valid API keys instead of using the local database | `Use local API key database` | LT_API_KEYS_REMOTE | | --api-keys-remote | Use this remote endpoint to query for valid API keys instead of using the local database | `Use local API key database` | LT_API_KEYS_REMOTE |
| --get-api-key-link | Show a link in the UI where to direct users to get an API key | `Don't show a link` | LT_GET_API_KEY_LINK | | --get-api-key-link | Show a link in the UI where to direct users to get an API key | `Don't show a link` | LT_GET_API_KEY_LINK |
| --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 |
| --load-only | Set available languages | `all from argostranslate` | LT_LOAD_ONLY | | --load-only | Set available languages | `all from argostranslate` | LT_LOAD_ONLY |
| --threads | Set number of threads | `4` | LT_THREADS | | --threads | Set number of threads | `4` | LT_THREADS |
| --suggestions | Allow user suggestions | `False` | LT_SUGGESTIONS | | --suggestions | Allow user suggestions | `False` | LT_SUGGESTIONS |

View file

@ -1 +1 @@
1.3.9 1.3.10

View file

@ -6,6 +6,7 @@ import uuid
from functools import wraps from functools import wraps
from html import unescape from html import unescape
from timeit import default_timer from timeit import default_timer
from datetime import datetime
import argostranslatefiles import argostranslatefiles
from argostranslatefiles import get_supported_formats from argostranslatefiles import get_supported_formats
@ -54,6 +55,15 @@ def get_req_api_key():
return ak return ak
def get_req_secret():
if request.is_json:
json = get_json_dict(request)
ak = json.get("secret")
else:
ak = request.values.get("secret")
return ak
def get_json_dict(request): def get_json_dict(request):
d = request.get_json() d = request.get_json()
@ -233,18 +243,28 @@ def create_app(args):
if args.api_keys: if args.api_keys:
ak = get_req_api_key() ak = get_req_api_key()
if ( if ak and api_keys_db.lookup(ak) is None:
ak and api_keys_db.lookup(ak) is None
):
abort( abort(
403, 403,
description=_("Invalid API key"), description=_("Invalid API key"),
) )
elif ( else:
args.require_api_key_origin need_key = False
and api_keys_db.lookup(ak) is None key_missing = api_keys_db.lookup(ak) is None
and not re.match(args.require_api_key_origin, request.headers.get("Origin", ""))
): if (args.require_api_key_origin
and key_missing
and not re.match(args.require_api_key_origin, request.headers.get("Origin", ""))
):
need_key = True
if (args.require_api_key_secret
and key_missing
and not flood.secret_match(get_req_secret())
):
need_key = True
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:
description = _("Visit %(url)s to get an API key", url=args.get_api_key_link) description = _("Visit %(url)s to get an API key", url=args.get_api_key_link)
@ -323,9 +343,18 @@ def create_app(args):
if args.disable_web_ui: if args.disable_web_ui:
abort(404) abort(404)
return Response(render_template("app.js.template", response = Response(render_template("app.js.template",
url_prefix=args.url_prefix, url_prefix=args.url_prefix,
get_api_key_link=args.get_api_key_link), content_type='application/javascript; charset=utf-8') get_api_key_link=args.get_api_key_link,
api_secret=flood.get_current_secret() if args.require_api_key_secret else ""), content_type='application/javascript; charset=utf-8')
if args.require_api_key_secret:
response.headers['Last-Modified'] = datetime.now()
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
return response
@bp.get("/languages") @bp.get("/languages")
@limiter.exempt @limiter.exempt

View file

@ -131,6 +131,11 @@ _default_options_objects = [
'default_value': '', 'default_value': '',
'value_type': 'str' 'value_type': 'str'
}, },
{
'name': 'REQUIRE_API_KEY_SECRET',
'default_value': False,
'value_type': 'bool'
},
{ {
'name': 'LOAD_ONLY', 'name': 'LOAD_ONLY',
'default_value': None, 'default_value': None,

View file

@ -1,11 +1,16 @@
import atexit import atexit
import random
import string
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
def generate_secret():
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=7))
banned = {} banned = {}
active = False active = False
threshold = -1 threshold = -1
secrets = [generate_secret(), generate_secret()]
def forgive_banned(): def forgive_banned():
global banned global banned
@ -22,6 +27,16 @@ def forgive_banned():
for ip in clear_list: for ip in clear_list:
del banned[ip] del banned[ip]
def rotate_secrets():
global secrets
secrets[0] = secrets[1]
secrets[1] = generate_secret()
def secret_match(s):
return s in secrets
def get_current_secret():
return secrets[1]
def setup(violations_threshold=100): def setup(violations_threshold=100):
global active global active
@ -32,6 +47,8 @@ def setup(violations_threshold=100):
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
scheduler.add_job(func=forgive_banned, trigger="interval", minutes=30) scheduler.add_job(func=forgive_banned, trigger="interval", minutes=30)
scheduler.add_job(func=rotate_secrets, trigger="interval", minutes=30)
scheduler.start() scheduler.start()
# Shut down the scheduler when exiting the app # Shut down the scheduler when exiting the app

View file

@ -120,6 +120,12 @@ def get_args():
default=DEFARGS['REQUIRE_API_KEY_ORIGIN'], default=DEFARGS['REQUIRE_API_KEY_ORIGIN'],
help="Require use of an API key for programmatic access to the API, unless the request origin matches this domain", help="Require use of an API key for programmatic access to the API, unless the request origin matches this domain",
) )
parser.add_argument(
"--require-api-key-secret",
default=DEFARGS['REQUIRE_API_KEY_SECRET'],
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( parser.add_argument(
"--load-only", "--load-only",
type=operator.methodcaller("split", ","), type=operator.methodcaller("split", ","),

View file

@ -39,7 +39,9 @@ document.addEventListener('DOMContentLoaded', function(){
loadingFileTranslation: false, loadingFileTranslation: false,
translatedFileUrl: false, translatedFileUrl: false,
filesTranslation: true, filesTranslation: true,
frontendTimeout: 500 frontendTimeout: 500,
apiSecret: "{{ api_secret }}"
}, },
mounted: function() { mounted: function() {
const self = this; const self = this;
@ -234,11 +236,19 @@ document.addEventListener('DOMContentLoaded', function(){
data.append("target", self.targetLang); data.append("target", self.targetLang);
data.append("format", self.isHtml ? "html" : "text"); data.append("format", self.isHtml ? "html" : "text");
data.append("api_key", localStorage.getItem("api_key") || ""); data.append("api_key", localStorage.getItem("api_key") || "");
if (self.apiSecret) data.append("secret", self.apiSecret);
request.open('POST', BaseUrl + '/translate', true); request.open('POST', BaseUrl + '/translate', true);
request.onload = function() { request.onload = function() {
try{ try{
{% if api_secret != "" %}
if (this.status === 403){
window.location.reload(true);
return;
}
{% endif %}
var res = JSON.parse(this.response); var res = JSON.parse(this.response);
// Success! // Success!
if (res.translatedText !== undefined){ if (res.translatedText !== undefined){
@ -365,12 +375,19 @@ document.addEventListener('DOMContentLoaded', function(){
data.append("source", this.sourceLang); data.append("source", this.sourceLang);
data.append("target", this.targetLang); data.append("target", this.targetLang);
data.append("api_key", localStorage.getItem("api_key") || ""); data.append("api_key", localStorage.getItem("api_key") || "");
if (self.apiSecret) data.append("secret", self.apiSecret);
this.loadingFileTranslation = true this.loadingFileTranslation = true
translateFileRequest.onload = function() { translateFileRequest.onload = function() {
if (translateFileRequest.readyState === 4 && translateFileRequest.status === 200) { if (translateFileRequest.readyState === 4 && translateFileRequest.status === 200) {
try{ try{
{% if api_secret != "" %}
if (this.status === 403){
window.location.reload(true);
return;
}
{% endif %}
self.loadingFileTranslation = false; self.loadingFileTranslation = false;
let res = JSON.parse(this.response); let res = JSON.parse(this.response);