From 2d43588a3bfd6afee8bcc4239a3259870bfc504b Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 19 Apr 2019 14:25:42 +0200 Subject: [PATCH] Add website sharing and directory listing cli-only --- install/check_lacked_trans.py | 1 + install/requirements.txt | 1 + onionshare/__init__.py | 15 ++++ onionshare/web/web.py | 6 +- onionshare/web/website_mode.py | 154 +++++++++++++++++++++++++++++++++ share/templates/listing.html | 40 +++++++++ 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 onionshare/web/website_mode.py create mode 100644 share/templates/listing.html diff --git a/install/check_lacked_trans.py b/install/check_lacked_trans.py index 010cdb7a..5ccce923 100755 --- a/install/check_lacked_trans.py +++ b/install/check_lacked_trans.py @@ -59,6 +59,7 @@ def main(): files_in(dir, 'onionshare_gui/mode') + \ files_in(dir, 'onionshare_gui/mode/share_mode') + \ files_in(dir, 'onionshare_gui/mode/receive_mode') + \ + files_in(dir, 'onionshare_gui/mode/website_mode') + \ files_in(dir, 'install/scripts') + \ files_in(dir, 'tests') pysrc = [p for p in src if p.endswith('.py')] diff --git a/install/requirements.txt b/install/requirements.txt index 0abd773f..fff0b009 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -3,6 +3,7 @@ certifi==2019.3.9 chardet==3.0.4 Click==7.0 Flask==1.0.2 +Flask-HTTPAuth future==0.17.1 idna==2.8 itsdangerous==1.1.0 diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 620ada98..dad092ed 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -51,6 +51,7 @@ def main(cwd=None): parser.add_argument('--connect-timeout', metavar='', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)") parser.add_argument('--stealth', action='store_true', dest='stealth', help="Use client authorization (advanced)") parser.add_argument('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them") + parser.add_argument('--website', action='store_true', dest='website', help=strings._("help_website")) parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)") parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk") parser.add_argument('filename', metavar='filename', nargs='*', help="List of files or folders to share") @@ -68,10 +69,13 @@ def main(cwd=None): connect_timeout = int(args.connect_timeout) stealth = bool(args.stealth) receive = bool(args.receive) + website = bool(args.website) config = args.config if receive: mode = 'receive' + elif website: + mode = 'website' else: mode = 'share' @@ -168,6 +172,15 @@ def main(cwd=None): print(e.args[0]) sys.exit() + if mode == 'website': + # Prepare files to share + print(strings._("preparing_website")) + try: + web.website_mode.set_file_info(filenames) + except OSError as e: + print(e.strerror) + sys.exit(1) + if mode == 'share': # Prepare files to share print("Compressing files.") @@ -206,6 +219,8 @@ def main(cwd=None): # Build the URL if common.settings.get('public_mode'): url = 'http://{0:s}'.format(app.onion_host) + elif mode == 'website': + url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) else: url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index edaf75f1..0ba8c6b3 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -15,7 +15,7 @@ from .. import strings from .share_mode import ShareModeWeb from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest - +from .website_mode import WebsiteModeWeb # Stub out flask's show_server_banner function, to avoiding showing warnings that # are not applicable to OnionShare @@ -111,13 +111,15 @@ class Web(object): self.receive_mode = None if self.mode == 'receive': self.receive_mode = ReceiveModeWeb(self.common, self) + elif self.mode == 'website': + self.website_mode = WebsiteModeWeb(self.common, self) elif self.mode == 'share': self.share_mode = ShareModeWeb(self.common, self) def define_common_routes(self): """ - Common web app routes between sending and receiving + Common web app routes between sending, receiving and website modes. """ @self.app.errorhandler(404) def page_not_found(e): diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py new file mode 100644 index 00000000..7b8429ae --- /dev/null +++ b/onionshare/web/website_mode.py @@ -0,0 +1,154 @@ +import os +import sys +import tempfile +import mimetypes +from flask import Response, request, render_template, make_response, send_from_directory +from flask_httpauth import HTTPBasicAuth + +from .. import strings + + +class WebsiteModeWeb(object): + """ + All of the web logic for share mode + """ + def __init__(self, common, web): + self.common = common + self.common.log('WebsiteModeWeb', '__init__') + + self.web = web + self.auth = HTTPBasicAuth() + + # Information about the file to be shared + self.file_info = [] + self.website_folder = '' + self.download_filesize = 0 + self.visit_count = 0 + + self.users = { } + + self.define_routes() + + def define_routes(self): + """ + The web app routes for sharing a website + """ + + @self.auth.get_password + def get_pw(username): + self.users['onionshare'] = self.web.slug + + if self.common.settings.get('public_mode'): + return True # let the request through, no questions asked! + elif username in self.users: + return self.users.get(username) + else: + return None + + @self.web.app.route('/download/') + @self.auth.login_required + def path_download(page_path): + return path_download(page_path) + + @self.web.app.route('/') + @self.auth.login_required + def path_public(page_path): + return path_logic(page_path) + + @self.web.app.route("/") + @self.auth.login_required + def index_public(): + return path_logic('') + + def path_download(file_path=''): + """ + Render the download links. + """ + self.web.add_request(self.web.REQUEST_LOAD, request.path) + if not os.path.isfile(os.path.join(self.website_folder, file_path)): + return self.web.error404() + + return send_from_directory(self.website_folder, file_path) + + def path_logic(page_path=''): + """ + Render the onionshare website. + """ + + self.web.add_request(self.web.REQUEST_LOAD, request.path) + + if self.file_info['files']: + self.website_folder = os.path.dirname(self.file_info['files'][0]['filename']) + elif self.file_info['dirs']: + self.website_folder = self.file_info['dirs'][0]['filename'] + else: + return self.web.error404() + + if any((fname == 'index.html') for fname in os.listdir(self.website_folder)): + self.web.app.static_url_path = self.website_folder + self.web.app.static_folder = self.website_folder + if not os.path.isfile(os.path.join(self.website_folder, page_path)): + page_path = os.path.join(page_path, 'index.html') + + return send_from_directory(self.website_folder, page_path) + + elif any(os.path.isfile(os.path.join(self.website_folder, i)) for i in os.listdir(self.website_folder)): + filenames = [] + for i in os.listdir(self.website_folder): + filenames.append(os.path.join(self.website_folder, i)) + + self.set_file_info(filenames) + + r = make_response(render_template( + 'listing.html', + file_info=self.file_info, + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize))) + + return self.web.add_security_headers(r) + + else: + return self.web.error404() + + + def set_file_info(self, filenames, processed_size_callback=None): + """ + Using the list of filenames being shared, fill in details that the web + page will need to display. This includes zipping up the file in order to + get the zip file's name and size. + """ + self.common.log("WebsiteModeWeb", "set_file_info") + self.web.cancel_compression = True + + self.cleanup_filenames = [] + + # build file info list + self.file_info = {'files': [], 'dirs': []} + for filename in filenames: + info = { + 'filename': filename, + 'basename': os.path.basename(filename.rstrip('/')) + } + if os.path.isfile(filename): + info['size'] = os.path.getsize(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) + self.file_info['files'].append(info) + if os.path.isdir(filename): + info['size'] = self.common.dir_size(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) + self.file_info['dirs'].append(info) + + self.download_filesize += info['size'] + + self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) + self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) + + # Check if there's only 1 file and no folders + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] + + self.download_filesize = os.path.getsize(self.download_filename) + + + return True diff --git a/share/templates/listing.html b/share/templates/listing.html new file mode 100644 index 00000000..a514e5d2 --- /dev/null +++ b/share/templates/listing.html @@ -0,0 +1,40 @@ + + + + OnionShare + + + + + +
+
+
    +
  • Total size: {{ filesize_human }}
  • +
+
+ +

OnionShare

+
+ + + + + + + + + {% for info in file_info.files %} + + + + + + {% endfor %} +
FilenameSize
+ + {{ info.basename }} + {{ info.size_human }}download
+ + +