all repos — caroster @ f23ba3d49ddd49492d4bb06c4a345acf3ec0235c

[Octree] Group carpool to your event https://caroster.io

e2e/src/server.py (view raw)

  1#!/usr/bin/env python3
  2
  3import cgi
  4import ntpath
  5import os
  6import signal
  7import smtplib
  8from email.mime.application import MIMEApplication
  9from email.mime.multipart import MIMEMultipart
 10from email.mime.text import MIMEText
 11from hashlib import scrypt
 12from http import HTTPStatus, cookies
 13from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
 14
 15from itsdangerous import URLSafeTimedSerializer
 16
 17SERVER_PORT = int(os.getenv('SERVER_PORT', '8080'))
 18SMTP_HOST = os.getenv('SMTP_HOST', 'localhost:1025')
 19USER_NAME = os.getenv('USER_NAME')
 20USER_MAIL = os.getenv('USER_MAIL', 'user@example')
 21USER_SALT = os.getenv('USER_SALT', '').encode() or \
 22    os.urandom(32).hex().encode()
 23USER_HASH = bytes.fromhex(os.getenv('USER_HASH', '')) or \
 24    scrypt('password'.encode(), salt=USER_SALT, n=2, r=8, p=1)
 25SECRET_KEY = os.getenv('SECRET_KEY') or os.urandom(32).hex()
 26SESSION_SECURE = os.getenv('SESSION_SECURE')
 27SESSION_MAX_AGE = int(os.getenv('SESSION_MAX_AGE', 0)) or None
 28HTML_DIR = os.path.dirname(__file__) + '/html'
 29
 30# Monkey-patch Python 3.7 cookies module to support the SameSite attribute:
 31cookies.Morsel._reserved[str('samesite')] = str('SameSite')
 32
 33
 34class MailRequestHandler(SimpleHTTPRequestHandler):
 35
 36    def get_sender(self):
 37        if USER_NAME:
 38            return '%s <%s>' % USER_NAME, USER_MAIL
 39        else:
 40            return USER_MAIL
 41
 42    def send_mail(self, **kwargs):
 43        msg = MIMEMultipart()
 44        msg['From'] = self.get_sender()
 45        msg['To'] = kwargs.get('recipient', '')
 46        msg['Subject'] = kwargs.get('subject', '')
 47        msg.attach(MIMEText(kwargs.get('content', '')))
 48        for attachment in kwargs.get('attachments', []):
 49            if not attachment.filename:
 50                break
 51            part = MIMEApplication(attachment.file.read())
 52            if attachment.type:
 53                part.set_type(attachment.type)
 54            part.add_header(
 55                'Content-Disposition',
 56                'attachment',
 57                # Remove Windows directory path added by IE:
 58                filename=ntpath.basename(attachment.filename)
 59            )
 60            msg.attach(part)
 61        smtp = smtplib.SMTP(SMTP_HOST)
 62        smtp.send_message(msg)
 63        smtp.quit()
 64
 65    def get_form_data(self):
 66        return cgi.FieldStorage(
 67            fp=self.rfile,
 68            headers=self.headers,
 69            environ={
 70                'REQUEST_METHOD': self.command,
 71                'CONTENT_TYPE': self.headers.get('content-type'),
 72            }
 73        )
 74
 75    def get_attachments(self, form):
 76        if 'attachments' in form:
 77            if type(form['attachments']) is list:
 78                return form['attachments']
 79            else:
 80                return [form['attachments']]
 81        else:
 82            return []
 83
 84    def create_cookie(self, value, max_age=None):
 85        cookie = cookies.SimpleCookie()
 86        cookie['session'] = value
 87        cookie['session']['path'] = '/'
 88        cookie['session']['httponly'] = True
 89        cookie['session']['samesite'] = 'Strict'
 90        if SESSION_SECURE:
 91            cookie['session']['secure'] = True
 92        if max_age is not None:
 93            cookie['session']['max-age'] = max_age
 94        return cookie
 95
 96    def login(self, **kwargs):
 97        if kwargs.get('email', '') != USER_MAIL:
 98            return None
 99        # Limit password length to 1024:
