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 |
|
| --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 |
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
1.3.9
|
1.3.10
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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", ","),
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue