mirror of
https://github.com/onionshare/onionshare.git
synced 2024-10-01 01:35:40 -04:00
518 lines
18 KiB
Python
518 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
OnionShare | https://onionshare.org/
|
|
|
|
Copyright (C) 2014-2021 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
|
|
import requests
|
|
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,
|
|
title=self.web.settings.get("general", "title"),
|
|
)
|
|
)
|
|
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}")
|
|
|
|
# Send webhook if configured
|
|
if (
|
|
self.web.settings.get("receive", "webhook_url")
|
|
and not request.upload_error
|
|
and len(files) > 0
|
|
):
|
|
if len(files) == 1:
|
|
file_msg = "1 file"
|
|
else:
|
|
file_msg = f"{len(files)} files"
|
|
self.send_webhook_notification(f"{file_msg} uploaded to OnionShare")
|
|
|
|
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,
|
|
title=self.web.settings.get("general", "title"),
|
|
)
|
|
}
|
|
)
|
|
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,
|
|
title=self.web.settings.get("general", "title"),
|
|
)
|
|
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)
|
|
|
|
def send_webhook_notification(self, data):
|
|
self.common.log("ReceiveModeWeb", "send_webhook_notification", data)
|
|
try:
|
|
requests.post(
|
|
self.web.settings.get("receive", "webhook_url"),
|
|
data=data,
|
|
timeout=5,
|
|
proxies=self.web.proxies,
|
|
)
|
|
except Exception as e:
|
|
print(f"Webhook notification failed: {e}")
|
|
|
|
|
|
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
|