100        password = kwargs.get('password', '')[0:1024].encode()
101        password_hash = scrypt(password, salt=USER_SALT, n=2, r=8, p=1)
102        if password_hash != USER_HASH:
103            return None
104        auth = URLSafeTimedSerializer(SECRET_KEY).dumps({'email': USER_MAIL})
105        return self.create_cookie(auth, SESSION_MAX_AGE)
106
107    def logout(self):
108        return self.create_cookie(0, 0)
109
110    def redirect(self, location, cookie=None):
111        self.send_response(HTTPStatus.FOUND)
112        if cookie:
113            self.send_header('Set-Cookie', cookie.output(header='', sep=''))
114        self.send_header('Location', location)
115        self.end_headers()
116
117    def authenticate(self):
118        try:
119            cookie = cookies.SimpleCookie(self.headers.get('cookie'))
120            session = cookie['session'].value
121            auth = URLSafeTimedSerializer(SECRET_KEY).loads(
122                session, max_age=SESSION_MAX_AGE)
123            if auth['email'] != USER_MAIL:
124                raise Exception('Email %s does not match %s' %
125                                auth['email'], USER_MAIL)
126        except Exception as err:
127            self.log_error('code %d, message %s',
128                           HTTPStatus.UNAUTHORIZED, str(err))
129            self.redirect('/login.html')
130            return False
131        return True
132
133    def handle_login(self):
134        form = self.get_form_data()
135        cookie = self.login(
136            email=form.getfirst('email', ''),
137            password=form.getfirst('password', '')
138        )
139        if cookie:
140            self.redirect('/', cookie)
141        else:
142            self.send_error(
143                HTTPStatus.UNAUTHORIZED,
144                'Authorization failed',
145                'Incorrect email or password'
146            )
147
148    def handle_logout(self):
149        if not self.authenticate():
150            return None
151        self.redirect('/login.html', self.logout())
152
153    def handle_mail(self):
154        if not self.authenticate():
155            return None
156        form = self.get_form_data()
157        try:
158            self.send_mail(
159                recipient=form.getfirst('recipient', ''),
160                subject=form.getfirst('subject', ''),
161                content=form.getfirst('content', ''),
162                attachments=self.get_attachments(form)
163            )
164        except Exception as err:
165            self.send_error(
166                HTTPStatus.BAD_GATEWAY,
167                'Sending mail failed',
168                str(err)
169            )
170            return None
171        self.redirect('/sent.html')
172
173    def do_POST(self):
174        if self.path == '/':
175            self.handle_mail()
176        elif self.path == '/login':
177            self.handle_login()
178        elif self.path == '/logout':
179            self.handle_logout()
180        else:
181            self.send_error(HTTPStatus.METHOD_NOT_ALLOWED)
182
183    def do_GET(self):
184        if self.path != '/' or self.authenticate():
185            super().do_GET()
186
187    def do_HEAD(self):
188        if self.path != '/' or self.authenticate():
189            super().do_HEAD()
190
191    # Override to add cache-control and vary headers and remove server header:
192    def send_response(self, code, message=None):
193        self.log_request(code)
194        self.send_response_only(code, message)
195        self.send_header('Date', self.date_time_string())
196        if self.path == '/':
197            self.send_header('Cache-Control', 'no-cache')
198            self.send_header('Vary', 'Cookie')
199
200
201def handle_exit(sig, frame): raise(SystemExit)
202
203
204# Graceful shutdown on SIGTERM/SIGINT:
205signal.signal(signal.SIGTERM, handle_exit)
206signal.signal(signal.SIGINT, handle_exit)
207
208try:
209    os.chdir(HTML_DIR)
210    server = ThreadingHTTPServer(('', SERVER_PORT), MailRequestHandler)
211    server.serve_forever()
212except SystemExit:
213    server.socket.close()