From 4f27fac8408383b17326c07909b525cd72f0f9ac Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:58:27 -0700 Subject: [PATCH] Refactor web to push share and receive mode logic into their respective files --- onionshare/web/receive_mode.py | 157 ++++++++++++++- onionshare/web/share_mode.py | 177 +++++++++++++++++ onionshare/web/web.py | 338 +-------------------------------- 3 files changed, 338 insertions(+), 334 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 90accc8c..0ebc9ccd 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -1,10 +1,165 @@ +import os import tempfile from datetime import datetime -from flask import Request +from flask import Request, request, render_template, make_response, flash, redirect +from werkzeug.utils import secure_filename +from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable from .. import strings +def receive_routes(web): + """ + The web app routes for receiving files + """ + def index_logic(): + web.add_request(web.REQUEST_LOAD, request.path) + + if web.common.settings.get('public_mode'): + upload_action = '/upload' + close_action = '/close' + else: + upload_action = '/{}/upload'.format(web.slug) + close_action = '/{}/close'.format(web.slug) + + r = make_response(render_template( + 'receive.html', + upload_action=upload_action, + close_action=close_action, + receive_allow_receiver_shutdown=web.common.settings.get('receive_allow_receiver_shutdown'))) + return web.add_security_headers(r) + + @web.app.route("/") + def index(slug_candidate): + web.check_slug_candidate(slug_candidate) + return index_logic() + + @web.app.route("/") + def index_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return index_logic() + + + def upload_logic(slug_candidate=''): + """ + Upload files. + """ + # Make sure downloads_dir exists + valid = True + try: + web.common.validate_downloads_dir() + except DownloadsDirErrorCannotCreate: + web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) + print(strings._('error_cannot_create_downloads_dir').format(web.common.settings.get('downloads_dir'))) + valid = False + except DownloadsDirErrorNotWritable: + web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) + print(strings._('error_downloads_dir_not_writable').format(web.common.settings.get('downloads_dir'))) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + files = request.files.getlist('file[]') + filenames = [] + print('') + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(web.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + basename = os.path.basename(local_path) + if f.filename != basename: + # Tell the GUI that the file has changed names + web.add_request(web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { + 'id': request.upload_id, + 'old_filename': f.filename, + 'new_filename': basename + }) + + web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print(strings._('receive_mode_received_file').format(local_path)) + f.save(local_path) + + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + if len(filenames) == 0: + flash('No files uploaded', 'info') + else: + for filename in filenames: + flash('Sent {}'.format(filename), 'info') + + if web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + @web.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + web.check_slug_candidate(slug_candidate) + return upload_logic(slug_candidate) + + @web.app.route("/upload", methods=['POST']) + def upload_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return upload_logic() + + + def close_logic(slug_candidate=''): + if web.common.settings.get('receive_allow_receiver_shutdown'): + web.force_shutdown() + r = make_response(render_template('closed.html')) + web.add_request(web.REQUEST_CLOSE_SERVER, request.path) + return web.add_security_headers(r) + else: + return redirect('/{}'.format(slug_candidate)) + + @web.app.route("//close", methods=['POST']) + def close(slug_candidate): + web.check_slug_candidate(slug_candidate) + return close_logic(slug_candidate) + + @web.app.route("/close", methods=['POST']) + def close_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return close_logic() + + class ReceiveModeWSGIMiddleware(object): """ Custom WSGI middleware in order to attach the Web object to environ, so diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index f066bde4..58cc9b99 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -1,6 +1,183 @@ import os +import sys import tempfile import zipfile +import mimetypes +from flask import Response, request, render_template, make_response + +from .. import strings + + +def share_routes(web): + """ + The web app routes for sharing files + """ + @web.app.route("/") + def index(slug_candidate): + web.check_slug_candidate(slug_candidate) + return index_logic() + + @web.app.route("/") + def index_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return index_logic() + + def index_logic(slug_candidate=''): + """ + Render the template for the onionshare landing page. + """ + web.add_request(web.REQUEST_LOAD, request.path) + + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not web.stay_open and web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return web.add_security_headers(r) + + # If download is allowed to continue, serve download page + if web.slug: + r = make_response(render_template( + 'send.html', + slug=web.slug, + file_info=web.file_info, + filename=os.path.basename(web.download_filename), + filesize=web.download_filesize, + filesize_human=web.common.human_readable_filesize(web.download_filesize), + is_zipped=web.is_zipped)) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=web.file_info, + filename=os.path.basename(web.download_filename), + filesize=web.download_filesize, + filesize_human=web.common.human_readable_filesize(web.download_filesize), + is_zipped=web.is_zipped)) + return web.add_security_headers(r) + + @web.app.route("//download") + def download(slug_candidate): + web.check_slug_candidate(slug_candidate) + return download_logic() + + @web.app.route("/download") + def download_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return download_logic() + + def download_logic(slug_candidate=''): + """ + Download the zip file. + """ + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not web.stay_open and web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return web.add_security_headers(r) + + # Each download has a unique id + download_id = web.download_count + web.download_count += 1 + + # Prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path + + # Tell GUI the download started + web.add_request(web.REQUEST_STARTED, path, { + 'id': download_id} + ) + + dirname = os.path.dirname(web.download_filename) + basename = os.path.basename(web.download_filename) + + def generate(): + # The user hasn't canceled the download + web.client_cancel = False + + # Starting a new download + if not web.stay_open: + web.download_in_progress = True + + chunk_size = 102400 # 100kb + + fp = open(web.download_filename, 'rb') + web.done = False + canceled = False + while not web.done: + # The user has canceled the download, so stop serving the file + if web.client_cancel: + web.add_request(web.REQUEST_CANCELED, path, { + 'id': download_id + }) + break + + chunk = fp.read(chunk_size) + if chunk == b'': + web.done = True + else: + try: + yield chunk + + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / web.download_filesize) * 100 + + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + if not web.gui_mode or web.common.platform == 'Linux' or web.common.platform == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(web.common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() + + web.add_request(web.REQUEST_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) + web.done = False + except: + # looks like the download was canceled + web.done = True + canceled = True + + # tell the GUI the download has canceled + web.add_request(web.REQUEST_CANCELED, path, { + 'id': download_id + }) + + fp.close() + + if web.common.platform != 'Darwin': + sys.stdout.write("\n") + + # Download is finished + if not web.stay_open: + web.download_in_progress = False + + # Close the server, if necessary + if not web.stay_open and not canceled: + print(strings._("closing_automatically")) + web.running = False + try: + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + except: + pass + + r = Response(generate()) + r.headers.set('Content-Length', web.download_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + r = web.add_security_headers(r) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r class ZipWriter(object): diff --git a/onionshare/web/web.py b/onionshare/web/web.py index ff149f21..0a6e6964 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -1,6 +1,5 @@ import hmac import logging -import mimetypes import os import queue import socket @@ -10,17 +9,12 @@ from distutils.version import LooseVersion as Version from urllib.request import urlopen import flask -from flask import ( - Flask, Response, request, render_template, abort, make_response, - flash, redirect, __version__ as flask_version -) -from werkzeug.utils import secure_filename +from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version from .. import strings -from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable -from .share_mode import ZipWriter -from .receive_mode import ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest +from .share_mode import share_routes, ZipWriter +from .receive_mode import receive_routes, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest # Stub out flask's show_server_banner function, to avoiding showing warnings that @@ -124,331 +118,9 @@ class Web(object): # Define the ewb app routes self.common_routes() if self.receive_mode: - self.receive_routes() + receive_routes(self) else: - self.send_routes() - - def send_routes(self): - """ - The web app routes for sharing files - """ - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - def index_logic(slug_candidate=''): - """ - Render the template for the onionshare landing page. - """ - self.add_request(Web.REQUEST_LOAD, request.path) - - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # If download is allowed to continue, serve download page - if self.slug: - r = make_response(render_template( - 'send.html', - slug=self.slug, - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - else: - # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - return self.add_security_headers(r) - - @self.app.route("//download") - def download(slug_candidate): - self.check_slug_candidate(slug_candidate) - return download_logic() - - @self.app.route("/download") - def download_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return download_logic() - - def download_logic(slug_candidate=''): - """ - Download the zip file. - """ - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # Each download has a unique id - download_id = self.download_count - self.download_count += 1 - - # Prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path - - # Tell GUI the download started - self.add_request(Web.REQUEST_STARTED, path, { - 'id': download_id} - ) - - dirname = os.path.dirname(self.download_filename) - basename = os.path.basename(self.download_filename) - - def generate(): - # The user hasn't canceled the download - self.client_cancel = False - - # Starting a new download - if not self.stay_open: - self.download_in_progress = True - - chunk_size = 102400 # 100kb - - fp = open(self.download_filename, 'rb') - self.done = False - canceled = False - while not self.done: - # The user has canceled the download, so stop serving the file - if self.client_cancel: - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - self.done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - self.add_request(Web.REQUEST_PROGRESS, path, { - 'id': download_id, - 'bytes': downloaded_bytes - }) - self.done = False - except: - # looks like the download was canceled - self.done = True - canceled = True - - # tell the GUI the download has canceled - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - - fp.close() - - if self.common.platform != 'Darwin': - sys.stdout.write("\n") - - # Download is finished - if not self.stay_open: - self.download_in_progress = False - - # Close the server, if necessary - if not self.stay_open and not canceled: - print(strings._("closing_automatically")) - self.running = False - try: - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - except: - pass - - r = Response(generate()) - r.headers.set('Content-Length', self.download_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - r = self.add_security_headers(r) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r - - def receive_routes(self): - """ - The web app routes for receiving files - """ - def index_logic(): - self.add_request(Web.REQUEST_LOAD, request.path) - - if self.common.settings.get('public_mode'): - upload_action = '/upload' - close_action = '/close' - else: - upload_action = '/{}/upload'.format(self.slug) - close_action = '/{}/close'.format(self.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action, - close_action=close_action, - receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) - return self.add_security_headers(r) - - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - - def upload_logic(slug_candidate=''): - """ - Upload files. - """ - # Make sure downloads_dir exists - valid = True - try: - self.common.validate_downloads_dir() - except DownloadsDirErrorCannotCreate: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) - valid = False - except DownloadsDirErrorNotWritable: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - files = request.files.getlist('file[]') - filenames = [] - print('') - for f in files: - if f.filename != '': - # Automatically rename the file, if a file of the same name already exists - filename = secure_filename(f.filename) - filenames.append(filename) - local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) - if os.path.exists(local_path): - if '.' in filename: - # Add "-i", e.g. change "foo.txt" to "foo-2.txt" - parts = filename.split('.') - name = parts[:-1] - ext = parts[-1] - - i = 2 - valid = False - while not valid: - new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - else: - # If no extension, just add "-i", e.g. change "foo" to "foo-2" - i = 2 - valid = False - while not valid: - new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - - basename = os.path.basename(local_path) - if f.filename != basename: - # Tell the GUI that the file has changed names - self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { - 'id': request.upload_id, - 'old_filename': f.filename, - 'new_filename': basename - }) - - self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print(strings._('receive_mode_received_file').format(local_path)) - f.save(local_path) - - # Note that flash strings are on English, and not translated, on purpose, - # to avoid leaking the locale of the OnionShare user - if len(filenames) == 0: - flash('No files uploaded', 'info') - else: - for filename in filenames: - flash('Sent {}'.format(filename), 'info') - - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - self.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @self.app.route("/upload", methods=['POST']) - def upload_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return upload_logic() - - - def close_logic(slug_candidate=''): - if self.common.settings.get('receive_allow_receiver_shutdown'): - self.force_shutdown() - r = make_response(render_template('closed.html')) - self.add_request(Web.REQUEST_CLOSE_SERVER, request.path) - return self.add_security_headers(r) - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//close", methods=['POST']) - def close(slug_candidate): - self.check_slug_candidate(slug_candidate) - return close_logic(slug_candidate) - - @self.app.route("/close", methods=['POST']) - def close_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return close_logic() + share_routes(self) def common_routes(self): """