-
Notifications
You must be signed in to change notification settings - Fork 2
/
moffics.py
executable file
·173 lines (137 loc) · 5.49 KB
/
moffics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env python3
"""
MOFFIcs
Reads reservations from moffi.io API
Return as ICS datas
"""
import argparse
import base64
import json
import sys
from typing import List
from Crypto.Cipher import AES
from flask import Flask, Response, abort, make_response, request
from ics import Calendar, Event
from moffi_sdk.auth import get_auth_token, signin
from moffi_sdk.reservations import ReservationItem, get_reservations
from utils import ConfigError, parse_config
APP = Flask(__name__)
MOFFI_API = "https://api.moffi.io/api"
def encrypt(message: str, key: bytes) -> str:
"""
Encrypt a message with AES256
Return as base64 urlsafe string
"""
cipher = AES.new(key, AES.MODE_EAX)
ciphertext, tag = cipher.encrypt_and_digest(message.encode("utf-8"))
nonce = base64.b64encode(cipher.nonce).decode("utf-8")
tag = base64.b64encode(tag).decode("utf-8")
ciphertext = base64.b64encode(ciphertext).decode("utf-8")
datas = {"nonce": nonce, "tag": tag, "ciphertext": ciphertext}
bdata = base64.urlsafe_b64encode(json.dumps(datas).encode("utf-8")).rstrip(b"=")
return bdata.decode("utf-8")
def decrypt(message: str, key: bytes) -> str:
"""
Decrypt a base64 urlsafe encrypted AES256 message
"""
bmsg = message.encode("utf-8")
padding = b"=" * (4 - (len(bmsg) % 4))
datas = json.loads(base64.urlsafe_b64decode(bmsg + padding))
nonce = base64.b64decode(datas.get("nonce"))
tag = base64.b64decode(datas.get("tag"))
ciphertext = base64.b64decode(datas.get("ciphertext"))
cipher = AES.new(key, AES.MODE_EAX, nonce)
return cipher.decrypt_and_verify(ciphertext, tag).decode("utf-8")
def generate_calendar(events: List[ReservationItem]) -> Calendar:
"""
Generate an ICS Calendar from a list of events
"""
cal = Calendar()
for item in events:
event = Event()
event.name = f"{item.workspace_name} - {item.desk_name}" if item.desk_name else item.workspace_name
event.begin = item.start.isoformat()
event.end = item.end.isoformat()
event.location = item.workspace_address
cal.events.add(event)
return cal
def get_ics_from_moffi(token: str) -> Response:
"""
Get all reservations from moffi
Return a flask responce object
"""
if not token:
abort(500, "missing token in user profile")
reservations = get_reservations(auth_token=token, steps=["waiting", "inProgress"])
calendar = generate_calendar(reservations)
response = make_response(calendar.serialize(), 200)
response.mimetype = "text/html"
return response
@APP.route("/")
def get_with_basicauth():
"""
Main call with basic authentication
"""
auth = request.authorization
if not auth:
abort(401, "missing authentication")
APP.logger.debug(f"Login : {auth.username}") # pylint: disable=no-member
token = get_auth_token(username=auth.username, password=auth.password)
return get_ics_from_moffi(token=token)
@APP.route("/getToken")
def generate_token():
"""
Generate a token to use in /token route
"""
if not APP.config.get("secret_key"):
abort(500, "missing secret key in conf")
auth = request.authorization
if not auth:
abort(401, "missing authentication")
APP.logger.debug(f"Login : {auth.username}") # pylint: disable=no-member
# ensure auth is legitimate
signin(username=auth.username, password=auth.password)
message = json.dumps({"login": auth.username, "password": auth.password})
token = encrypt(message, APP.config.get("secret_key"))
return {"token": token}
@APP.route("/token/<string:token>")
def get_with_token(token: str):
"""
Main call with token authentication
"""
if not APP.config.get("secret_key"):
abort(500, "missing secret key in conf")
authent = decrypt(token, APP.config.get("secret_key"))
jauth = json.loads(authent)
APP.logger.debug(f"Login : {jauth.get('login')}") # pylint: disable=no-member
moffi_token = get_auth_token(username=jauth.get("login"), password=jauth.get("password"))
return get_ics_from_moffi(token=moffi_token)
if __name__ == "__main__":
PARSER = argparse.ArgumentParser()
PARSER.add_argument("--verbose", "-v", action="store_true", help="More verbose")
PARSER.add_argument("--listen", "-l", help="Listen address")
PARSER.add_argument("--port", "-p", help="Listen port")
PARSER.add_argument(
"--secret",
"-s",
help="Secret key for token auth",
)
PARSER.add_argument("--config", help="Config file")
CONFIG_TEMPLATE = {
"verbose": {"section": "Logging", "key": "Verbose", "mandatory": False, "default_value": False},
"listen": {"section": "Moffics", "key": "Listen", "mandatory": True, "default_value": "0.0.0.0"},
"port": {"section": "Moffics", "key": "Port", "mandatory": True, "default_value": "8888"},
"secret": {"section": "Moffics", "key": "Secret", "mandatory": False},
}
try: # pylint: disable=R0801
CONF = parse_config(argv=PARSER.parse_args(), config_template=CONFIG_TEMPLATE)
except ConfigError as ex:
PARSER.print_help()
sys.stderr.write(f"\nerror: {str(ex)}\n")
sys.exit(2)
if CONF.get("secret"):
if len(CONF.get("secret")) not in [16, 24, 32]:
APP.logger.error("Secret key must be 16, 24 or 32 chars long")
sys.exit(1)
APP.config["secret_key"] = CONF.get("secret").encode("utf-8")
APP.run(host=CONF.get("listen"), port=CONF.get("port"), debug=CONF.get("verbose"))