2018-09-20 23:43:04 -07:00
import hmac
import logging
import os
import queue
import socket
import sys
import tempfile
2019-05-20 20:01:14 -07:00
import requests
2018-09-20 23:43:04 -07:00
from distutils . version import LooseVersion as Version
from urllib . request import urlopen
import flask
2018-09-20 23:58:27 -07:00
from flask import Flask , request , render_template , abort , make_response , __version__ as flask_version
2019-05-20 17:59:20 -07:00
from flask_httpauth import HTTPBasicAuth
2018-09-20 23:43:04 -07:00
from . . import strings
2018-09-21 11:14:32 -07:00
from . share_mode import ShareModeWeb
2019-02-11 22:46:39 -08:00
from . receive_mode import ReceiveModeWeb , ReceiveModeWSGIMiddleware , ReceiveModeRequest
2019-04-19 14:25:42 +02:00
from . website_mode import WebsiteModeWeb
2018-09-20 23:43:04 -07: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-02-24 18:11:13 -08:00
try :
flask . cli . show_server_banner = stubbed_show_server_banner
except :
pass
2018-09-20 23:43:04 -07:00
class Web ( object ) :
"""
The Web object is the OnionShare web server , powered by flask
"""
REQUEST_LOAD = 0
REQUEST_STARTED = 1
REQUEST_PROGRESS = 2
REQUEST_OTHER = 3
REQUEST_CANCELED = 4
REQUEST_RATE_LIMIT = 5
2019-01-19 20:43:25 -08:00
REQUEST_UPLOAD_FILE_RENAMED = 6
REQUEST_UPLOAD_SET_DIR = 7
REQUEST_UPLOAD_FINISHED = 8
2019-01-20 15:42:09 -08:00
REQUEST_UPLOAD_CANCELED = 9
REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10
2019-05-20 19:04:50 -07:00
REQUEST_INVALID_SLUG = 11
2018-09-20 23:43:04 -07:00
2018-09-21 11:14:32 -07:00
def __init__ ( self , common , is_gui , mode = ' share ' ) :
2018-09-20 23:43:04 -07:00
self . common = common
2018-09-21 11:41:49 -07:00
self . common . log ( ' Web ' , ' __init__ ' , ' is_gui= {} , mode= {} ' . format ( is_gui , mode ) )
2018-09-20 23:43:04 -07:00
# The flask app
self . app = Flask ( __name__ ,
static_folder = self . common . get_resource_path ( ' static ' ) ,
template_folder = self . common . get_resource_path ( ' templates ' ) )
self . app . secret_key = self . common . random_string ( 8 )
2019-05-20 17:59:20 -07:00
self . auth = HTTPBasicAuth ( )
2019-05-20 19:04:50 -07:00
self . auth . error_handler ( self . error401 )
2018-09-20 23:43:04 -07:00
2019-04-18 19:53:21 -07:00
# Verbose mode?
if self . common . verbose :
self . verbose_mode ( )
2018-09-20 23:43:04 -07:00
# Are we running in GUI mode?
2018-09-21 11:14:32 -07:00
self . is_gui = is_gui
2018-09-20 23:43:04 -07:00
2019-01-20 15:25:36 -08: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-20 23:43:04 -07:00
# Are we using receive mode?
2018-09-21 11:14:32 -07:00
self . mode = mode
if self . mode == ' receive ' :
2018-09-20 23:43:04 -07:00
# Use custom WSGI middleware, to modify environ
self . app . wsgi_app = ReceiveModeWSGIMiddleware ( self . app . wsgi_app , self )
# Use a custom Request class to track upload progess
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.
if Version ( flask_version ) < Version ( ' 0.11 ' ) :
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask . select_jinja_autoescape = self . _safe_select_jinja_autoescape
self . security_headers = [
( ' Content-Security-Policy ' , ' default-src \' self \' ; style-src \' self \' ; script-src \' self \' ; img-src \' self \' data:; ' ) ,
( ' X-Frame-Options ' , ' DENY ' ) ,
( ' X-Xss-Protection ' , ' 1; mode=block ' ) ,
( ' X-Content-Type-Options ' , ' nosniff ' ) ,
( ' Referrer-Policy ' , ' no-referrer ' ) ,
( ' Server ' , ' OnionShare ' )
]
self . q = queue . Queue ( )
self . slug = None
2019-05-20 19:04:50 -07:00
self . reset_invalid_slugs ( )
2018-09-20 23:43:04 -07: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
self . shutdown_slug = self . common . random_string ( 16 )
# Keep track if the server is running
self . running = False
2018-09-21 11:14:32 -07:00
# Define the web app routes
self . define_common_routes ( )
2018-09-20 23:43:04 -07:00
2018-09-21 11:14:32 -07:00
# Create the mode web object, which defines its own routes
self . share_mode = None
self . receive_mode = None
if self . mode == ' receive ' :
2018-09-21 11:41:49 -07:00
self . receive_mode = ReceiveModeWeb ( self . common , self )
2019-04-19 14:25:42 +02:00
elif self . mode == ' website ' :
self . website_mode = WebsiteModeWeb ( self . common , self )
2018-09-21 11:14:32 -07:00
elif self . mode == ' share ' :
2018-09-21 11:41:49 -07:00
self . share_mode = ShareModeWeb ( self . common , self )
2018-09-21 11:14:32 -07:00
def define_common_routes ( self ) :
2018-09-20 23:43:04 -07:00
"""
2019-05-20 17:59:20 -07:00
Common web app routes between all modes .
2018-09-20 23:43:04 -07:00
"""
2019-05-20 17:59:20 -07:00
@self.auth.get_password
def get_pw ( username ) :
if username == ' onionshare ' :
return self . slug
2019-05-20 20:01:14 -07:00
elif username == ' shutdown ' :
return self . shutdown_slug
2019-05-20 17:59:20 -07:00
else :
return None
@self.app.before_request
def conditional_auth_check ( ) :
if not self . common . settings . get ( ' public_mode ' ) :
@self.auth.login_required
def _check_login ( ) :
return None
return _check_login ( )
2018-09-20 23:43:04 -07:00
@self.app.errorhandler ( 404 )
2019-05-20 19:04:50 -07:00
def not_found ( e ) :
2018-09-20 23:43:04 -07:00
return self . error404 ( )
@self.app.route ( " /<slug_candidate>/shutdown " )
def shutdown ( slug_candidate ) :
"""
Stop the flask web server , from the context of an http request .
"""
self . check_shutdown_slug_candidate ( slug_candidate )
self . force_shutdown ( )
return " "
2019-02-14 09:31:39 -08:00
@self.app.route ( " /noscript-xss-instructions " )
def noscript_xss_instructions ( ) :
"""
Display instructions for disabling Tor Browser ' s NoScript XSS setting
"""
r = make_response ( render_template ( ' receive_noscript_xss.html ' ) )
return self . add_security_headers ( r )
2019-05-20 19:04:50 -07:00
def error401 ( self ) :
auth = request . authorization
if auth :
if auth [ ' username ' ] == ' onionshare ' and auth [ ' password ' ] not in self . invalid_slugs :
print ( ' Invalid password guess: {} ' . format ( auth [ ' password ' ] ) )
self . add_request ( Web . REQUEST_INVALID_SLUG , data = auth [ ' password ' ] )
2018-09-20 23:43:04 -07:00
2019-05-20 19:04:50 -07:00
self . invalid_slugs . append ( auth [ ' password ' ] )
self . invalid_slugs_count + = 1
if self . invalid_slugs_count == 20 :
self . add_request ( Web . REQUEST_RATE_LIMIT )
2018-09-20 23:43:04 -07:00
self . force_shutdown ( )
2019-05-20 19:04:50 -07: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-20 23:43:04 -07:00
2019-05-20 19:04:50 -07:00
r = make_response ( render_template ( ' 401.html ' ) , 401 )
return self . add_security_headers ( r )
def error404 ( self ) :
self . add_request ( Web . REQUEST_OTHER , request . path )
2018-09-20 23:43:04 -07:00
r = make_response ( render_template ( ' 404.html ' ) , 404 )
return self . add_security_headers ( r )
2018-10-01 16:42:54 +10:00
def error403 ( self ) :
self . add_request ( Web . REQUEST_OTHER , request . path )
r = make_response ( render_template ( ' 403.html ' ) , 403 )
return self . add_security_headers ( r )
2018-09-20 23:43:04 -07:00
def add_security_headers ( self , r ) :
"""
Add security headers to a request
"""
for header , value in self . security_headers :
r . headers . set ( header , value )
return r
def _safe_select_jinja_autoescape ( self , filename ) :
if filename is None :
return True
return filename . endswith ( ( ' .html ' , ' .htm ' , ' .xml ' , ' .xhtml ' ) )
2019-05-20 19:04:50 -07:00
def add_request ( self , request_type , path = None , data = None ) :
2018-09-20 23:43:04 -07:00
"""
Add a request to the queue , to communicate with the GUI .
"""
self . q . put ( {
' type ' : request_type ,
' path ' : path ,
' data ' : data
} )
def generate_slug ( self , persistent_slug = None ) :
self . common . log ( ' Web ' , ' generate_slug ' , ' persistent_slug= {} ' . format ( persistent_slug ) )
if persistent_slug != None and persistent_slug != ' ' :
self . slug = persistent_slug
self . common . log ( ' Web ' , ' generate_slug ' , ' persistent_slug sent, so slug is: " {} " ' . format ( self . slug ) )
else :
self . slug = self . common . build_slug ( )
self . common . log ( ' Web ' , ' generate_slug ' , ' built random slug: " {} " ' . format ( self . slug ) )
2019-04-18 19:53:21 -07:00
def verbose_mode ( self ) :
2018-09-20 23:43:04 -07:00
"""
2019-04-19 09:32:11 -07:00
Turn on verbose mode , which will log flask errors to a file .
2018-09-20 23:43:04 -07:00
"""
2019-04-19 09:32:11 -07:00
flask_log_filename = os . path . join ( self . common . build_data_dir ( ) , ' flask.log ' )
log_handler = logging . FileHandler ( flask_log_filename )
2018-09-20 23:43:04 -07:00
log_handler . setLevel ( logging . WARNING )
self . app . logger . addHandler ( log_handler )
def check_shutdown_slug_candidate ( self , slug_candidate ) :
self . common . log ( ' Web ' , ' check_shutdown_slug_candidate: slug_candidate= {} ' . format ( slug_candidate ) )
if not hmac . compare_digest ( self . shutdown_slug , slug_candidate ) :
abort ( 404 )
2019-05-20 19:04:50 -07:00
def reset_invalid_slugs ( self ) :
self . invalid_slugs_count = 0
self . invalid_slugs = [ ]
2018-09-20 23:43:04 -07:00
def force_shutdown ( self ) :
"""
Stop the flask web server , from the context of the flask app .
"""
# Shutdown the flask service
try :
func = request . environ . get ( ' werkzeug.server.shutdown ' )
if func is None :
raise RuntimeError ( ' Not running with the Werkzeug Server ' )
func ( )
except :
pass
self . running = False
2019-03-05 10:28:27 +11:00
def start ( self , port , stay_open = False , public_mode = False , slug = None ) :
2018-09-20 23:43:04 -07:00
"""
Start the flask web server .
"""
2019-03-05 10:28:27 +11:00
self . common . log ( ' Web ' , ' start ' , ' port= {} , stay_open= {} , public_mode= {} , slug= {} ' . format ( port , stay_open , public_mode , slug ) )
2018-09-20 23:43:04 -07:00
self . stay_open = stay_open
2019-01-20 15:25:36 -08: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-20 23:43:04 -07:00
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
if os . path . exists ( ' /usr/share/anon-ws-base-files/workstation ' ) :
host = ' 0.0.0.0 '
else :
host = ' 127.0.0.1 '
self . running = True
self . app . run ( host = host , port = port , threaded = True )
def stop ( self , port ) :
"""
Stop the flask web server by loading / shutdown .
"""
2019-01-20 15:25:36 -08:00
self . common . log ( ' Web ' , ' stop ' , ' stopping server ' )
2018-09-20 23:43:04 -07:00
2019-01-20 15:25:36 -08:00
# Let the mode know that the user stopped the server
self . stop_q . put ( True )
2018-09-20 23:43:04 -07:00
2019-02-19 08:37:32 +11:00
# Reset any slug that was in use
2019-03-05 10:28:27 +11:00
self . slug = None
2019-02-19 08:37:32 +11:00
2019-05-20 20:01:14 -07:00
# To stop flask, load http://shutdown:[shutdown_slug]@127.0.0.1/[shutdown_slug]/shutdown
# (We're putting the shutdown_slug in the path as well to make routing simpler)
2018-09-20 23:43:04 -07:00
if self . running :
2019-05-20 20:01:14 -07:00
requests . get ( ' http://127.0.0.1: {} / {} /shutdown ' . format ( port , self . shutdown_slug ) ,
auth = requests . auth . HTTPBasicAuth ( ' shutdown ' , self . shutdown_slug ) )