mirror of
https://github.com/norohind/FDEV-CAPI-Handler.git
synced 2025-04-22 01:30:28 +03:00
init
This commit is contained in:
commit
318bbb8626
36
config.py
Normal file
36
config.py
Normal file
@ -0,0 +1,36 @@
|
||||
import requests
|
||||
from os import getenv
|
||||
|
||||
CLIENT_ID = getenv('client_id')
|
||||
assert CLIENT_ID, "No client_id in env"
|
||||
|
||||
REDIRECT_URL = requests.utils.quote("http://127.0.0.1:9000/fdev-redirect")
|
||||
AUTH_URL = 'https://auth.frontierstore.net/auth'
|
||||
TOKEN_URL = 'https://auth.frontierstore.net/token'
|
||||
PROPER_USER_AGENT = 'EDCD-a31-0.1'
|
||||
REDIRECT_HTML_TEMPLATE = """
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; url={link}">
|
||||
</head>
|
||||
<body>
|
||||
<a href="{link}">
|
||||
You should be redirected shortly...
|
||||
</a>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
ADMIN_USERS_TEMPLATE = """
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
ADMIN_USER_TEMPLATE = '<a href="{link}">{desc}</a>'
|
230
main.py
Normal file
230
main.py
Normal file
@ -0,0 +1,230 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
import falcon
|
||||
import requests
|
||||
import waitress
|
||||
|
||||
import config
|
||||
from config import REDIRECT_URL, AUTH_URL, TOKEN_URL, PROPER_USER_AGENT, CLIENT_ID, REDIRECT_HTML_TEMPLATE
|
||||
from refresher import refresh_one_token
|
||||
|
||||
|
||||
# import logging
|
||||
# from sys import stdout
|
||||
|
||||
|
||||
def generate_challenge(_verifier):
|
||||
""" It takes code verifier and return code challenge"""
|
||||
return base64.urlsafe_b64encode(hashlib.sha256(_verifier).digest())[:-1].decode('utf-8')
|
||||
|
||||
|
||||
"""
|
||||
# setting up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
stdout_handler = logging.StreamHandler(stdout)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(levelname)s - %(process)d:%(thread)d: %(module)s:%(lineno)d: %(message)s')
|
||||
stdout_handler.setFormatter(formatter)
|
||||
logger.addHandler(stdout_handler)
|
||||
"""
|
||||
|
||||
"""
|
||||
Typical workflow:
|
||||
1. User open Authorize endpoint
|
||||
2. Authorize building link for FDEV's /auth, sending link to user
|
||||
3. Authorize write code_verifier, state, timestamp_init to DB
|
||||
4. User approving client, redirecting to FDEV_redirect endpoint
|
||||
5. Searching in DB if we have record with this state
|
||||
if don't have:
|
||||
raise error for user, exit
|
||||
|
||||
6. Write received code to DB
|
||||
7. Build Token Request
|
||||
if not success:
|
||||
remove all related info from DB
|
||||
raise error for user, exit
|
||||
8. Store access_token, refresh_token, expires_in, timestamp_got_expires_in
|
||||
9. Raise "all right" for user
|
||||
|
||||
DB Tables:
|
||||
1. authorizations
|
||||
code_verifier text - store code_verifier, don't storing code_challenge because we can get it from verifier
|
||||
state text - state
|
||||
timestamp_init text - unix timestamp when authorization was started, can be used to count 25 days until next auth
|
||||
code text - code (from frontier redirect)
|
||||
access_token text - access token aka bearer
|
||||
refresh_token text - refresh token
|
||||
expires_in text - expires in for access token (in secs)
|
||||
timestamp_got_expires_in text - unix timestamp, when we got expires_in for access token
|
||||
nickname text - nickname of tokens owner
|
||||
"""
|
||||
|
||||
sql_conn = sqlite3.connect(database="companion-api.sqlite")
|
||||
with sql_conn:
|
||||
sql_conn.execute(
|
||||
"create table if not exists authorizations (code_verifier text , state text, "
|
||||
"timestamp_init text , code text, access_token text, refresh_token text, expires_in text, "
|
||||
"timestamp_got_expires_in text, nickname text unique);")
|
||||
sql_conn.close()
|
||||
|
||||
|
||||
class Authorize:
|
||||
def on_get(self, req, resp):
|
||||
sql_connection = sqlite3.connect(database="companion-api.sqlite")
|
||||
code_verifier = base64.urlsafe_b64encode(os.urandom(32))
|
||||
code_challenge = generate_challenge(code_verifier)
|
||||
state_string = base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8")
|
||||
|
||||
redirect_user_to = f"{AUTH_URL}?audience=all&scope=capi&response_type=code&client_id={CLIENT_ID}" + \
|
||||
f"&code_challenge={code_challenge}&code_challenge_method=S256&state={state_string}" + \
|
||||
f"&redirect_uri={REDIRECT_URL}"
|
||||
with sql_connection:
|
||||
sql_connection.execute(
|
||||
"insert into authorizations (code_verifier, state, timestamp_init) values "
|
||||
"(?, ?, ?);", [code_verifier.decode('utf-8'), state_string, int(time.time())])
|
||||
sql_connection.close()
|
||||
resp.content_type = falcon.MEDIA_HTML
|
||||
resp.text = REDIRECT_HTML_TEMPLATE.format(link=redirect_user_to)
|
||||
|
||||
|
||||
class FDEV_redirect:
|
||||
def on_get(self, req, resp):
|
||||
error_id = hashlib.sha256(os.urandom(32)).hexdigest()
|
||||
code = req.get_param('code')
|
||||
state = req.get_param('state')
|
||||
if not code or not state:
|
||||
raise falcon.HTTPBadRequest(description=f"No code or no state")
|
||||
|
||||
sql_connection = sqlite3.connect(database="companion-api.sqlite")
|
||||
with sql_connection:
|
||||
code_verifier = sql_connection.execute("select code_verifier from authorizations where state = ?;",
|
||||
[state]).fetchone()
|
||||
if code_verifier is None:
|
||||
# Somebody got here not by frontier redirect
|
||||
raise falcon.HTTPBadRequest(description="Don't show here without passing frontier authorization!")
|
||||
code_verifier = code_verifier[0]
|
||||
|
||||
with sql_connection:
|
||||
sql_connection.execute("update authorizations set code = ? where state = ?;", [code, state])
|
||||
|
||||
token_request = requests.post(
|
||||
url=TOKEN_URL,
|
||||
headers={'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': PROPER_USER_AGENT},
|
||||
data=f'redirect_uri={REDIRECT_URL}&code={code}&grant_type=authorization_code&code_verifier={code_verifier}'
|
||||
f'&client_id={CLIENT_ID}')
|
||||
|
||||
if token_request.status_code != 200:
|
||||
# Something went wrong
|
||||
print(f"error_id: {error_id}\n{token_request.text}")
|
||||
with sql_connection:
|
||||
sql_connection.execute("delete from authorizations where state = ?;", [state])
|
||||
sql_connection.close()
|
||||
raise falcon.HTTPBadRequest(description=f'Something went wrong, your error id is {error_id}')
|
||||
|
||||
token_request = token_request.json()
|
||||
access_token = token_request["access_token"]
|
||||
refresh_token = token_request["refresh_token"]
|
||||
expires_in = token_request["expires_in"]
|
||||
timestamp_got_expires_in = int(time.time())
|
||||
|
||||
with sql_connection:
|
||||
sql_connection.execute(
|
||||
"update authorizations set access_token = ?, refresh_token = ?, expires_in = ?, "
|
||||
"timestamp_got_expires_in = ? where state = ?",
|
||||
[access_token, refresh_token, expires_in, timestamp_got_expires_in, state])
|
||||
|
||||
try:
|
||||
nickname = requests.get(
|
||||
url='https://companion.orerve.net/profile',
|
||||
headers={'Authorization': f'Bearer {access_token}',
|
||||
'User-Agent': PROPER_USER_AGENT}).json()["commander"]["name"]
|
||||
try:
|
||||
sql_connection.execute("update authorizations set nickname = ? where state = ?;", [nickname, state])
|
||||
sql_connection.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
already_saved_state = sql_connection.execute(
|
||||
"select state from authorizations where nickname = ?", [nickname]).fetchone()[0]
|
||||
resp.text = f"We already have your token (CMDR {nickname}) in DB, your key: {already_saved_state}"
|
||||
with sql_connection:
|
||||
sql_connection.execute("delete from authorizations where state = ?;", [state])
|
||||
sql_connection.close()
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
resp.text = f"Something went wrong, your error id is {error_id}"
|
||||
print(f"error_id: {error_id}\n{e}")
|
||||
with sql_connection:
|
||||
sql_connection.execute("delete from authorizations where state = ?;", [state])
|
||||
else: # all fine
|
||||
resp.text = f"All right, {nickname}, we have your tokens\nSave and keep this key: {state}"
|
||||
sql_connection.close()
|
||||
|
||||
|
||||
class User:
|
||||
def on_get(self, req, resp, user):
|
||||
sql_connection = sqlite3.connect(database="companion-api.sqlite")
|
||||
if sql_connection.execute('select timestamp_got_expires_in, expires_in from authorizations where state = ?;',
|
||||
[user]).fetchone() is None:
|
||||
sql_connection.close()
|
||||
raise falcon.HTTPNotFound(description="Couldn't find you in DB")
|
||||
|
||||
refresh_one_token(user)
|
||||
|
||||
access_token, expires_timestamp, nickname = sql_connection.execute(
|
||||
'select access_token, timestamp_got_expires_in + expires_in, nickname from authorizations where state = ?',
|
||||
[user]).fetchone()
|
||||
|
||||
import json
|
||||
resp.text = json.dumps({'access_token': access_token,
|
||||
'expires_over': expires_timestamp - int(time.time()),
|
||||
'expires_on': expires_timestamp,
|
||||
'nickname': nickname})
|
||||
resp.content_type = falcon.MEDIA_JSON
|
||||
sql_connection.close()
|
||||
|
||||
def on_delete(self, req, resp, user):
|
||||
sql_connection = sqlite3.connect(database="companion-api.sqlite")
|
||||
|
||||
with sql_connection:
|
||||
sql_connection.execute('delete from authorizations where state = ?;', [user])
|
||||
sql_connection.close()
|
||||
|
||||
|
||||
class Admin:
|
||||
def on_get(self, req, resp, _state):
|
||||
sql_connection = sqlite3.connect(database="companion-api.sqlite")
|
||||
try:
|
||||
if sql_connection.execute(
|
||||
'select nickname from authorizations where state = ?;', [_state]).fetchone()[0] != 'Aleksey31':
|
||||
sql_connection.close()
|
||||
raise falcon.HTTPNotFound(description="You aren't an admin")
|
||||
|
||||
except TypeError:
|
||||
raise falcon.HTTPNotFound(description="You aren't an admin")
|
||||
|
||||
users_html = ""
|
||||
for user in sql_connection.execute(
|
||||
'select nickname, state from authorizations where nickname is not null;').fetchall():
|
||||
user_name = user[0]
|
||||
user_state = "/users/" + user[1]
|
||||
users_html = users_html + config.ADMIN_USER_TEMPLATE.format(link=user_state, desc=user_name) + "\n<br>\n"
|
||||
|
||||
resp.content_type = falcon.MEDIA_HTML
|
||||
resp.text = config.ADMIN_USERS_TEMPLATE.format(users_html)
|
||||
sql_connection.close()
|
||||
|
||||
|
||||
app = falcon.App()
|
||||
|
||||
# routing
|
||||
app.add_route('/authorize', Authorize())
|
||||
app.add_route('/fdev-redirect', FDEV_redirect())
|
||||
app.add_route('/users/{user}', User())
|
||||
app.add_route('/admin/{_state}', Admin())
|
||||
|
||||
waitress.serve(app, host='127.0.0.1', port=9000)
|
71
refresher.py
Normal file
71
refresher.py
Normal file
@ -0,0 +1,71 @@
|
||||
import sqlite3
|
||||
from config import CLIENT_ID, TOKEN_URL
|
||||
import requests
|
||||
import time
|
||||
from hashlib import sha256
|
||||
from os import urandom
|
||||
|
||||
"""
|
||||
1. For every expired access token in DB:
|
||||
Utilise refresh token and write new access_token, refresh_token, expires_in to DB
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def refresh_one_token(_state: str, _force: bool = False):
|
||||
error_id = sha256(urandom(32)).hexdigest()
|
||||
sql_connection = sqlite3.connect(database="companion-api.sqlite")
|
||||
try:
|
||||
tokens_sql_request = sql_connection.execute(
|
||||
'select refresh_token, timestamp_got_expires_in, expires_in from authorizations where state = ?;',
|
||||
[_state]).fetchone()
|
||||
|
||||
if int(time.time()) < int(tokens_sql_request[1]) + int(tokens_sql_request[2]) and not _force:
|
||||
return # token isn't expired and _force is not True
|
||||
|
||||
refresh_token = tokens_sql_request[0]
|
||||
refresh_request = requests.post(
|
||||
url=TOKEN_URL,
|
||||
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
data=f'grant_type=refresh_token&client_id={CLIENT_ID}&refresh_token={refresh_token}')
|
||||
|
||||
if refresh_request.status_code == 418: # Server's maintenance
|
||||
print(f"Error id {error_id}, got 418 status code, {refresh_request.text}")
|
||||
sql_connection.close()
|
||||
return
|
||||
|
||||
try:
|
||||
refresh_request_json = refresh_request.json()
|
||||
|
||||
except Exception as e2:
|
||||
print(f'Error id: {error_id}\n{refresh_request.text, refresh_request.status_code}, {e2}')
|
||||
sql_connection.close()
|
||||
return
|
||||
|
||||
new_access_token = refresh_request_json["access_token"]
|
||||
new_refresh_token = refresh_request_json["refresh_token"]
|
||||
new_expires_in = refresh_request_json["expires_in"]
|
||||
|
||||
sql_connection.execute("update authorizations set access_token = ?, refresh_token = ?, expires_in = ?, "
|
||||
"timestamp_got_expires_in = ? where state = ?;",
|
||||
[new_access_token, new_refresh_token, new_expires_in, int(time.time()), _state])
|
||||
sql_connection.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error id: {error_id}\n{e}\nRequest result: {refresh_request.text}, code '
|
||||
f'{refresh_request.status_code}')
|
||||
sql_connection.execute("delete from authorizations where state = ?;", [_state])
|
||||
sql_connection.commit()
|
||||
sql_connection.close()
|
||||
raise e
|
||||
sql_connection.close()
|
||||
|
||||
|
||||
def refresh_all_suitable_tokens():
|
||||
sql_connection = sqlite3.connect(database="companion-api.sqlite")
|
||||
suitable_tokens = sql_connection.execute("select state from authorizations;")
|
||||
|
||||
for state in suitable_tokens.fetchall():
|
||||
state = state[0]
|
||||
refresh_one_token(state)
|
||||
sql_connection.close()
|
7
requerements.txt
Normal file
7
requerements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
certifi==2021.5.30
|
||||
chardet==4.0.0
|
||||
falcon==3.0.1
|
||||
idna==2.10
|
||||
requests==2.25.1
|
||||
urllib3==1.26.6
|
||||
waitress==2.0.0
|
Loading…
x
Reference in New Issue
Block a user