2020-08-27 19:13:08 -04:00
# -*- coding: utf-8 -*-
"""
OnionShare | https : / / onionshare . org /
2021-02-22 16:35:14 -05:00
Copyright ( C ) 2014 - 2021 Micah Lee , et al . < micah @micahflee.com >
2020-08-27 19:13:08 -04:00
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program . If not , see < http : / / www . gnu . org / licenses / > .
"""
2018-09-21 02:43:04 -04:00
import logging
import os
import queue
2019-05-20 23:01:14 -04:00
import requests
2021-05-04 02:21:42 -04:00
import shutil
2018-09-21 02:43:04 -04:00
from distutils . version import LooseVersion as Version
import flask
2019-10-13 00:01:25 -04:00
from flask import (
Flask ,
request ,
render_template ,
abort ,
make_response ,
send_file ,
__version__ as flask_version ,
)
2019-05-20 20:59:20 -04:00
from flask_httpauth import HTTPBasicAuth
2020-03-08 05:21:43 -04:00
from flask_socketio import SocketIO
2018-09-21 02:43:04 -04:00
2018-09-21 14:14:32 -04:00
from . share_mode import ShareModeWeb
2019-02-12 01:46:39 -05:00
from . receive_mode import ReceiveModeWeb , ReceiveModeWSGIMiddleware , ReceiveModeRequest
2019-04-19 08:25:42 -04:00
from . website_mode import WebsiteModeWeb
2020-03-08 05:21:43 -04:00
from . chat_mode import ChatModeWeb
2018-09-21 02:43:04 -04:00
2021-04-29 20:13:05 -04:00
2018-09-21 02:43:04 -04:00
# Stub out flask's show_server_banner function, to avoiding showing warnings that
# are not applicable to OnionShare
def stubbed_show_server_banner ( env , debug , app_import_path , eager_loading ) :
pass
2019-10-13 00:01:25 -04:00
2019-02-24 21:11:13 -05:00
try :
flask . cli . show_server_banner = stubbed_show_server_banner
2021-04-29 20:13:05 -04:00
except Exception :
2019-02-24 21:11:13 -05:00
pass
2018-09-21 02:43:04 -04:00
2019-09-01 19:03:57 -04:00
class Web :
2018-09-21 02:43:04 -04:00
"""
The Web object is the OnionShare web server , powered by flask
"""
2019-10-13 00:01:25 -04:00
2018-09-21 02:43:04 -04:00
REQUEST_LOAD = 0
REQUEST_STARTED = 1
REQUEST_PROGRESS = 2
2019-09-04 00:46:32 -04:00
REQUEST_CANCELED = 3
REQUEST_RATE_LIMIT = 4
2021-04-30 20:16:02 -04:00
REQUEST_UPLOAD_INCLUDES_MESSAGE = 5
REQUEST_UPLOAD_FILE_RENAMED = 6
REQUEST_UPLOAD_SET_DIR = 7
REQUEST_UPLOAD_FINISHED = 8
REQUEST_UPLOAD_CANCELED = 9
REQUEST_INDIVIDUAL_FILE_STARTED = 10
REQUEST_INDIVIDUAL_FILE_PROGRESS = 11
REQUEST_INDIVIDUAL_FILE_CANCELED = 12
REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 13
REQUEST_OTHER = 14
REQUEST_INVALID_PASSWORD = 15
2018-09-21 02:43:04 -04:00
2019-11-02 17:35:51 -04:00
def __init__ ( self , common , is_gui , mode_settings , mode = " share " ) :
2018-09-21 02:43:04 -04:00
self . common = common
2019-10-20 13:15:16 -04:00
self . common . log ( " Web " , " __init__ " , f " is_gui= { is_gui } , mode= { mode } " )
2018-09-21 02:43:04 -04:00
2019-11-02 17:35:51 -04:00
self . settings = mode_settings
2019-11-02 13:43:20 -04:00
2018-09-21 02:43:04 -04:00
# The flask app
2019-10-13 00:01:25 -04:00
self . app = Flask (
__name__ ,
static_folder = self . common . get_resource_path ( " static " ) ,
2019-10-20 13:15:16 -04:00
static_url_path = f " /static_ { self . common . random_string ( 16 ) } " , # randomize static_url_path to avoid making /static unusable
2019-10-13 00:01:25 -04:00
template_folder = self . common . get_resource_path ( " templates " ) ,
)
2018-09-21 02:43:04 -04:00
self . app . secret_key = self . common . random_string ( 8 )
2019-09-01 19:03:57 -04:00
self . generate_static_url_path ( )
2019-05-20 20:59:20 -04:00
self . auth = HTTPBasicAuth ( )
2019-05-20 22:04:50 -04:00
self . auth . error_handler ( self . error401 )
2018-09-21 02:43:04 -04:00
2019-04-18 22:53:21 -04:00
# Verbose mode?
if self . common . verbose :
self . verbose_mode ( )
2018-09-21 02:43:04 -04:00
# Are we running in GUI mode?
2018-09-21 14:14:32 -04:00
self . is_gui = is_gui
2018-09-21 02:43:04 -04:00
2019-01-20 18:25:36 -05:00
# If the user stops the server while a transfer is in progress, it should
# immediately stop the transfer. In order to make it thread-safe, stop_q
# is a queue. If anything is in it, then the user stopped the server
self . stop_q = queue . Queue ( )
2018-09-21 02:43:04 -04:00
# Are we using receive mode?
2018-09-21 14:14:32 -04:00
self . mode = mode
2019-10-13 00:01:25 -04:00
if self . mode == " receive " :
2018-09-21 02:43:04 -04:00
# Use custom WSGI middleware, to modify environ
self . app . wsgi_app = ReceiveModeWSGIMiddleware ( self . app . wsgi_app , self )
2020-12-23 18:34:47 -05:00
# Use a custom Request class to track upload progress
2018-09-21 02:43:04 -04:00
self . app . request_class = ReceiveModeRequest
# Starting in Flask 0.11, render_template_string autoescapes template variables
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
2019-10-13 00:01:25 -04:00
if Version ( flask_version ) < Version ( " 0.11 " ) :
2018-09-21 02:43:04 -04:00
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask . select_jinja_autoescape = self . _safe_select_jinja_autoescape
2019-09-15 22:30:20 -04:00
self . security_headers = [
2019-10-13 00:01:25 -04:00
( " X-Frame-Options " , " DENY " ) ,
( " X-Xss-Protection " , " 1; mode=block " ) ,
( " X-Content-Type-Options " , " nosniff " ) ,
( " Referrer-Policy " , " no-referrer " ) ,
( " Server " , " OnionShare " ) ,
2019-09-15 22:30:20 -04:00
]
2018-09-21 02:43:04 -04:00
self . q = queue . Queue ( )
2019-05-21 01:18:49 -04:00
self . password = None
2019-05-20 22:04:50 -04:00
2019-05-21 01:18:49 -04:00
self . reset_invalid_passwords ( )
2018-09-21 02:43:04 -04:00
self . done = False
# shutting down the server only works within the context of flask, so the easiest way to do it is over http
2019-05-21 01:18:49 -04:00
self . shutdown_password = self . common . random_string ( 16 )
2018-09-21 02:43:04 -04:00
# Keep track if the server is running
self . running = False
2018-09-21 14:14:32 -04:00
# Define the web app routes
self . define_common_routes ( )
2018-09-21 02:43:04 -04:00
2018-09-21 14:14:32 -04:00
# Create the mode web object, which defines its own routes
self . share_mode = None
self . receive_mode = None
2019-09-15 17:46:29 -04:00
self . website_mode = None
2020-03-08 05:21:43 -04:00
self . chat_mode = None
2019-10-13 00:01:25 -04:00
if self . mode == " share " :
2019-09-15 17:46:29 -04:00
self . share_mode = ShareModeWeb ( self . common , self )
2019-10-13 00:01:25 -04:00
elif self . mode == " receive " :
2018-09-21 14:41:49 -04:00
self . receive_mode = ReceiveModeWeb ( self . common , self )
2019-10-13 00:01:25 -04:00
elif self . mode == " website " :
2019-04-19 08:25:42 -04:00
self . website_mode = WebsiteModeWeb ( self . common , self )
2020-03-08 05:21:43 -04:00
elif self . mode == " chat " :
self . socketio = SocketIO ( )
self . socketio . init_app ( self . app )
self . chat_mode = ChatModeWeb ( self . common , self )
2019-09-15 17:46:29 -04:00
2021-05-04 02:21:42 -04:00
self . cleanup_filenames = [ ]
2019-09-15 17:46:29 -04:00
def get_mode ( self ) :
2019-10-13 00:01:25 -04:00
if self . mode == " share " :
2019-09-15 17:46:29 -04:00
return self . share_mode
2019-10-13 00:01:25 -04:00
elif self . mode == " receive " :
2019-09-15 17:46:29 -04:00
return self . receive_mode
2019-10-13 00:01:25 -04:00
elif self . mode == " website " :
2019-09-15 17:46:29 -04:00
return self . website_mode
2020-03-08 05:21:43 -04:00
elif self . mode == " chat " :
return self . chat_mode
2019-09-15 17:46:29 -04:00
else :
return None
2018-09-21 14:14:32 -04:00
2019-09-01 19:03:57 -04:00
def generate_static_url_path ( self ) :
# The static URL path has a 128-bit random number in it to avoid having name
# collisions with files that might be getting shared
2019-10-20 13:15:16 -04:00
self . static_url_path = f " /static_ { self . common . random_string ( 16 ) } "
2019-10-13 00:01:25 -04:00
self . common . log (
" Web " ,
" generate_static_url_path " ,
2019-10-20 13:15:16 -04:00
f " new static_url_path is { self . static_url_path } " ,
2019-10-13 00:01:25 -04:00
)
2019-09-01 19:03:57 -04:00
# Update the flask route to handle the new static URL path
self . app . static_url_path = self . static_url_path
self . app . add_url_rule (
2019-10-13 00:01:25 -04:00
self . static_url_path + " /<path:filename> " ,
endpoint = " static " ,
view_func = self . app . send_static_file ,
)
2018-09-21 14:14:32 -04:00
def define_common_routes ( self ) :
2018-09-21 02:43:04 -04:00
"""
2019-05-20 20:59:20 -04:00
Common web app routes between all modes .
2018-09-21 02:43:04 -04:00
"""
2019-05-20 20:59:20 -04:00
@self.auth.get_password
def get_pw ( username ) :
2019-10-13 00:01:25 -04:00
if username == " onionshare " :
2019-05-21 01:18:49 -04:00
return self . password
2019-05-20 20:59:20 -04:00
else :
return None
@self.app.before_request
def conditional_auth_check ( ) :
2019-05-22 23:15:49 -04:00
# Allow static files without basic authentication
2019-10-13 00:01:25 -04:00
if request . path . startswith ( self . static_url_path + " / " ) :
2019-05-22 23:15:49 -04:00
return None
# If public mode is disabled, require authentication
2019-11-02 17:35:51 -04:00
if not self . settings . get ( " general " , " public " ) :
2019-10-13 00:01:25 -04:00
2019-05-20 20:59:20 -04:00
@self.auth.login_required
def _check_login ( ) :
return None
return _check_login ( )
2021-05-13 04:17:51 -04:00
@self.app.after_request
2021-05-31 08:04:24 -04:00
def add_security_headers ( self , r ) :
2021-05-13 04:17:51 -04:00
"""
Add security headers to a response
"""
for header , value in self . security_headers :
r . headers . set ( header , value )
# Set a CSP header unless in website mode and the user has disabled it
if not self . settings . get ( " website " , " disable_csp " ) or self . mode != " website " :
r . headers . set (
" Content-Security-Policy " ,
" default-src ' self ' ; frame-ancestors ' none ' ; form-action ' self ' ; base-uri ' self ' ; img-src ' self ' data:; " ,
)
return r
2018-09-21 02:43:04 -04:00
@self.app.errorhandler ( 404 )
2019-05-20 22:04:50 -04:00
def not_found ( e ) :
2019-09-15 17:46:29 -04:00
mode = self . get_mode ( )
history_id = mode . cur_history_id
mode . cur_history_id + = 1
return self . error404 ( history_id )
2018-09-21 02:43:04 -04:00
2021-05-31 08:04:24 -04:00
@self.app.errorhandler ( 405 )
def method_not_allowed ( e ) :
mode = self . get_mode ( )
history_id = mode . cur_history_id
mode . cur_history_id + = 1
return self . error405 ( history_id )
@self.app.errorhandler ( 500 )
def method_not_allowed ( e ) :
mode = self . get_mode ( )
history_id = mode . cur_history_id
mode . cur_history_id + = 1
return self . error500 ( history_id )
2019-05-21 01:18:49 -04:00
@self.app.route ( " /<password_candidate>/shutdown " )
def shutdown ( password_candidate ) :
2018-09-21 02:43:04 -04:00
"""
Stop the flask web server , from the context of an http request .
"""
2019-05-21 01:18:49 -04:00
if password_candidate == self . shutdown_password :
2019-05-21 01:02:43 -04:00
self . force_shutdown ( )
return " "
abort ( 404 )
2018-09-21 02:43:04 -04:00
2019-10-13 00:01:25 -04:00
if self . mode != " website " :
2019-09-09 03:22:18 -04:00
@self.app.route ( " /favicon.ico " )
def favicon ( ) :
2019-10-13 00:01:25 -04:00
return send_file (
2019-10-20 20:59:12 -04:00
f " { self . common . get_resource_path ( ' static ' ) } /img/favicon.ico "
2019-10-13 00:01:25 -04:00
)
2019-09-09 03:22:18 -04:00
2019-05-20 22:04:50 -04:00
def error401 ( self ) :
auth = request . authorization
if auth :
2019-10-13 00:01:25 -04:00
if (
auth [ " username " ] == " onionshare "
and auth [ " password " ] not in self . invalid_passwords
) :
2019-10-20 20:59:12 -04:00
print ( f " Invalid password guess: { auth [ ' password ' ] } " )
2019-10-13 00:01:25 -04:00
self . add_request ( Web . REQUEST_INVALID_PASSWORD , data = auth [ " password " ] )
self . invalid_passwords . append ( auth [ " password " ] )
2019-05-21 01:18:49 -04:00
self . invalid_passwords_count + = 1
2019-05-20 22:04:50 -04:00
2019-05-21 01:18:49 -04:00
if self . invalid_passwords_count == 20 :
2019-05-20 22:04:50 -04:00
self . add_request ( Web . REQUEST_RATE_LIMIT )
2018-09-21 02:43:04 -04:00
self . force_shutdown ( )
2019-10-13 00:01:25 -04:00
print (
" Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share. "
)
2018-09-21 02:43:04 -04:00
2021-05-13 04:17:51 -04:00
return render_template ( " 401.html " , static_url_path = self . static_url_path ) , 401
2019-05-20 22:04:50 -04:00
2019-09-04 00:46:32 -04:00
def error403 ( self ) :
2019-05-20 22:04:50 -04:00
self . add_request ( Web . REQUEST_OTHER , request . path )
2021-05-13 04:17:51 -04:00
return render_template ( " 403.html " , static_url_path = self . static_url_path ) , 403
2018-10-01 02:42:54 -04:00
2019-09-09 02:35:05 -04:00
def error404 ( self , history_id ) :
2021-05-31 08:04:24 -04:00
mode = self . get_mode ( )
if mode . supports_file_requests :
self . add_request (
self . REQUEST_INDIVIDUAL_FILE_STARTED ,
request . path ,
{ " id " : history_id , " status_code " : 404 } ,
)
2019-09-04 01:18:30 -04:00
2018-10-01 02:42:54 -04:00
self . add_request ( Web . REQUEST_OTHER , request . path )
2021-05-13 04:17:51 -04:00
return render_template ( " 404.html " , static_url_path = self . static_url_path ) , 404
2018-10-01 02:42:54 -04:00
2019-10-18 23:50:40 -04:00
def error405 ( self , history_id ) :
2021-05-31 08:04:24 -04:00
mode = self . get_mode ( )
if mode . supports_file_requests :
self . add_request (
self . REQUEST_INDIVIDUAL_FILE_STARTED ,
request . path ,
{ " id " : history_id , " status_code " : 405 } ,
)
2019-10-18 23:50:40 -04:00
self . add_request ( Web . REQUEST_OTHER , request . path )
2021-05-13 04:17:51 -04:00
return render_template ( " 405.html " , static_url_path = self . static_url_path ) , 405
2018-09-21 02:43:04 -04:00
2021-05-31 08:04:24 -04:00
def error500 ( self , history_id ) :
mode = self . get_mode ( )
if mode . supports_file_requests :
self . add_request (
self . REQUEST_INDIVIDUAL_FILE_STARTED ,
request . path ,
{ " id " : history_id , " status_code " : 500 } ,
)
self . add_request ( Web . REQUEST_OTHER , request . path )
return render_template ( " 500.html " , static_url_path = self . static_url_path ) , 500
2018-09-21 02:43:04 -04:00
def _safe_select_jinja_autoescape ( self , filename ) :
if filename is None :
return True
2019-10-13 00:01:25 -04:00
return filename . endswith ( ( " .html " , " .htm " , " .xml " , " .xhtml " ) )
2018-09-21 02:43:04 -04:00
2019-05-20 22:04:50 -04:00
def add_request ( self , request_type , path = None , data = None ) :
2018-09-21 02:43:04 -04:00
"""
Add a request to the queue , to communicate with the GUI .
"""
2019-10-13 00:01:25 -04:00
self . q . put ( { " type " : request_type , " path " : path , " data " : data } )
2018-09-21 02:43:04 -04:00
2019-12-08 13:13:56 -05:00
def generate_password ( self , saved_password = None ) :
self . common . log ( " Web " , " generate_password " , f " saved_password= { saved_password } " )
2021-04-29 20:13:05 -04:00
if saved_password is not None and saved_password != " " :
2019-12-08 13:13:56 -05:00
self . password = saved_password
2019-10-13 00:01:25 -04:00
self . common . log (
" Web " ,
" generate_password " ,
2019-12-08 13:13:56 -05:00
f ' saved_password sent, so password is: " { self . password } " ' ,
2019-10-13 00:01:25 -04:00
)
2018-09-21 02:43:04 -04:00
else :
2019-05-21 01:18:49 -04:00
self . password = self . common . build_password ( )
2019-10-13 00:01:25 -04:00
self . common . log (
2019-10-20 20:59:12 -04:00
" Web " , " generate_password " , f ' built random password: " { self . password } " '
2019-10-13 00:01:25 -04:00
)
2018-09-21 02:43:04 -04:00
2019-04-18 22:53:21 -04:00
def verbose_mode ( self ) :
2018-09-21 02:43:04 -04:00
"""
2019-04-19 12:32:11 -04:00
Turn on verbose mode , which will log flask errors to a file .
2018-09-21 02:43:04 -04:00
"""
2019-10-13 00:01:25 -04:00
flask_log_filename = os . path . join ( self . common . build_data_dir ( ) , " flask.log " )
2019-04-19 12:32:11 -04:00
log_handler = logging . FileHandler ( flask_log_filename )
2018-09-21 02:43:04 -04:00
log_handler . setLevel ( logging . WARNING )
self . app . logger . addHandler ( log_handler )
2019-05-21 01:18:49 -04:00
def reset_invalid_passwords ( self ) :
self . invalid_passwords_count = 0
self . invalid_passwords = [ ]
2019-05-20 22:04:50 -04:00
2018-09-21 02:43:04 -04:00
def force_shutdown ( self ) :
"""
Stop the flask web server , from the context of the flask app .
"""
# Shutdown the flask service
try :
2019-10-13 00:01:25 -04:00
func = request . environ . get ( " werkzeug.server.shutdown " )
2021-04-18 17:29:22 -04:00
if func is None and self . mode != " chat " :
2019-10-13 00:01:25 -04:00
raise RuntimeError ( " Not running with the Werkzeug Server " )
2018-09-21 02:43:04 -04:00
func ( )
2021-04-29 20:13:05 -04:00
except Exception :
2018-09-21 02:43:04 -04:00
pass
2021-04-18 17:29:22 -04:00
2018-09-21 02:43:04 -04:00
self . running = False
2021-04-18 17:29:22 -04:00
# If chat, shutdown the socket server
if self . mode == " chat " :
self . socketio . stop ( )
2019-11-02 18:37:21 -04:00
def start ( self , port ) :
2018-09-21 02:43:04 -04:00
"""
Start the flask web server .
"""
2019-11-02 18:37:21 -04:00
self . common . log ( " Web " , " start " , f " port= { port } " )
2018-09-21 02:43:04 -04:00
2019-01-20 18:25:36 -05:00
# Make sure the stop_q is empty when starting a new server
while not self . stop_q . empty ( ) :
try :
self . stop_q . get ( block = False )
except queue . Empty :
pass
2018-09-21 02:43:04 -04:00
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
2019-10-13 00:01:25 -04:00
if os . path . exists ( " /usr/share/anon-ws-base-files/workstation " ) :
host = " 0.0.0.0 "
2018-09-21 02:43:04 -04:00
else :
2019-10-13 00:01:25 -04:00
host = " 127.0.0.1 "
2018-09-21 02:43:04 -04:00
self . running = True
2020-03-08 05:21:43 -04:00
if self . mode == " chat " :
self . socketio . run ( self . app , host = host , port = port )
else :
self . app . run ( host = host , port = port , threaded = True )
2018-09-21 02:43:04 -04:00
def stop ( self , port ) :
"""
Stop the flask web server by loading / shutdown .
"""
2019-10-13 00:01:25 -04:00
self . common . log ( " Web " , " stop " , " stopping server " )
2018-09-21 02:43:04 -04:00
2019-01-20 18:25:36 -05:00
# Let the mode know that the user stopped the server
self . stop_q . put ( True )
2018-09-21 02:43:04 -04:00
2019-05-21 01:18:49 -04:00
# To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown
# (We're putting the shutdown_password in the path as well to make routing simpler)
2018-09-21 02:43:04 -04:00
if self . running :
2019-11-10 16:36:35 -05:00
if self . password :
requests . get (
f " http://127.0.0.1: { port } / { self . shutdown_password } /shutdown " ,
auth = requests . auth . HTTPBasicAuth ( " onionshare " , self . password ) ,
)
else :
requests . get (
f " http://127.0.0.1: { port } / { self . shutdown_password } /shutdown "
)
2019-05-21 01:02:43 -04:00
2019-05-21 01:18:49 -04:00
# Reset any password that was in use
self . password = None
2021-05-04 02:21:42 -04:00
def cleanup ( self ) :
"""
Shut everything down and clean up temporary files , etc .
"""
self . common . log ( " Web " , " cleanup " )
# Cleanup files
try :
for filename in self . cleanup_filenames :
if os . path . isfile ( filename ) :
os . remove ( filename )
elif os . path . isdir ( filename ) :
shutil . rmtree ( filename )
except Exception :
# Don't crash if file is still in use
pass
self . cleanup_filenames = [ ]