mirror of
https://github.com/LibreTranslate/LibreTranslate.git
synced 2024-11-27 18:31:00 +00:00
Add require-api-key-secret
This commit is contained in:
parent
b3e9105d21
commit
f2792e5001
7 changed files with 88 additions and 13 deletions
|
@ -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 |
|
||||
| --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-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 |
|
||||
| --threads | Set number of threads | `4` | LT_THREADS |
|
||||
| --suggestions | Allow user suggestions | `False` | LT_SUGGESTIONS |
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.3.9
|
||||
1.3.10
|
||||
|
|
|
@ -6,6 +6,7 @@ import uuid
|
|||
from functools import wraps
|
||||
from html import unescape
|
||||
from timeit import default_timer
|
||||
from datetime import datetime
|
||||
|
||||
import argostranslatefiles
|
||||
from argostranslatefiles import get_supported_formats
|
||||
|
@ -54,6 +55,15 @@ def get_req_api_key():
|
|||
|
||||
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):
|
||||
d = request.get_json()
|
||||
|
@ -233,18 +243,28 @@ def create_app(args):
|
|||
|
||||
if args.api_keys:
|
||||
ak = get_req_api_key()
|
||||
if (
|
||||
ak and api_keys_db.lookup(ak) is None
|
||||
):
|
||||
if ak and api_keys_db.lookup(ak) is None:
|
||||
abort(
|
||||
403,
|
||||
description=_("Invalid API key"),
|
||||
)
|
||||
elif (
|
||||
args.require_api_key_origin
|
||||
and api_keys_db.lookup(ak) is None
|
||||
and not re.match(args.require_api_key_origin, request.headers.get("Origin", ""))
|
||||
):
|
||||
else:
|
||||
need_key = False
|
||||
key_missing = api_keys_db.lookup(ak) is None
|
||||
|
||||
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")
|
||||
if 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:
|
||||
abort(404)
|
||||
|
||||
return Response(render_template("app.js.template",
|
||||
response = Response(render_template("app.js.template",
|
||||
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")
|
||||
@limiter.exempt
|
||||
|
|
|
@ -131,6 +131,11 @@ _default_options_objects = [
|
|||
'default_value': '',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'REQUIRE_API_KEY_SECRET',
|
||||
'default_value': False,
|
||||
'value_type': 'bool'
|
||||
},
|
||||
{
|
||||
'name': 'LOAD_ONLY',
|
||||
'default_value': None,
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import atexit
|
||||
import random
|
||||
import string
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
def generate_secret():
|
||||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=7))
|
||||
|
||||
banned = {}
|
||||
active = False
|
||||
threshold = -1
|
||||
|
||||
secrets = [generate_secret(), generate_secret()]
|
||||
|
||||
def forgive_banned():
|
||||
global banned
|
||||
|
@ -22,6 +27,16 @@ def forgive_banned():
|
|||
for ip in clear_list:
|
||||
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):
|
||||
global active
|
||||
|
@ -32,6 +47,8 @@ def setup(violations_threshold=100):
|
|||
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(func=forgive_banned, trigger="interval", minutes=30)
|
||||
scheduler.add_job(func=rotate_secrets, trigger="interval", minutes=30)
|
||||
|
||||
scheduler.start()
|
||||
|
||||
# Shut down the scheduler when exiting the app
|
||||
|
|
|
@ -120,6 +120,12 @@ def get_args():
|
|||
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",
|
||||
)
|
||||
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(
|
||||
"--load-only",
|
||||
type=operator.methodcaller("split", ","),
|
||||
|
|
|
@ -39,7 +39,9 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
loadingFileTranslation: false,
|
||||
translatedFileUrl: false,
|
||||
filesTranslation: true,
|
||||
frontendTimeout: 500
|
||||
frontendTimeout: 500,
|
||||
|
||||
apiSecret: "{{ api_secret }}"
|
||||
},
|
||||
mounted: function() {
|
||||
const self = this;
|
||||
|
@ -234,11 +236,19 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
data.append("target", self.targetLang);
|
||||
data.append("format", self.isHtml ? "html" : "text");
|
||||
data.append("api_key", localStorage.getItem("api_key") || "");
|
||||
if (self.apiSecret) data.append("secret", self.apiSecret);
|
||||
|
||||
request.open('POST', BaseUrl + '/translate', true);
|
||||
|
||||
request.onload = function() {
|
||||
try{
|
||||
{% if api_secret != "" %}
|
||||
if (this.status === 403){
|
||||
window.location.reload(true);
|
||||
return;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
var res = JSON.parse(this.response);
|
||||
// Success!
|
||||
if (res.translatedText !== undefined){
|
||||
|
@ -365,12 +375,19 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
data.append("source", this.sourceLang);
|
||||
data.append("target", this.targetLang);
|
||||
data.append("api_key", localStorage.getItem("api_key") || "");
|
||||
if (self.apiSecret) data.append("secret", self.apiSecret);
|
||||
|
||||
this.loadingFileTranslation = true
|
||||
|
||||
translateFileRequest.onload = function() {
|
||||
if (translateFileRequest.readyState === 4 && translateFileRequest.status === 200) {
|
||||
try{
|
||||
{% if api_secret != "" %}
|
||||
if (this.status === 403){
|
||||
window.location.reload(true);
|
||||
return;
|
||||
}
|
||||
{% endif %}
|
||||
self.loadingFileTranslation = false;
|
||||
|
||||
let res = JSON.parse(this.response);
|
||||
|
|
Loading…
Reference in a new issue