mirror of
https://github.com/onionshare/onionshare.git
synced 2025-09-26 11:01:03 -04:00
Add onionshare CLI to cli folder, move GUI to desktop folder, and start refactoring it to work with briefcase
This commit is contained in:
parent
b81a55f546
commit
f4abcf1be9
583 changed files with 14871 additions and 474 deletions
21
cli/onionshare_cli/web/__init__.py
Normal file
21
cli/onionshare_cli/web/__init__.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- 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/>.
|
||||
"""
|
||||
|
||||
from .web import Web
|
159
cli/onionshare_cli/web/chat_mode.py
Normal file
159
cli/onionshare_cli/web/chat_mode.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
# -*- 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/>.
|
||||
"""
|
||||
|
||||
from flask import (
|
||||
Request,
|
||||
request,
|
||||
render_template,
|
||||
make_response,
|
||||
jsonify,
|
||||
redirect,
|
||||
session,
|
||||
)
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask_socketio import emit, join_room, leave_room
|
||||
|
||||
|
||||
class ChatModeWeb:
|
||||
"""
|
||||
All of the web logic for chat mode
|
||||
"""
|
||||
|
||||
def __init__(self, common, web):
|
||||
self.common = common
|
||||
self.common.log("ChatModeWeb", "__init__")
|
||||
|
||||
self.web = web
|
||||
|
||||
# This tracks users in the room
|
||||
self.connected_users = []
|
||||
|
||||
# This tracks the history id
|
||||
self.cur_history_id = 0
|
||||
|
||||
self.define_routes()
|
||||
|
||||
def define_routes(self):
|
||||
"""
|
||||
The web app routes for chatting
|
||||
"""
|
||||
|
||||
@self.web.app.route("/")
|
||||
def index():
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
session["name"] = (
|
||||
session.get("name")
|
||||
if session.get("name")
|
||||
else self.common.build_username()
|
||||
)
|
||||
session["room"] = self.web.settings.default_settings["chat"]["room"]
|
||||
self.web.add_request(
|
||||
request.path, {"id": history_id, "status_code": 200},
|
||||
)
|
||||
|
||||
self.web.add_request(self.web.REQUEST_LOAD, request.path)
|
||||
r = make_response(
|
||||
render_template(
|
||||
"chat.html",
|
||||
static_url_path=self.web.static_url_path,
|
||||
username=session.get("name"),
|
||||
)
|
||||
)
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
@self.web.app.route("/update-session-username", methods=["POST"])
|
||||
def update_session_username():
|
||||
history_id = self.cur_history_id
|
||||
data = request.get_json()
|
||||
if data.get("username", "") not in self.connected_users:
|
||||
session["name"] = data.get("username", session.get("name"))
|
||||
self.web.add_request(
|
||||
request.path, {"id": history_id, "status_code": 200},
|
||||
)
|
||||
|
||||
self.web.add_request(self.web.REQUEST_LOAD, request.path)
|
||||
r = make_response(jsonify(username=session.get("name"), success=True,))
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
@self.web.socketio.on("joined", namespace="/chat")
|
||||
def joined(message):
|
||||
"""Sent by clients when they enter a room.
|
||||
A status message is broadcast to all people in the room."""
|
||||
self.connected_users.append(session.get("name"))
|
||||
join_room(session.get("room"))
|
||||
emit(
|
||||
"status",
|
||||
{
|
||||
"username": session.get("name"),
|
||||
"msg": "{} has joined.".format(session.get("name")),
|
||||
"connected_users": self.connected_users,
|
||||
"user": session.get("name"),
|
||||
},
|
||||
room=session.get("room"),
|
||||
)
|
||||
|
||||
@self.web.socketio.on("text", namespace="/chat")
|
||||
def text(message):
|
||||
"""Sent by a client when the user entered a new message.
|
||||
The message is sent to all people in the room."""
|
||||
emit(
|
||||
"message",
|
||||
{"username": session.get("name"), "msg": message["msg"]},
|
||||
room=session.get("room"),
|
||||
)
|
||||
|
||||
@self.web.socketio.on("update_username", namespace="/chat")
|
||||
def update_username(message):
|
||||
"""Sent by a client when the user updates their username.
|
||||
The message is sent to all people in the room."""
|
||||
current_name = session.get("name")
|
||||
if message["username"] not in self.connected_users:
|
||||
session["name"] = message["username"]
|
||||
self.connected_users[
|
||||
self.connected_users.index(current_name)
|
||||
] = session.get("name")
|
||||
emit(
|
||||
"status",
|
||||
{
|
||||
"msg": "{} has updated their username to: {}".format(
|
||||
current_name, session.get("name")
|
||||
),
|
||||
"connected_users": self.connected_users,
|
||||
"old_name": current_name,
|
||||
"new_name": session.get("name"),
|
||||
},
|
||||
room=session.get("room"),
|
||||
)
|
||||
|
||||
@self.web.socketio.on("disconnect", namespace="/chat")
|
||||
def disconnect():
|
||||
"""Sent by clients when they disconnect from a room.
|
||||
A status message is broadcast to all people in the room."""
|
||||
self.connected_users.remove(session.get("name"))
|
||||
leave_room(session.get("room"))
|
||||
emit(
|
||||
"status",
|
||||
{
|
||||
"msg": "{} has left the room.".format(session.get("name")),
|
||||
"connected_users": self.connected_users,
|
||||
},
|
||||
room=session.get("room"),
|
||||
)
|
488
cli/onionshare_cli/web/receive_mode.py
Normal file
488
cli/onionshare_cli/web/receive_mode.py
Normal file
|
@ -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
|
321
cli/onionshare_cli/web/send_base_mode.py
Normal file
321
cli/onionshare_cli/web/send_base_mode.py
Normal file
|
@ -0,0 +1,321 @@
|
|||
# -*- 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 sys
|
||||
import tempfile
|
||||
import mimetypes
|
||||
import gzip
|
||||
from flask import Response, request, render_template, make_response
|
||||
|
||||
|
||||
class SendBaseModeWeb:
|
||||
"""
|
||||
All of the web logic shared between share and website mode (modes where the user sends files)
|
||||
"""
|
||||
|
||||
def __init__(self, common, web):
|
||||
super(SendBaseModeWeb, self).__init__()
|
||||
self.common = common
|
||||
self.web = web
|
||||
|
||||
# Information about the file to be shared
|
||||
self.is_zipped = False
|
||||
self.download_filename = None
|
||||
self.download_filesize = None
|
||||
self.gzip_filename = None
|
||||
self.gzip_filesize = None
|
||||
self.zip_writer = None
|
||||
|
||||
# If autostop_sharing, only allow one download at a time
|
||||
self.download_in_progress = False
|
||||
|
||||
# This tracks the history id
|
||||
self.cur_history_id = 0
|
||||
|
||||
self.define_routes()
|
||||
self.init()
|
||||
|
||||
def set_file_info(self, filenames, processed_size_callback=None):
|
||||
"""
|
||||
Build a data structure that describes the list of files
|
||||
"""
|
||||
# If there's just one folder, replace filenames with a list of files inside that folder
|
||||
if len(filenames) == 1 and os.path.isdir(filenames[0]):
|
||||
filenames = [
|
||||
os.path.join(filenames[0], x) for x in os.listdir(filenames[0])
|
||||
]
|
||||
|
||||
# Re-initialize
|
||||
self.files = {} # Dictionary mapping file paths to filenames on disk
|
||||
self.root_files = (
|
||||
{}
|
||||
) # This is only the root files and dirs, as opposed to all of them
|
||||
self.cleanup_filenames = []
|
||||
self.cur_history_id = 0
|
||||
self.file_info = {"files": [], "dirs": []}
|
||||
self.gzip_individual_files = {}
|
||||
self.init()
|
||||
|
||||
# Build the file list
|
||||
for filename in filenames:
|
||||
basename = os.path.basename(filename.rstrip("/"))
|
||||
|
||||
# If it's a filename, add it
|
||||
if os.path.isfile(filename):
|
||||
self.files[basename] = filename
|
||||
self.root_files[basename] = filename
|
||||
|
||||
# If it's a directory, add it recursively
|
||||
elif os.path.isdir(filename):
|
||||
self.root_files[basename + "/"] = filename
|
||||
|
||||
for root, _, nested_filenames in os.walk(filename):
|
||||
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
|
||||
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
|
||||
# The normalized_root should be "some_folder/foobar"
|
||||
normalized_root = os.path.join(
|
||||
basename, root[len(filename) :].lstrip("/")
|
||||
).rstrip("/")
|
||||
|
||||
# Add the dir itself
|
||||
self.files[normalized_root + "/"] = root
|
||||
|
||||
# Add the files in this dir
|
||||
for nested_filename in nested_filenames:
|
||||
self.files[
|
||||
os.path.join(normalized_root, nested_filename)
|
||||
] = os.path.join(root, nested_filename)
|
||||
|
||||
self.set_file_info_custom(filenames, processed_size_callback)
|
||||
|
||||
def directory_listing(self, filenames, path="", filesystem_path=None):
|
||||
# Tell the GUI about the directory listing
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
self.web.add_request(
|
||||
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
|
||||
f"/{path}",
|
||||
{"id": history_id, "method": request.method, "status_code": 200},
|
||||
)
|
||||
|
||||
breadcrumbs = [("☗", "/")]
|
||||
parts = path.split("/")[:-1]
|
||||
for i in range(len(parts)):
|
||||
breadcrumbs.append((parts[i], f"/{'/'.join(parts[0 : i + 1])}/"))
|
||||
breadcrumbs_leaf = breadcrumbs.pop()[0]
|
||||
|
||||
# If filesystem_path is None, this is the root directory listing
|
||||
files, dirs = self.build_directory_listing(filenames, filesystem_path)
|
||||
r = self.directory_listing_template(
|
||||
path, files, dirs, breadcrumbs, breadcrumbs_leaf
|
||||
)
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
def build_directory_listing(self, filenames, filesystem_path):
|
||||
files = []
|
||||
dirs = []
|
||||
|
||||
for filename in filenames:
|
||||
if filesystem_path:
|
||||
this_filesystem_path = os.path.join(filesystem_path, filename)
|
||||
else:
|
||||
this_filesystem_path = self.files[filename]
|
||||
|
||||
is_dir = os.path.isdir(this_filesystem_path)
|
||||
|
||||
if is_dir:
|
||||
dirs.append({"basename": filename})
|
||||
else:
|
||||
size = os.path.getsize(this_filesystem_path)
|
||||
size_human = self.common.human_readable_filesize(size)
|
||||
files.append({"basename": filename, "size_human": size_human})
|
||||
return files, dirs
|
||||
|
||||
def stream_individual_file(self, filesystem_path):
|
||||
"""
|
||||
Return a flask response that's streaming the download of an individual file, and gzip
|
||||
compressing it if the browser supports it.
|
||||
"""
|
||||
use_gzip = self.should_use_gzip()
|
||||
|
||||
# gzip compress the individual file, if it hasn't already been compressed
|
||||
if use_gzip:
|
||||
if filesystem_path not in self.gzip_individual_files:
|
||||
gzip_filename = tempfile.mkstemp("wb+")[1]
|
||||
self._gzip_compress(filesystem_path, gzip_filename, 6, None)
|
||||
self.gzip_individual_files[filesystem_path] = gzip_filename
|
||||
|
||||
# Make sure the gzip file gets cleaned up when onionshare stops
|
||||
self.cleanup_filenames.append(gzip_filename)
|
||||
|
||||
file_to_download = self.gzip_individual_files[filesystem_path]
|
||||
filesize = os.path.getsize(self.gzip_individual_files[filesystem_path])
|
||||
else:
|
||||
file_to_download = filesystem_path
|
||||
filesize = os.path.getsize(filesystem_path)
|
||||
|
||||
path = request.path
|
||||
|
||||
# Tell GUI the individual file started
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
|
||||
# Only GET requests are allowed, any other method should fail
|
||||
if request.method != "GET":
|
||||
return self.web.error405(history_id)
|
||||
|
||||
self.web.add_request(
|
||||
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
|
||||
path,
|
||||
{"id": history_id, "filesize": filesize},
|
||||
)
|
||||
|
||||
def generate():
|
||||
chunk_size = 102400 # 100kb
|
||||
|
||||
fp = open(file_to_download, "rb")
|
||||
done = False
|
||||
while not done:
|
||||
chunk = fp.read(chunk_size)
|
||||
if chunk == b"":
|
||||
done = True
|
||||
else:
|
||||
try:
|
||||
yield chunk
|
||||
|
||||
# Tell GUI the progress
|
||||
downloaded_bytes = fp.tell()
|
||||
percent = (1.0 * downloaded_bytes / filesize) * 100
|
||||
if (
|
||||
not self.web.is_gui
|
||||
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.web.add_request(
|
||||
self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS,
|
||||
path,
|
||||
{
|
||||
"id": history_id,
|
||||
"bytes": downloaded_bytes,
|
||||
"filesize": filesize,
|
||||
},
|
||||
)
|
||||
done = False
|
||||
except:
|
||||
# Looks like the download was canceled
|
||||
done = True
|
||||
|
||||
# Tell the GUI the individual file was canceled
|
||||
self.web.add_request(
|
||||
self.web.REQUEST_INDIVIDUAL_FILE_CANCELED,
|
||||
path,
|
||||
{"id": history_id},
|
||||
)
|
||||
|
||||
fp.close()
|
||||
|
||||
if self.common.platform != "Darwin":
|
||||
sys.stdout.write("\n")
|
||||
|
||||
basename = os.path.basename(filesystem_path)
|
||||
|
||||
r = Response(generate())
|
||||
if use_gzip:
|
||||
r.headers.set("Content-Encoding", "gzip")
|
||||
r.headers.set("Content-Length", filesize)
|
||||
r.headers.set("Content-Disposition", "inline", filename=basename)
|
||||
r = self.web.add_security_headers(r)
|
||||
(content_type, _) = mimetypes.guess_type(basename, strict=False)
|
||||
if content_type is not None:
|
||||
r.headers.set("Content-Type", content_type)
|
||||
return r
|
||||
|
||||
def should_use_gzip(self):
|
||||
"""
|
||||
Should we use gzip for this browser?
|
||||
"""
|
||||
return (not self.is_zipped) and (
|
||||
"gzip" in request.headers.get("Accept-Encoding", "").lower()
|
||||
)
|
||||
|
||||
def _gzip_compress(
|
||||
self, input_filename, output_filename, level, processed_size_callback=None
|
||||
):
|
||||
"""
|
||||
Compress a file with gzip, without loading the whole thing into memory
|
||||
Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
|
||||
"""
|
||||
bytes_processed = 0
|
||||
blocksize = 1 << 16 # 64kB
|
||||
with open(input_filename, "rb") as input_file:
|
||||
output_file = gzip.open(output_filename, "wb", level)
|
||||
while True:
|
||||
if processed_size_callback is not None:
|
||||
processed_size_callback(bytes_processed)
|
||||
|
||||
block = input_file.read(blocksize)
|
||||
if len(block) == 0:
|
||||
break
|
||||
output_file.write(block)
|
||||
bytes_processed += blocksize
|
||||
|
||||
output_file.close()
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
Inherited class will implement this
|
||||
"""
|
||||
pass
|
||||
|
||||
def define_routes(self):
|
||||
"""
|
||||
Inherited class will implement this
|
||||
"""
|
||||
pass
|
||||
|
||||
def directory_listing_template(self):
|
||||
"""
|
||||
Inherited class will implement this. It should call render_template and return
|
||||
the response.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_file_info_custom(self, filenames, processed_size_callback):
|
||||
"""
|
||||
Inherited class will implement this.
|
||||
"""
|
||||
pass
|
||||
|
||||
def render_logic(self, path=""):
|
||||
"""
|
||||
Inherited class will implement this.
|
||||
"""
|
||||
pass
|
411
cli/onionshare_cli/web/share_mode.py
Normal file
411
cli/onionshare_cli/web/share_mode.py
Normal file
|
@ -0,0 +1,411 @@
|
|||
# -*- 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 sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
import mimetypes
|
||||
from flask import Response, request, render_template, make_response
|
||||
|
||||
from .send_base_mode import SendBaseModeWeb
|
||||
|
||||
|
||||
class ShareModeWeb(SendBaseModeWeb):
|
||||
"""
|
||||
All of the web logic for share mode
|
||||
"""
|
||||
|
||||
def init(self):
|
||||
self.common.log("ShareModeWeb", "init")
|
||||
|
||||
# Allow downloading individual files if "Stop sharing after files have been sent" is unchecked
|
||||
self.download_individual_files = not self.web.settings.get(
|
||||
"share", "autostop_sharing"
|
||||
)
|
||||
|
||||
def define_routes(self):
|
||||
"""
|
||||
The web app routes for sharing files
|
||||
"""
|
||||
|
||||
@self.web.app.route("/", defaults={"path": ""})
|
||||
@self.web.app.route("/<path:path>")
|
||||
def index(path):
|
||||
"""
|
||||
Render the template for the onionshare landing page.
|
||||
"""
|
||||
self.web.add_request(self.web.REQUEST_LOAD, request.path)
|
||||
|
||||
# Deny new downloads if "Stop sharing after files have been sent" is checked and there is
|
||||
# currently a download
|
||||
deny_download = (
|
||||
self.web.settings.get("share", "autostop_sharing")
|
||||
and self.download_in_progress
|
||||
)
|
||||
if deny_download:
|
||||
r = make_response(
|
||||
render_template("denied.html"),
|
||||
static_url_path=self.web.static_url_path,
|
||||
)
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
# If download is allowed to continue, serve download page
|
||||
if self.should_use_gzip():
|
||||
self.filesize = self.gzip_filesize
|
||||
else:
|
||||
self.filesize = self.download_filesize
|
||||
|
||||
return self.render_logic(path)
|
||||
|
||||
@self.web.app.route("/download")
|
||||
def download():
|
||||
"""
|
||||
Download the zip file.
|
||||
"""
|
||||
# Deny new downloads if "Stop After First Download" is checked and there is
|
||||
# currently a download
|
||||
deny_download = (
|
||||
self.web.settings.get("share", "autostop_sharing")
|
||||
and self.download_in_progress
|
||||
)
|
||||
if deny_download:
|
||||
r = make_response(
|
||||
render_template(
|
||||
"denied.html", static_url_path=self.web.static_url_path
|
||||
)
|
||||
)
|
||||
return self.web.add_security_headers(r)
|
||||
|
||||
# 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
|
||||
|
||||
# If this is a zipped file, then serve as-is. If it's not zipped, then,
|
||||
# if the http client supports gzip compression, gzip the file first
|
||||
# and serve that
|
||||
use_gzip = self.should_use_gzip()
|
||||
if use_gzip:
|
||||
file_to_download = self.gzip_filename
|
||||
self.filesize = self.gzip_filesize
|
||||
else:
|
||||
file_to_download = self.download_filename
|
||||
self.filesize = self.download_filesize
|
||||
|
||||
# Tell GUI the download started
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
self.web.add_request(
|
||||
self.web.REQUEST_STARTED, path, {"id": history_id, "use_gzip": use_gzip}
|
||||
)
|
||||
|
||||
basename = os.path.basename(self.download_filename)
|
||||
|
||||
def generate():
|
||||
# Starting a new download
|
||||
if self.web.settings.get("share", "autostop_sharing"):
|
||||
self.download_in_progress = True
|
||||
|
||||
chunk_size = 102400 # 100kb
|
||||
|
||||
fp = open(file_to_download, "rb")
|
||||
self.web.done = False
|
||||
canceled = False
|
||||
while not self.web.done:
|
||||
# The user has canceled the download, so stop serving the file
|
||||
if not self.web.stop_q.empty():
|
||||
self.web.add_request(
|
||||
self.web.REQUEST_CANCELED, path, {"id": history_id}
|
||||
)
|
||||
break
|
||||
|
||||
chunk = fp.read(chunk_size)
|
||||
if chunk == b"":
|
||||
self.web.done = True
|
||||
else:
|
||||
try:
|
||||
yield chunk
|
||||
|
||||
# tell GUI the progress
|
||||
downloaded_bytes = fp.tell()
|
||||
percent = (1.0 * downloaded_bytes / self.filesize) * 100
|
||||
|
||||
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
|
||||
if (
|
||||
not self.web.is_gui
|
||||
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.web.add_request(
|
||||
self.web.REQUEST_PROGRESS,
|
||||
path,
|
||||
{"id": history_id, "bytes": downloaded_bytes},
|
||||
)
|
||||
self.web.done = False
|
||||
except:
|
||||
# looks like the download was canceled
|
||||
self.web.done = True
|
||||
canceled = True
|
||||
|
||||
# tell the GUI the download has canceled
|
||||
self.web.add_request(
|
||||
self.web.REQUEST_CANCELED, path, {"id": history_id}
|
||||
)
|
||||
|
||||
fp.close()
|
||||
|
||||
if self.common.platform != "Darwin":
|
||||
sys.stdout.write("\n")
|
||||
|
||||
# Download is finished
|
||||
if self.web.settings.get("share", "autostop_sharing"):
|
||||
self.download_in_progress = False
|
||||
|
||||
# Close the server, if necessary
|
||||
if self.web.settings.get("share", "autostop_sharing") and not canceled:
|
||||
print("Stopped because transfer is complete")
|
||||
self.web.running = False
|
||||
try:
|
||||
if shutdown_func is None:
|
||||
raise RuntimeError("Not running with the Werkzeug Server")
|
||||
shutdown_func()
|
||||
except:
|
||||
pass
|
||||
|
||||
r = Response(generate())
|
||||
if use_gzip:
|
||||
r.headers.set("Content-Encoding", "gzip")
|
||||
r.headers.set("Content-Length", self.filesize)
|
||||
r.headers.set("Content-Disposition", "attachment", filename=basename)
|
||||
r = self.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
|
||||
|
||||
def directory_listing_template(
|
||||
self, path, files, dirs, breadcrumbs, breadcrumbs_leaf
|
||||
):
|
||||
return make_response(
|
||||
render_template(
|
||||
"send.html",
|
||||
file_info=self.file_info,
|
||||
files=files,
|
||||
dirs=dirs,
|
||||
breadcrumbs=breadcrumbs,
|
||||
breadcrumbs_leaf=breadcrumbs_leaf,
|
||||
filename=os.path.basename(self.download_filename),
|
||||
filesize=self.filesize,
|
||||
filesize_human=self.common.human_readable_filesize(
|
||||
self.download_filesize
|
||||
),
|
||||
is_zipped=self.is_zipped,
|
||||
static_url_path=self.web.static_url_path,
|
||||
download_individual_files=self.download_individual_files,
|
||||
)
|
||||
)
|
||||
|
||||
def set_file_info_custom(self, filenames, processed_size_callback):
|
||||
self.common.log("ShareModeWeb", "set_file_info_custom")
|
||||
self.web.cancel_compression = False
|
||||
self.build_zipfile_list(filenames, processed_size_callback)
|
||||
|
||||
def render_logic(self, path=""):
|
||||
if path in self.files:
|
||||
filesystem_path = self.files[path]
|
||||
|
||||
# If it's a directory
|
||||
if os.path.isdir(filesystem_path):
|
||||
# Render directory listing
|
||||
filenames = []
|
||||
for filename in os.listdir(filesystem_path):
|
||||
if os.path.isdir(os.path.join(filesystem_path, filename)):
|
||||
filenames.append(filename + "/")
|
||||
else:
|
||||
filenames.append(filename)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path, filesystem_path)
|
||||
|
||||
# If it's a file
|
||||
elif os.path.isfile(filesystem_path):
|
||||
if self.download_individual_files:
|
||||
return self.stream_individual_file(filesystem_path)
|
||||
else:
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
|
||||
# If it's not a directory or file, throw a 404
|
||||
else:
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
else:
|
||||
# Special case loading /
|
||||
|
||||
if path == "":
|
||||
# Root directory listing
|
||||
filenames = list(self.root_files)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path)
|
||||
|
||||
else:
|
||||
# If the path isn't found, throw a 404
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
|
||||
def build_zipfile_list(self, filenames, processed_size_callback=None):
|
||||
self.common.log("ShareModeWeb", "build_zipfile_list")
|
||||
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.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"]
|
||||
|
||||
# Compress the file with gzip now, so we don't have to do it on each request
|
||||
self.gzip_filename = tempfile.mkstemp("wb+")[1]
|
||||
self._gzip_compress(
|
||||
self.download_filename, self.gzip_filename, 6, processed_size_callback
|
||||
)
|
||||
self.gzip_filesize = os.path.getsize(self.gzip_filename)
|
||||
|
||||
# Make sure the gzip file gets cleaned up when onionshare stops
|
||||
self.cleanup_filenames.append(self.gzip_filename)
|
||||
|
||||
self.is_zipped = False
|
||||
|
||||
else:
|
||||
# Zip up the files and folders
|
||||
self.zip_writer = ZipWriter(
|
||||
self.common, processed_size_callback=processed_size_callback
|
||||
)
|
||||
self.download_filename = self.zip_writer.zip_filename
|
||||
for info in self.file_info["files"]:
|
||||
self.zip_writer.add_file(info["filename"])
|
||||
# Canceling early?
|
||||
if self.web.cancel_compression:
|
||||
self.zip_writer.close()
|
||||
return False
|
||||
|
||||
for info in self.file_info["dirs"]:
|
||||
if not self.zip_writer.add_dir(info["filename"]):
|
||||
return False
|
||||
|
||||
self.zip_writer.close()
|
||||
self.download_filesize = os.path.getsize(self.download_filename)
|
||||
|
||||
# Make sure the zip file gets cleaned up when onionshare stops
|
||||
self.cleanup_filenames.append(self.zip_writer.zip_filename)
|
||||
|
||||
self.is_zipped = True
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ZipWriter(object):
|
||||
"""
|
||||
ZipWriter accepts files and directories and compresses them into a zip file
|
||||
with. If a zip_filename is not passed in, it will use the default onionshare
|
||||
filename.
|
||||
"""
|
||||
|
||||
def __init__(self, common, zip_filename=None, processed_size_callback=None):
|
||||
self.common = common
|
||||
self.cancel_compression = False
|
||||
|
||||
if zip_filename:
|
||||
self.zip_filename = zip_filename
|
||||
else:
|
||||
self.zip_filename = (
|
||||
f"{tempfile.mkdtemp()}/onionshare_{self.common.random_string(4, 6)}.zip"
|
||||
)
|
||||
|
||||
self.z = zipfile.ZipFile(self.zip_filename, "w", allowZip64=True)
|
||||
self.processed_size_callback = processed_size_callback
|
||||
if self.processed_size_callback is None:
|
||||
self.processed_size_callback = lambda _: None
|
||||
self._size = 0
|
||||
self.processed_size_callback(self._size)
|
||||
|
||||
def add_file(self, filename):
|
||||
"""
|
||||
Add a file to the zip archive.
|
||||
"""
|
||||
self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
|
||||
self._size += os.path.getsize(filename)
|
||||
self.processed_size_callback(self._size)
|
||||
|
||||
def add_dir(self, filename):
|
||||
"""
|
||||
Add a directory, and all of its children, to the zip archive.
|
||||
"""
|
||||
dir_to_strip = os.path.dirname(filename.rstrip("/")) + "/"
|
||||
for dirpath, dirnames, filenames in os.walk(filename):
|
||||
for f in filenames:
|
||||
# Canceling early?
|
||||
if self.cancel_compression:
|
||||
return False
|
||||
|
||||
full_filename = os.path.join(dirpath, f)
|
||||
if not os.path.islink(full_filename):
|
||||
arc_filename = full_filename[len(dir_to_strip) :]
|
||||
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
|
||||
self._size += os.path.getsize(full_filename)
|
||||
self.processed_size_callback(self._size)
|
||||
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the zip archive.
|
||||
"""
|
||||
self.z.close()
|
424
cli/onionshare_cli/web/web.py
Normal file
424
cli/onionshare_cli/web/web.py
Normal file
|
@ -0,0 +1,424 @@
|
|||
# -*- 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 hmac
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import socket
|
||||
import sys
|
||||
import tempfile
|
||||
import requests
|
||||
from distutils.version import LooseVersion as Version
|
||||
from urllib.request import urlopen
|
||||
|
||||
import flask
|
||||
from flask import (
|
||||
Flask,
|
||||
request,
|
||||
render_template,
|
||||
abort,
|
||||
make_response,
|
||||
send_file,
|
||||
__version__ as flask_version,
|
||||
)
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
from .share_mode import ShareModeWeb
|
||||
from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest
|
||||
from .website_mode import WebsiteModeWeb
|
||||
from .chat_mode import ChatModeWeb
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
try:
|
||||
flask.cli.show_server_banner = stubbed_show_server_banner
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class Web:
|
||||
"""
|
||||
The Web object is the OnionShare web server, powered by flask
|
||||
"""
|
||||
|
||||
REQUEST_LOAD = 0
|
||||
REQUEST_STARTED = 1
|
||||
REQUEST_PROGRESS = 2
|
||||
REQUEST_CANCELED = 3
|
||||
REQUEST_RATE_LIMIT = 4
|
||||
REQUEST_UPLOAD_FILE_RENAMED = 5
|
||||
REQUEST_UPLOAD_SET_DIR = 6
|
||||
REQUEST_UPLOAD_FINISHED = 7
|
||||
REQUEST_UPLOAD_CANCELED = 8
|
||||
REQUEST_INDIVIDUAL_FILE_STARTED = 9
|
||||
REQUEST_INDIVIDUAL_FILE_PROGRESS = 10
|
||||
REQUEST_INDIVIDUAL_FILE_CANCELED = 11
|
||||
REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 12
|
||||
REQUEST_OTHER = 13
|
||||
REQUEST_INVALID_PASSWORD = 14
|
||||
|
||||
def __init__(self, common, is_gui, mode_settings, mode="share"):
|
||||
self.common = common
|
||||
self.common.log("Web", "__init__", f"is_gui={is_gui}, mode={mode}")
|
||||
|
||||
self.settings = mode_settings
|
||||
|
||||
# The flask app
|
||||
self.app = Flask(
|
||||
__name__,
|
||||
static_folder=self.common.get_resource_path("static"),
|
||||
static_url_path=f"/static_{self.common.random_string(16)}", # randomize static_url_path to avoid making /static unusable
|
||||
template_folder=self.common.get_resource_path("templates"),
|
||||
)
|
||||
self.app.secret_key = self.common.random_string(8)
|
||||
self.generate_static_url_path()
|
||||
self.auth = HTTPBasicAuth()
|
||||
self.auth.error_handler(self.error401)
|
||||
|
||||
# Verbose mode?
|
||||
if self.common.verbose:
|
||||
self.verbose_mode()
|
||||
|
||||
# Are we running in GUI mode?
|
||||
self.is_gui = is_gui
|
||||
|
||||
# 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()
|
||||
|
||||
# Are we using receive mode?
|
||||
self.mode = mode
|
||||
if self.mode == "receive":
|
||||
# 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 = [
|
||||
("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.password = None
|
||||
|
||||
self.reset_invalid_passwords()
|
||||
|
||||
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_password = self.common.random_string(16)
|
||||
|
||||
# Keep track if the server is running
|
||||
self.running = False
|
||||
|
||||
# Define the web app routes
|
||||
self.define_common_routes()
|
||||
|
||||
# Create the mode web object, which defines its own routes
|
||||
self.share_mode = None
|
||||
self.receive_mode = None
|
||||
self.website_mode = None
|
||||
self.chat_mode = None
|
||||
if self.mode == "share":
|
||||
self.share_mode = ShareModeWeb(self.common, self)
|
||||
elif self.mode == "receive":
|
||||
self.receive_mode = ReceiveModeWeb(self.common, self)
|
||||
elif self.mode == "website":
|
||||
self.website_mode = WebsiteModeWeb(self.common, self)
|
||||
elif self.mode == "chat":
|
||||
self.socketio = SocketIO()
|
||||
self.socketio.init_app(self.app)
|
||||
self.chat_mode = ChatModeWeb(self.common, self)
|
||||
|
||||
def get_mode(self):
|
||||
if self.mode == "share":
|
||||
return self.share_mode
|
||||
elif self.mode == "receive":
|
||||
return self.receive_mode
|
||||
elif self.mode == "website":
|
||||
return self.website_mode
|
||||
elif self.mode == "chat":
|
||||
return self.chat_mode
|
||||
else:
|
||||
return None
|
||||
|
||||
def generate_static_url_path(self):
|
||||
# The static URL path has a 128-bit random number in it to avoid having name
|
||||
# collisions with files that might be getting shared
|
||||
self.static_url_path = f"/static_{self.common.random_string(16)}"
|
||||
self.common.log(
|
||||
"Web",
|
||||
"generate_static_url_path",
|
||||
f"new static_url_path is {self.static_url_path}",
|
||||
)
|
||||
|
||||
# Update the flask route to handle the new static URL path
|
||||
self.app.static_url_path = self.static_url_path
|
||||
self.app.add_url_rule(
|
||||
self.static_url_path + "/<path:filename>",
|
||||
endpoint="static",
|
||||
view_func=self.app.send_static_file,
|
||||
)
|
||||
|
||||
def define_common_routes(self):
|
||||
"""
|
||||
Common web app routes between all modes.
|
||||
"""
|
||||
|
||||
@self.auth.get_password
|
||||
def get_pw(username):
|
||||
if username == "onionshare":
|
||||
return self.password
|
||||
else:
|
||||
return None
|
||||
|
||||
@self.app.before_request
|
||||
def conditional_auth_check():
|
||||
# Allow static files without basic authentication
|
||||
if request.path.startswith(self.static_url_path + "/"):
|
||||
return None
|
||||
|
||||
# If public mode is disabled, require authentication
|
||||
if not self.settings.get("general", "public"):
|
||||
|
||||
@self.auth.login_required
|
||||
def _check_login():
|
||||
return None
|
||||
|
||||
return _check_login()
|
||||
|
||||
@self.app.errorhandler(404)
|
||||
def not_found(e):
|
||||
mode = self.get_mode()
|
||||
history_id = mode.cur_history_id
|
||||
mode.cur_history_id += 1
|
||||
return self.error404(history_id)
|
||||
|
||||
@self.app.route("/<password_candidate>/shutdown")
|
||||
def shutdown(password_candidate):
|
||||
"""
|
||||
Stop the flask web server, from the context of an http request.
|
||||
"""
|
||||
if password_candidate == self.shutdown_password:
|
||||
self.force_shutdown()
|
||||
return ""
|
||||
abort(404)
|
||||
|
||||
if self.mode != "website":
|
||||
|
||||
@self.app.route("/favicon.ico")
|
||||
def favicon():
|
||||
return send_file(
|
||||
f"{self.common.get_resource_path('static')}/img/favicon.ico"
|
||||
)
|
||||
|
||||
def error401(self):
|
||||
auth = request.authorization
|
||||
if auth:
|
||||
if (
|
||||
auth["username"] == "onionshare"
|
||||
and auth["password"] not in self.invalid_passwords
|
||||
):
|
||||
print(f"Invalid password guess: {auth['password']}")
|
||||
self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth["password"])
|
||||
|
||||
self.invalid_passwords.append(auth["password"])
|
||||
self.invalid_passwords_count += 1
|
||||
|
||||
if self.invalid_passwords_count == 20:
|
||||
self.add_request(Web.REQUEST_RATE_LIMIT)
|
||||
self.force_shutdown()
|
||||
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."
|
||||
)
|
||||
|
||||
r = make_response(
|
||||
render_template("401.html", static_url_path=self.static_url_path), 401
|
||||
)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def error403(self):
|
||||
self.add_request(Web.REQUEST_OTHER, request.path)
|
||||
r = make_response(
|
||||
render_template("403.html", static_url_path=self.static_url_path), 403
|
||||
)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def error404(self, history_id):
|
||||
self.add_request(
|
||||
self.REQUEST_INDIVIDUAL_FILE_STARTED,
|
||||
request.path,
|
||||
{"id": history_id, "status_code": 404},
|
||||
)
|
||||
|
||||
self.add_request(Web.REQUEST_OTHER, request.path)
|
||||
r = make_response(
|
||||
render_template("404.html", static_url_path=self.static_url_path), 404
|
||||
)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def error405(self, history_id):
|
||||
self.add_request(
|
||||
self.REQUEST_INDIVIDUAL_FILE_STARTED,
|
||||
request.path,
|
||||
{"id": history_id, "status_code": 405},
|
||||
)
|
||||
|
||||
self.add_request(Web.REQUEST_OTHER, request.path)
|
||||
r = make_response(
|
||||
render_template("405.html", static_url_path=self.static_url_path), 405
|
||||
)
|
||||
return self.add_security_headers(r)
|
||||
|
||||
def add_security_headers(self, r):
|
||||
"""
|
||||
Add security headers to a request
|
||||
"""
|
||||
for header, value in self.security_headers:
|
||||
r.headers.set(header, value)
|
||||
# Set a CSP header unless in website mode and the user has disabled it
|
||||
if not self.settings.get("website", "disable_csp") or self.mode != "website":
|
||||
r.headers.set(
|
||||
"Content-Security-Policy",
|
||||
"default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:;",
|
||||
)
|
||||
return r
|
||||
|
||||
def _safe_select_jinja_autoescape(self, filename):
|
||||
if filename is None:
|
||||
return True
|
||||
return filename.endswith((".html", ".htm", ".xml", ".xhtml"))
|
||||
|
||||
def add_request(self, request_type, path=None, data=None):
|
||||
"""
|
||||
Add a request to the queue, to communicate with the GUI.
|
||||
"""
|
||||
self.q.put({"type": request_type, "path": path, "data": data})
|
||||
|
||||
def generate_password(self, saved_password=None):
|
||||
self.common.log("Web", "generate_password", f"saved_password={saved_password}")
|
||||
if saved_password != None and saved_password != "":
|
||||
self.password = saved_password
|
||||
self.common.log(
|
||||
"Web",
|
||||
"generate_password",
|
||||
f'saved_password sent, so password is: "{self.password}"',
|
||||
)
|
||||
else:
|
||||
self.password = self.common.build_password()
|
||||
self.common.log(
|
||||
"Web", "generate_password", f'built random password: "{self.password}"'
|
||||
)
|
||||
|
||||
def verbose_mode(self):
|
||||
"""
|
||||
Turn on verbose mode, which will log flask errors to a file.
|
||||
"""
|
||||
flask_log_filename = os.path.join(self.common.build_data_dir(), "flask.log")
|
||||
log_handler = logging.FileHandler(flask_log_filename)
|
||||
log_handler.setLevel(logging.WARNING)
|
||||
self.app.logger.addHandler(log_handler)
|
||||
|
||||
def reset_invalid_passwords(self):
|
||||
self.invalid_passwords_count = 0
|
||||
self.invalid_passwords = []
|
||||
|
||||
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
|
||||
|
||||
def start(self, port):
|
||||
"""
|
||||
Start the flask web server.
|
||||
"""
|
||||
self.common.log("Web", "start", f"port={port}")
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
if self.mode == "chat":
|
||||
self.socketio.run(self.app, host=host, port=port)
|
||||
else:
|
||||
self.app.run(host=host, port=port, threaded=True)
|
||||
|
||||
def stop(self, port):
|
||||
"""
|
||||
Stop the flask web server by loading /shutdown.
|
||||
"""
|
||||
self.common.log("Web", "stop", "stopping server")
|
||||
|
||||
# Let the mode know that the user stopped the server
|
||||
self.stop_q.put(True)
|
||||
|
||||
# To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown
|
||||
# (We're putting the shutdown_password in the path as well to make routing simpler)
|
||||
if self.running:
|
||||
if self.password:
|
||||
requests.get(
|
||||
f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown",
|
||||
auth=requests.auth.HTTPBasicAuth("onionshare", self.password),
|
||||
)
|
||||
else:
|
||||
requests.get(
|
||||
f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown"
|
||||
)
|
||||
|
||||
# Reset any password that was in use
|
||||
self.password = None
|
123
cli/onionshare_cli/web/website_mode.py
Normal file
123
cli/onionshare_cli/web/website_mode.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
# -*- 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 sys
|
||||
import tempfile
|
||||
import mimetypes
|
||||
from flask import Response, request, render_template, make_response
|
||||
|
||||
from .send_base_mode import SendBaseModeWeb
|
||||
|
||||
|
||||
class WebsiteModeWeb(SendBaseModeWeb):
|
||||
"""
|
||||
All of the web logic for website mode
|
||||
"""
|
||||
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def define_routes(self):
|
||||
"""
|
||||
The web app routes for sharing a website
|
||||
"""
|
||||
|
||||
@self.web.app.route("/", defaults={"path": ""})
|
||||
@self.web.app.route("/<path:path>")
|
||||
def path_public(path):
|
||||
return path_logic(path)
|
||||
|
||||
def path_logic(path=""):
|
||||
"""
|
||||
Render the onionshare website.
|
||||
"""
|
||||
return self.render_logic(path)
|
||||
|
||||
def directory_listing_template(
|
||||
self, path, files, dirs, breadcrumbs, breadcrumbs_leaf
|
||||
):
|
||||
return make_response(
|
||||
render_template(
|
||||
"listing.html",
|
||||
path=path,
|
||||
files=files,
|
||||
dirs=dirs,
|
||||
breadcrumbs=breadcrumbs,
|
||||
breadcrumbs_leaf=breadcrumbs_leaf,
|
||||
static_url_path=self.web.static_url_path,
|
||||
)
|
||||
)
|
||||
|
||||
def set_file_info_custom(self, filenames, processed_size_callback):
|
||||
self.common.log("WebsiteModeWeb", "set_file_info_custom")
|
||||
self.web.cancel_compression = True
|
||||
|
||||
def render_logic(self, path=""):
|
||||
if path in self.files:
|
||||
filesystem_path = self.files[path]
|
||||
|
||||
# If it's a directory
|
||||
if os.path.isdir(filesystem_path):
|
||||
# Is there an index.html?
|
||||
index_path = os.path.join(path, "index.html")
|
||||
if index_path in self.files:
|
||||
# Render it
|
||||
return self.stream_individual_file(self.files[index_path])
|
||||
|
||||
else:
|
||||
# Otherwise, render directory listing
|
||||
filenames = []
|
||||
for filename in os.listdir(filesystem_path):
|
||||
if os.path.isdir(os.path.join(filesystem_path, filename)):
|
||||
filenames.append(filename + "/")
|
||||
else:
|
||||
filenames.append(filename)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path, filesystem_path)
|
||||
|
||||
# If it's a file
|
||||
elif os.path.isfile(filesystem_path):
|
||||
return self.stream_individual_file(filesystem_path)
|
||||
|
||||
# If it's not a directory or file, throw a 404
|
||||
else:
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
||||
else:
|
||||
# Special case loading /
|
||||
|
||||
if path == "":
|
||||
index_path = "index.html"
|
||||
if index_path in self.files:
|
||||
# Render it
|
||||
return self.stream_individual_file(self.files[index_path])
|
||||
else:
|
||||
# Root directory listing
|
||||
filenames = list(self.root_files)
|
||||
filenames.sort()
|
||||
return self.directory_listing(filenames, path)
|
||||
|
||||
else:
|
||||
# If the path isn't found, throw a 404
|
||||
history_id = self.cur_history_id
|
||||
self.cur_history_id += 1
|
||||
return self.web.error404(history_id)
|
Loading…
Add table
Add a link
Reference in a new issue