diff options
Diffstat (limited to 'cli/onionshare_cli/web/receive_mode.py')
-rw-r--r-- | cli/onionshare_cli/web/receive_mode.py | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/cli/onionshare_cli/web/receive_mode.py b/cli/onionshare_cli/web/receive_mode.py new file mode 100644 index 00000000..a0da0d3c --- /dev/null +++ b/cli/onionshare_cli/web/receive_mode.py @@ -0,0 +1,488 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +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/>. +""" + +import os +import tempfile +import json +from datetime import datetime +from flask import Request, request, render_template, make_response, flash, redirect +from werkzeug.utils import secure_filename + + +class ReceiveModeWeb: + """ + All of the web logic for receive mode + """ + + def __init__(self, common, web): + self.common = common + self.common.log("ReceiveModeWeb", "__init__") + + self.web = web + + self.can_upload = True + self.uploads_in_progress = [] + + # This tracks the history id + self.cur_history_id = 0 + + self.define_routes() + + def define_routes(self): + """ + The web app routes for receiving files + """ + + @self.web.app.route("/") + def index(): + history_id = self.cur_history_id + self.cur_history_id += 1 + self.web.add_request( + self.web.REQUEST_INDIVIDUAL_FILE_STARTED, + request.path, + {"id": history_id, "status_code": 200}, + ) + + self.web.add_request(self.web.REQUEST_LOAD, request.path) + r = make_response( + render_template( + "receive.html", static_url_path=self.web.static_url_path + ) + ) + return self.web.add_security_headers(r) + + @self.web.app.route("/upload", methods=["POST"]) + def upload(ajax=False): + """ + Handle the upload files POST request, though at this point, the files have + already been uploaded and saved to their correct locations. + """ + files = request.files.getlist("file[]") + filenames = [] + for f in files: + if f.filename != "": + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(request.receive_mode_dir, filename) + basename = os.path.basename(local_path) + + # Tell the GUI the receive mode directory for this file + self.web.add_request( + self.web.REQUEST_UPLOAD_SET_DIR, + request.path, + { + "id": request.history_id, + "filename": basename, + "dir": request.receive_mode_dir, + }, + ) + + self.common.log( + "ReceiveModeWeb", + "define_routes", + f"/upload, uploaded {f.filename}, saving to {local_path}", + ) + print(f"\nReceived: {local_path}") + + if request.upload_error: + self.common.log( + "ReceiveModeWeb", + "define_routes", + "/upload, there was an upload error", + ) + + self.web.add_request( + self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, + request.path, + {"receive_mode_dir": request.receive_mode_dir}, + ) + print( + f"Could not create OnionShare data folder: {request.receive_mode_dir}" + ) + + msg = "Error uploading, please inform the OnionShare user" + if ajax: + return json.dumps({"error_flashes": [msg]}) + else: + flash(msg, "error") + return redirect("/") + + if ajax: + info_flashes = [] + + if len(filenames) == 0: + msg = "No files uploaded" + if ajax: + info_flashes.append(msg) + else: + flash(msg, "info") + else: + msg = "Sent " + for filename in filenames: + msg += f"{filename}, " + msg = msg.rstrip(", ") + if ajax: + info_flashes.append(msg) + else: + flash(msg, "info") + + if self.can_upload: + if ajax: + return json.dumps({"info_flashes": info_flashes}) + else: + return redirect("/") + else: + if ajax: + return json.dumps( + { + "new_body": render_template( + "thankyou.html", + static_url_path=self.web.static_url_path, + ) + } + ) + else: + # It was the last upload and the timer ran out + r = make_response( + render_template("thankyou.html"), + static_url_path=self.web.static_url_path, + ) + return self.web.add_security_headers(r) + + @self.web.app.route("/upload-ajax", methods=["POST"]) + def upload_ajax_public(): + if not self.can_upload: + return self.web.error403() + return upload(ajax=True) + + +class ReceiveModeWSGIMiddleware(object): + """ + Custom WSGI middleware in order to attach the Web object to environ, so + ReceiveModeRequest can access it. + """ + + def __init__(self, app, web): + self.app = app + self.web = web + + def __call__(self, environ, start_response): + environ["web"] = self.web + environ["stop_q"] = self.web.stop_q + return self.app(environ, start_response) + + +class ReceiveModeFile(object): + """ + A custom file object that tells ReceiveModeRequest every time data gets + written to it, in order to track the progress of uploads. It starts out with + a .part file extension, and when it's complete it removes that extension. + """ + + def __init__(self, request, filename, write_func, close_func): + self.onionshare_request = request + self.onionshare_filename = filename + self.onionshare_write_func = write_func + self.onionshare_close_func = close_func + + self.filename = os.path.join(self.onionshare_request.receive_mode_dir, filename) + self.filename_in_progress = f"{self.filename}.part" + + # Open the file + self.upload_error = False + try: + self.f = open(self.filename_in_progress, "wb+") + except: + # This will only happen if someone is messing with the data dir while + # OnionShare is running, but if it does make sure to throw an error + self.upload_error = True + self.f = tempfile.TemporaryFile("wb+") + + # Make all the file-like methods and attributes actually access the + # TemporaryFile, except for write + attrs = [ + "closed", + "detach", + "fileno", + "flush", + "isatty", + "mode", + "name", + "peek", + "raw", + "read", + "read1", + "readable", + "readinto", + "readinto1", + "readline", + "readlines", + "seek", + "seekable", + "tell", + "truncate", + "writable", + "writelines", + ] + for attr in attrs: + setattr(self, attr, getattr(self.f, attr)) + + def write(self, b): + """ + Custom write method that calls out to onionshare_write_func + """ + if self.upload_error or (not self.onionshare_request.stop_q.empty()): + self.close() + self.onionshare_request.close() + return + + try: + bytes_written = self.f.write(b) + self.onionshare_write_func(self.onionshare_filename, bytes_written) + + except: + self.upload_error = True + + def close(self): + """ + Custom close method that calls out to onionshare_close_func + """ + try: + self.f.close() + + if not self.upload_error: + # Rename the in progress file to the final filename + os.rename(self.filename_in_progress, self.filename) + + except: + self.upload_error = True + + self.onionshare_close_func(self.onionshare_filename, self.upload_error) + + +class ReceiveModeRequest(Request): + """ + A custom flask Request object that keeps track of how much data has been + uploaded for each file, for receive mode. + """ + + def __init__(self, environ, populate_request=True, shallow=False): + super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) + self.web = environ["web"] + self.stop_q = environ["stop_q"] + + self.web.common.log("ReceiveModeRequest", "__init__") + + # Prevent running the close() method more than once + self.closed = False + + # Is this a valid upload request? + self.upload_request = False + if self.method == "POST": + if self.path == "/upload" or self.path == "/upload-ajax": + self.upload_request = True + + if self.upload_request: + # No errors yet + self.upload_error = False + + # Figure out what files should be saved + now = datetime.now() + date_dir = now.strftime("%Y-%m-%d") + time_dir = now.strftime("%H.%M.%S") + self.receive_mode_dir = os.path.join( + self.web.settings.get("receive", "data_dir"), date_dir, time_dir + ) + + # Create that directory, which shouldn't exist yet + try: + os.makedirs(self.receive_mode_dir, 0o700, exist_ok=False) + except OSError: + # If this directory already exists, maybe someone else is uploading files at + # the same second, so use a different name in that case + if os.path.exists(self.receive_mode_dir): + # Keep going until we find a directory name that's available + i = 1 + while True: + new_receive_mode_dir = f"{self.receive_mode_dir}-{i}" + try: + os.makedirs(new_receive_mode_dir, 0o700, exist_ok=False) + self.receive_mode_dir = new_receive_mode_dir + break + except OSError: + pass + i += 1 + # Failsafe + if i == 100: + self.web.common.log( + "ReceiveModeRequest", + "__init__", + "Error finding available receive mode directory", + ) + self.upload_error = True + break + except PermissionError: + self.web.add_request( + self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, + request.path, + {"receive_mode_dir": self.receive_mode_dir}, + ) + print( + f"Could not create OnionShare data folder: {self.receive_mode_dir}" + ) + self.web.common.log( + "ReceiveModeRequest", + "__init__", + "Permission denied creating receive mode directory", + ) + self.upload_error = True + + # If there's an error so far, finish early + if self.upload_error: + return + + # A dictionary that maps filenames to the bytes uploaded so far + self.progress = {} + + # Prevent new uploads if we've said so (timer expired) + if self.web.receive_mode.can_upload: + + # Create an history_id, attach it to the request + self.history_id = self.web.receive_mode.cur_history_id + self.web.receive_mode.cur_history_id += 1 + + # Figure out the content length + try: + self.content_length = int(self.headers["Content-Length"]) + except: + self.content_length = 0 + + date_str = datetime.now().strftime("%b %d, %I:%M%p") + size_str = self.web.common.human_readable_filesize(self.content_length) + print(f"{date_str}: Upload of total size {size_str} is starting") + + # Don't tell the GUI that a request has started until we start receiving files + self.told_gui_about_request = False + + self.previous_file = None + + def _get_file_stream( + self, total_content_length, content_type, filename=None, content_length=None + ): + """ + This gets called for each file that gets uploaded, and returns an file-like + writable stream. + """ + if self.upload_request: + if not self.told_gui_about_request: + # Tell the GUI about the request + self.web.add_request( + self.web.REQUEST_STARTED, + self.path, + {"id": self.history_id, "content_length": self.content_length}, + ) + self.web.receive_mode.uploads_in_progress.append(self.history_id) + + self.told_gui_about_request = True + + self.filename = secure_filename(filename) + + self.progress[self.filename] = {"uploaded_bytes": 0, "complete": False} + + f = ReceiveModeFile( + self, self.filename, self.file_write_func, self.file_close_func + ) + if f.upload_error: + self.web.common.log( + "ReceiveModeRequest", "_get_file_stream", "Error creating file" + ) + self.upload_error = True + return f + + def close(self): + """ + Closing the request. + """ + super(ReceiveModeRequest, self).close() + + # Prevent calling this method more than once per request + if self.closed: + return + self.closed = True + + self.web.common.log("ReceiveModeRequest", "close") + + try: + if self.told_gui_about_request: + history_id = self.history_id + + if ( + not self.web.stop_q.empty() + or not self.progress[self.filename]["complete"] + ): + # Inform the GUI that the upload has canceled + self.web.add_request( + self.web.REQUEST_UPLOAD_CANCELED, self.path, {"id": history_id} + ) + else: + # Inform the GUI that the upload has finished + self.web.add_request( + self.web.REQUEST_UPLOAD_FINISHED, self.path, {"id": history_id} + ) + self.web.receive_mode.uploads_in_progress.remove(history_id) + + except AttributeError: + pass + + def file_write_func(self, filename, length): + """ + This function gets called when a specific file is written to. + """ + if self.closed: + return + + if self.upload_request: + self.progress[filename]["uploaded_bytes"] += length + + if self.previous_file != filename: + self.previous_file = filename + + size_str = self.web.common.human_readable_filesize( + self.progress[filename]["uploaded_bytes"] + ) + print(f"\r=> {size_str} {filename} ", end="") + + # Update the GUI on the upload progress + if self.told_gui_about_request: + self.web.add_request( + self.web.REQUEST_PROGRESS, + self.path, + {"id": self.history_id, "progress": self.progress}, + ) + + def file_close_func(self, filename, upload_error=False): + """ + This function gets called when a specific file is closed. + """ + self.progress[filename]["complete"] = True + + # If the file tells us there was an upload error, let the request know as well + if upload_error: + self.upload_error = True |