summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/onionshare_cli/__init__.py44
-rw-r--r--cli/onionshare_cli/common.py218
-rw-r--r--cli/onionshare_cli/mode_settings.py4
-rw-r--r--cli/onionshare_cli/onion.py34
-rw-r--r--cli/onionshare_cli/onionshare.py7
-rw-r--r--cli/onionshare_cli/resources/static/css/style.css7
-rw-r--r--cli/onionshare_cli/resources/static/js/receive.js65
-rw-r--r--cli/onionshare_cli/resources/templates/receive.html21
-rw-r--r--cli/onionshare_cli/settings.py12
-rw-r--r--cli/onionshare_cli/web/receive_mode.py257
-rw-r--r--cli/onionshare_cli/web/send_base_mode.py2
-rw-r--r--cli/onionshare_cli/web/share_mode.py90
-rw-r--r--cli/onionshare_cli/web/web.py28
-rw-r--r--cli/poetry.lock23
-rw-r--r--cli/pyproject.toml1
-rw-r--r--cli/setup.py7
-rw-r--r--cli/tests/conftest.py39
-rw-r--r--cli/tests/test_cli_common.py6
-rw-r--r--cli/tests/test_cli_web.py147
-rw-r--r--cli/tests/test_range_request.py33
-rwxr-xr-xdesktop/package/linux/build-appimage.py2
-rw-r--r--desktop/package/windows/build.py2
-rwxr-xr-xdesktop/scripts/get-tor-osx.py1
-rw-r--r--desktop/scripts/get-tor-windows.py5
-rw-r--r--desktop/src/onionshare/__init__.py1
-rw-r--r--desktop/src/onionshare/gui_common.py4
-rw-r--r--desktop/src/onionshare/resources/images/open_message.pngbin0 -> 5403 bytes
-rw-r--r--desktop/src/onionshare/resources/locale/en.json5
-rw-r--r--desktop/src/onionshare/tab/mode/__init__.py16
-rw-r--r--desktop/src/onionshare/tab/mode/chat_mode/__init__.py6
-rw-r--r--desktop/src/onionshare/tab/mode/history.py84
-rw-r--r--desktop/src/onionshare/tab/mode/mode_settings_widget.py2
-rw-r--r--desktop/src/onionshare/tab/mode/receive_mode/__init__.py40
-rw-r--r--desktop/src/onionshare/tab/mode/share_mode/__init__.py3
-rw-r--r--desktop/src/onionshare/tab/mode/share_mode/threads.py2
-rw-r--r--desktop/src/onionshare/tab/mode/website_mode/__init__.py5
-rw-r--r--desktop/src/onionshare/tab/server_status.py2
-rw-r--r--desktop/src/onionshare/tab/tab.py3
-rw-r--r--desktop/src/onionshare/threads.py2
-rw-r--r--desktop/src/onionshare/tor_connection_dialog.py2
-rw-r--r--desktop/src/onionshare/update_checker.py3
-rw-r--r--desktop/src/setup.py7
-rw-r--r--desktop/tests/conftest.py28
-rw-r--r--desktop/tests/test_gui_receive.py54
44 files changed, 906 insertions, 418 deletions
diff --git a/cli/onionshare_cli/__init__.py b/cli/onionshare_cli/__init__.py
index a9c66510..6a7a0fde 100644
--- a/cli/onionshare_cli/__init__.py
+++ b/cli/onionshare_cli/__init__.py
@@ -18,13 +18,22 @@ 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, sys, time, argparse, threading
+import os
+import sys
+import time
+import argparse
+import threading
from datetime import datetime
from datetime import timedelta
from .common import Common, CannotFindTor
from .web import Web
-from .onion import *
+from .onion import (
+ TorErrorProtocolError,
+ TorTooOldEphemeral,
+ TorTooOldStealth,
+ Onion,
+)
from .onionshare import OnionShare
from .mode_settings import ModeSettings
@@ -155,6 +164,18 @@ def main(cwd=None):
default=None,
help="Receive files: URL to receive webhook notifications",
)
+ parser.add_argument(
+ "--disable-text",
+ action="store_true",
+ dest="disable_text",
+ help="Receive files: Disable receiving text messages",
+ )
+ parser.add_argument(
+ "--disable-files",
+ action="store_true",
+ dest="disable_files",
+ help="Receive files: Disable receiving files",
+ )
# Website args
parser.add_argument(
"--disable_csp",
@@ -199,6 +220,8 @@ def main(cwd=None):
autostop_sharing = not bool(args.no_autostop_sharing)
data_dir = args.data_dir
webhook_url = args.webhook_url
+ disable_text = args.disable_text
+ disable_files = args.disable_files
disable_csp = bool(args.disable_csp)
verbose = bool(args.verbose)
@@ -250,6 +273,8 @@ def main(cwd=None):
mode_settings.set("receive", "data_dir", data_dir)
if webhook_url:
mode_settings.set("receive", "webhook_url", webhook_url)
+ mode_settings.set("receive", "disable_text", disable_text)
+ mode_settings.set("receive", "disable_files", disable_files)
if mode == "website":
mode_settings.set("website", "disable_csp", disable_csp)
else:
@@ -292,6 +317,11 @@ def main(cwd=None):
if persistent_filename:
mode_settings.set(mode, "filenames", filenames)
+ # In receive mode, you must allows either text, files, or both
+ if mode == "receive" and disable_text and disable_files:
+ print("You cannot disable both text and files")
+ sys.exit()
+
# Create the Web object
web = Web(common, False, mode_settings, mode)
@@ -315,7 +345,7 @@ def main(cwd=None):
except KeyboardInterrupt:
print("")
sys.exit()
- except Exception as e:
+ except Exception:
sys.exit()
# Start the onionshare app
@@ -359,7 +389,9 @@ def main(cwd=None):
)
print("")
print(
- "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
+ "Warning: Receive mode lets people upload files to your computer. Some files can potentially take "
+ "control of your computer if you open them. Only open things from people you trust, or if you know "
+ "what you are doing."
)
print("")
if mode_settings.get("general", "client_auth"):
@@ -453,7 +485,9 @@ def main(cwd=None):
)
print("")
print(
- "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
+ "Warning: Receive mode lets people upload files to your computer. Some files can potentially take "
+ "control of your computer if you open them. Only open things from people you trust, or if you know "
+ "what you are doing."
)
print("")
diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py
index 0b0d084f..7ec31ec6 100644
--- a/cli/onionshare_cli/common.py
+++ b/cli/onionshare_cli/common.py
@@ -29,6 +29,9 @@ import time
import shutil
from pkg_resources import resource_filename
+import colorama
+from colorama import Fore, Back, Style
+
from .settings import Settings
@@ -43,18 +46,11 @@ class Common:
The Common object is shared amongst all parts of OnionShare.
"""
- C_RESET = "\033[0m"
- C_BG_PURPLE = "\033[45m"
- C_BOLD = "\033[01m"
- C_WHITE = "\033[97m"
- C_LIGHTGRAY = "\033[37m"
- C_DARKGRAY = "\033[90m"
- C_LIGHTPURPLE = "\033[95m"
- C_DARKPURPLE = "\033[35m"
-
def __init__(self, verbose=False):
self.verbose = verbose
+ colorama.init(autoreset=True)
+
# The platform OnionShare is running on
self.platform = platform.system()
if self.platform.endswith("BSD") or self.platform == "DragonFly":
@@ -90,219 +86,195 @@ class Common:
╰───────────────────────────────────────────╯
"""
+ if self.platform == "Windows":
+ pass
+ else:
+ pass
+
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
- + "╭───────────────────────────────────────────╮"
- + self.C_RESET
+ Back.MAGENTA + Fore.WHITE + "╭───────────────────────────────────────────╮"
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " * "
- + self.C_WHITE
+ + Fore.WHITE
+ "▄▄█████▄▄"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " * "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ▄████▀▀▀████▄"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " * "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ▀▀█▀ ▀██▄ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " * "
- + self.C_WHITE
+ + Fore.WHITE
+ "▄█▄ ▀██▄ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ▄█████▄ ███"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " -+- "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ███ ▀█████▀ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ▀██▄ ▀█▀ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " * "
- + self.C_WHITE
+ + Fore.WHITE
+ "▀██▄ ▄█▄▄"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " * "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " * "
- + self.C_WHITE
+ + Fore.WHITE
+ "▀████▄▄▄████▀ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ▀▀█████▀▀ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_LIGHTPURPLE
+ + Fore.LIGHTMAGENTA_EX
+ " -+- * "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ▄▀▄ ▄▀▀ █ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " █ █ ▀ ▀▄ █ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " █ █ █▀▄ █ ▄▀▄ █▀▄ ▀▄ █▀▄ ▄▀▄ █▄▀ ▄█▄ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_WHITE
+ + Fore.WHITE
+ " ▀▄▀ █ █ █ ▀▄▀ █ █ ▄▄▀ █ █ ▀▄█ █ ▀▄▄ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
- + "│ │"
- + self.C_RESET
+ Back.MAGENTA + Fore.WHITE + "│ │"
)
left_spaces = (43 - len(self.version) - 1) // 2
right_spaces = left_spaces
if left_spaces + len(self.version) + right_spaces < 43:
right_spaces += 1
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ f"{' '*left_spaces}v{self.version}{' '*right_spaces}"
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
- + "│ │"
- + self.C_RESET
+ Back.MAGENTA + Fore.WHITE + "│ │"
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
+ Back.MAGENTA
+ + Fore.WHITE
+ "│"
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ " https://onionshare.org/ "
- + self.C_LIGHTGRAY
+ + Fore.WHITE
+ "│"
- + self.C_RESET
)
print(
- self.C_BG_PURPLE
- + self.C_LIGHTGRAY
- + "╰───────────────────────────────────────────╯"
- + self.C_RESET
+ Back.MAGENTA + Fore.WHITE + "╰───────────────────────────────────────────╯"
)
print()
@@ -319,9 +291,11 @@ class Common:
"""
if self.verbose:
timestamp = time.strftime("%b %d %Y %X")
- final_msg = f"{self.C_DARKGRAY}[{timestamp}]{self.C_RESET} {self.C_LIGHTGRAY}{module}.{func}{self.C_RESET}"
+ final_msg = f"{Fore.LIGHTBLACK_EX + Style.DIM}[{timestamp}]{Style.RESET_ALL} {Fore.WHITE + Style.DIM}{module}.{func}{Style.RESET_ALL}"
if msg:
- final_msg = f"{final_msg}{self.C_LIGHTGRAY}: {msg}{self.C_RESET}"
+ final_msg = (
+ f"{final_msg}{Fore.WHITE + Style.DIM}: {msg}{Style.RESET_ALL}"
+ )
print(final_msg)
def get_resource_path(self, filename):
@@ -383,7 +357,7 @@ class Common:
try:
xdg_config_home = os.environ["XDG_CONFIG_HOME"]
onionshare_data_dir = f"{xdg_config_home}/onionshare"
- except:
+ except Exception:
onionshare_data_dir = os.path.expanduser("~/.config/onionshare")
elif self.platform == "Darwin":
onionshare_data_dir = os.path.expanduser(
@@ -393,7 +367,7 @@ class Common:
try:
xdg_config_home = os.environ["XDG_CONFIG_HOME"]
onionshare_data_dir = f"{xdg_config_home}/onionshare"
- except:
+ except Exception:
onionshare_data_dir = os.path.expanduser("~/.config/onionshare")
# Modify the data dir if running tests
diff --git a/cli/onionshare_cli/mode_settings.py b/cli/onionshare_cli/mode_settings.py
index 9ebf8e61..47900997 100644
--- a/cli/onionshare_cli/mode_settings.py
+++ b/cli/onionshare_cli/mode_settings.py
@@ -54,6 +54,8 @@ class ModeSettings:
"receive": {
"data_dir": self.build_default_receive_data_dir(),
"webhook_url": None,
+ "disable_text": False,
+ "disable_files": False,
},
"website": {"disable_csp": False, "filenames": []},
"chat": {"room": "default"},
@@ -127,7 +129,7 @@ class ModeSettings:
self.fill_in_defaults()
self.common.log("ModeSettings", "load", f"loaded {self.filename}")
return
- except:
+ except Exception:
pass
# If loading settings didn't work, create the settings file
diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py
index 000d9308..38062d43 100644
--- a/cli/onionshare_cli/onion.py
+++ b/cli/onionshare_cli/onion.py
@@ -222,7 +222,7 @@ class Onion(object):
)
try:
self.tor_socks_port = self.common.get_available_port(1000, 65535)
- except:
+ except Exception:
print("OnionShare port not available")
raise PortNotAvailable()
self.tor_torrc = os.path.join(self.tor_data_directory_name, "torrc")
@@ -244,7 +244,7 @@ class Onion(object):
proc.terminate()
proc.wait()
break
- except:
+ except Exception:
pass
if self.common.platform == "Windows" or self.common.platform == "Darwin":
@@ -255,7 +255,7 @@ class Onion(object):
torrc_template += "ControlPort {{control_port}}\n"
try:
self.tor_control_port = self.common.get_available_port(1000, 65535)
- except:
+ except Exception:
print("OnionShare port not available")
raise PortNotAvailable()
self.tor_control_socket = None
@@ -428,7 +428,7 @@ class Onion(object):
try:
self.c = Controller.from_port(port=int(env_port))
found_tor = True
- except:
+ except Exception:
pass
else:
@@ -438,7 +438,7 @@ class Onion(object):
for port in ports:
self.c = Controller.from_port(port=port)
found_tor = True
- except:
+ except Exception:
pass
# If this still didn't work, try guessing the default socket file path
@@ -452,7 +452,7 @@ class Onion(object):
self.c = Controller.from_socket_file(path=socket_file_path)
found_tor = True
- except:
+ except Exception:
pass
# If connecting to default control ports failed, so let's try
@@ -474,14 +474,14 @@ class Onion(object):
self.c = Controller.from_socket_file(path=socket_file_path)
- except:
+ except Exception:
print(automatic_error)
raise TorErrorAutomatic()
# Try authenticating
try:
self.c.authenticate()
- except:
+ except Exception:
print(automatic_error)
raise TorErrorAutomatic()
@@ -504,7 +504,7 @@ class Onion(object):
print(invalid_settings_error)
raise TorErrorInvalidSetting()
- except:
+ except Exception:
if self.settings.get("connection_type") == "control_port":
print(
"Can't connect to the Tor controller at {}:{}.".format(
@@ -582,7 +582,7 @@ class Onion(object):
tmp_service_id = res.service_id
self.c.remove_ephemeral_hidden_service(tmp_service_id)
self.supports_stealth = True
- except:
+ except Exception:
# ephemeral stealth onion services are not supported
self.supports_stealth = False
@@ -708,7 +708,7 @@ class Onion(object):
self.c.remove_ephemeral_hidden_service(
mode_settings.get("general", "service_id")
)
- except:
+ except Exception:
self.common.log(
"Onion", "stop_onion_service", f"failed to remove {onion_host}"
)
@@ -729,12 +729,12 @@ class Onion(object):
"Onion", "cleanup", f"trying to remove onion {onion_host}"
)
self.c.remove_ephemeral_hidden_service(service_id)
- except:
+ except Exception:
self.common.log(
"Onion", "cleanup", f"failed to remove onion {onion_host}"
)
pass
- except:
+ except Exception:
pass
if stop_tor:
@@ -777,7 +777,7 @@ class Onion(object):
)
symbols_i = (symbols_i + 1) % len(symbols)
time.sleep(1)
- except:
+ except Exception:
pass
self.tor_proc.terminate()
@@ -797,7 +797,7 @@ class Onion(object):
"cleanup",
"Tried to kill tor process but it's still running",
)
- except:
+ except Exception:
self.common.log(
"Onion", "cleanup", "Exception while killing tor process"
)
@@ -810,7 +810,7 @@ class Onion(object):
# Delete the temporary tor data directory
if self.use_tmp_dir:
self.tor_data_directory.cleanup()
- except:
+ except Exception:
pass
def get_tor_socks_port(self):
@@ -835,5 +835,5 @@ class Onion(object):
key = RSA.importKey(base64.b64decode(key))
# Is this a v2 Onion key? (1024 bits) If so, we should keep using it.
return key.n.bit_length() == 1024
- except:
+ except Exception:
return False
diff --git a/cli/onionshare_cli/onionshare.py b/cli/onionshare_cli/onionshare.py
index 4e34cf4b..e518d2fb 100644
--- a/cli/onionshare_cli/onionshare.py
+++ b/cli/onionshare_cli/onionshare.py
@@ -18,7 +18,8 @@ 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, shutil
+import os
+import shutil
from .common import AutoStopTimer
@@ -57,7 +58,7 @@ class OnionShare(object):
"""
try:
self.port = self.common.get_available_port(17600, 17650)
- except:
+ except Exception:
raise OSError("Cannot find an available OnionShare port")
def start_onion_service(self, mode, mode_settings, await_publication=True):
@@ -102,7 +103,7 @@ class OnionShare(object):
os.remove(filename)
elif os.path.isdir(filename):
shutil.rmtree(filename)
- except:
+ except Exception:
# Don't crash if file is still in use
pass
self.cleanup_filenames = []
diff --git a/cli/onionshare_cli/resources/static/css/style.css b/cli/onionshare_cli/resources/static/css/style.css
index 88d9cb79..57b23fdb 100644
--- a/cli/onionshare_cli/resources/static/css/style.css
+++ b/cli/onionshare_cli/resources/static/css/style.css
@@ -285,6 +285,13 @@ ul.breadcrumbs li a:link, ul.breadcrumbs li a:visited {
margin: 0 0 20px 0;
}
+.upload-wrapper textarea {
+ max-width: 95%;
+ width: 600px;
+ height: 150px;
+ padding: 10px;
+}
+
div#uploads {
width: 800px;
max-width: 90%;
diff --git a/cli/onionshare_cli/resources/static/js/receive.js b/cli/onionshare_cli/resources/static/js/receive.js
index eac67412..487b8e1d 100644
--- a/cli/onionshare_cli/resources/static/js/receive.js
+++ b/cli/onionshare_cli/resources/static/js/receive.js
@@ -1,24 +1,35 @@
-$(function(){
+$(function () {
// Add a flash message
- var flash = function(category, message) {
+ var flash = function (category, message) {
$('#flashes').append($('<li>').addClass(category).text(message));
};
var scriptSrc = document.getElementById('receive-script').src;
- var staticImgPath = scriptSrc.substr(0, scriptSrc.lastIndexOf( '/' )+1).replace('js', 'img');
+ var staticImgPath = scriptSrc.substr(0, scriptSrc.lastIndexOf('/') + 1).replace('js', 'img');
// Intercept submitting the form
- $('#send').submit(function(event){
+ $('#send').submit(function (event) {
event.preventDefault();
- // Create form data, and list of filenames
- var files = $('#file-select').get(0).files;
- var filenames = [];
+ // Build the form data
var formData = new FormData();
- for(var i = 0; i < files.length; i++) {
- var file = files[i];
- filenames.push(file.name);
- formData.append('file[]', file, file.name);
+
+ // Files
+ var filenames = [];
+ var $fileSelect = $('#file-select');
+ if ($fileSelect.length > 0) {
+ var files = $fileSelect.get(0).files;
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ filenames.push(file.name);
+ formData.append('file[]', file, file.name);
+ }
+ }
+
+ // Text message
+ var $text = $('#text');
+ if ($text.length > 0) {
+ formData.append("text", $text.val())
}
// Reset the upload form
@@ -28,9 +39,9 @@ $(function(){
// have access to the the XMLHttpRequest object
var ajax = new XMLHttpRequest();
- ajax.upload.addEventListener('progress', function(event){
+ ajax.upload.addEventListener('progress', function (event) {
// Update progress bar for this specific upload
- if(event.lengthComputable) {
+ if (event.lengthComputable) {
$('progress', ajax.$upload_div).attr({
value: event.loaded,
max: event.total,
@@ -39,13 +50,13 @@ $(function(){
// If it's finished sending all data to the first Tor node, remove cancel button
// and update the status
- if(event.loaded == event.total) {
+ if (event.loaded == event.total) {
$('.cancel', ajax.$upload_div).remove();
$('.upload-status', ajax.$upload_div).html('<img src="' + staticImgPath + '/ajax.gif" alt="" /> Waiting for data to finish traversing Tor network ...');
}
}, false);
- ajax.addEventListener('load', function(event){
+ ajax.addEventListener('load', function (event) {
// Remove the upload div
ajax.$upload_div.remove();
@@ -54,38 +65,38 @@ $(function(){
var response = JSON.parse(ajax.response);
// The 'new_body' response replaces the whole HTML document and ends
- if('new_body' in response) {
+ if ('new_body' in response) {
$('body').html(response['new_body']);
return;
}
// Show error flashes
- if('error_flashes' in response) {
- for(var i=0; i<response['error_flashes'].length; i++) {
+ if ('error_flashes' in response) {
+ for (var i = 0; i < response['error_flashes'].length; i++) {
flash('error', response['error_flashes'][i]);
}
}
// Show info flashes
- if('info_flashes' in response) {
- for(var i=0; i<response['info_flashes'].length; i++) {
+ if ('info_flashes' in response) {
+ for (var i = 0; i < response['info_flashes'].length; i++) {
flash('info', response['info_flashes'][i]);
}
}
- } catch(e) {
- flash('error', 'Invalid response from server: '+data);
+ } catch (e) {
+ flash('error', 'Invalid response from server: ' + data);
}
}, false);
- ajax.addEventListener('error', function(event){
- flash('error', 'Error uploading: '+filenames.join(', '));
+ ajax.addEventListener('error', function (event) {
+ flash('error', 'Error uploading: ' + filenames.join(', '));
// Remove the upload div
ajax.$upload_div.remove()
}, false);
- ajax.addEventListener('abort', function(event){
- flash('error', 'Upload aborted: '+filenames.join(', '));
+ ajax.addEventListener('abort', function (event) {
+ flash('error', 'Upload aborted: ' + filenames.join(', '));
}, false);
// Make the upload div
@@ -114,7 +125,7 @@ $(function(){
)
.append($progress);
- $cancel_button.click(function(){
+ $cancel_button.click(function () {
// Abort the upload, and remove the upload div
ajax.abort();
$upload_div.remove()
diff --git a/cli/onionshare_cli/resources/templates/receive.html b/cli/onionshare_cli/resources/templates/receive.html
index 08b9623c..c28813ea 100644
--- a/cli/onionshare_cli/resources/templates/receive.html
+++ b/cli/onionshare_cli/resources/templates/receive.html
@@ -19,8 +19,18 @@
<div class="upload-wrapper">
<p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
- <p class="upload-header">Send Files</p>
- <p class="upload-description">Select the files you want to send, then click "Send Files"...</p>
+ {% if not disable_text and not disable_files %}
+ <p class="upload-header">Submit Files or Messages</p>
+ <p class="upload-description">You can submit files, a message, or both</p>
+ {% endif %}
+ {% if not disable_text and disable_files %}
+ <p class="upload-header">Submit Messages</p>
+ <p class="upload-description">You can submit a message</p>
+ {% endif %}
+ {% if disable_text and not disable_files %}
+ <p class="upload-header">Submit Files</p>
+ <p class="upload-description">You can submit files</p>
+ {% endif %}
<div id="uploads"></div>
@@ -37,8 +47,13 @@
</div>
<form id="send" method="post" enctype="multipart/form-data" action="/upload">
+ {% if not disable_files %}
<p><input type="file" id="file-select" name="file[]" multiple /></p>
- <p><button type="submit" id="send-button" class="button">Send Files</button></p>
+ {% endif %}
+ {% if not disable_text %}
+ <p><textarea id="text" name="text" placeholder="Write a message"></textarea></p>
+ {% endif %}
+ <p><button type="submit" id="send-button" class="button">Submit</button></p>
</form>
</div>
diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py
index 00e3e79b..59fcf8df 100644
--- a/cli/onionshare_cli/settings.py
+++ b/cli/onionshare_cli/settings.py
@@ -22,12 +22,6 @@ import json
import os
import locale
-try:
- # We only need pwd module in macOS, and it's not available in Windows
- import pwd
-except:
- pass
-
class Settings(object):
"""
@@ -166,13 +160,13 @@ class Settings(object):
with open(self.filename, "r") as f:
self._settings = json.load(f)
self.fill_in_defaults()
- except:
+ except Exception:
pass
# Make sure data_dir exists
try:
os.makedirs(self.get("data_dir"), exist_ok=True)
- except:
+ except Exception:
pass
def save(self):
@@ -191,7 +185,7 @@ class Settings(object):
if key == "control_port_port" or key == "socks_port":
try:
val = int(val)
- except:
+ except Exception:
if key == "control_port_port":
val = self.default_settings["control_port_port"]
elif key == "socks_port":
diff --git a/cli/onionshare_cli/web/receive_mode.py b/cli/onionshare_cli/web/receive_mode.py
index 21e83f58..f5aae296 100644
--- a/cli/onionshare_cli/web/receive_mode.py
+++ b/cli/onionshare_cli/web/receive_mode.py
@@ -26,6 +26,26 @@ from datetime import datetime
from flask import Request, request, render_template, make_response, flash, redirect
from werkzeug.utils import secure_filename
+# Receive mode uses a special flask requests object, ReceiveModeRequest, in
+# order to keep track of upload progress. Here's what happens when someone
+# uploads files:
+# - new ReceiveModeRequest object is created
+# - ReceiveModeRequest.__init__
+# - creates a directory based on the timestamp
+# - creates empty self.progress = dict, which will map uploaded files to their upload progress
+# - ReceiveModeRequest._get_file_stream
+# - called for each file that gets upload
+# - the first time, send REQUEST_STARTED to GUI, and append to self.web.receive_mode.uploads_in_progress
+# - updates self.progress[self.filename] for the current file
+# - uses custom ReceiveModeFile to save file to disk
+# - ReceiveModeRequest.file_write_func called on each write
+# - Display progress in CLI, and send REQUEST_PROGRESS to GUI
+# - ReceiveModeRequest.file_close_func called when each file closes
+# - self.progress[filename]["complete"] = True
+# - ReceiveModeRequest.close
+# - send either REQUEST_UPLOAD_CANCELED or REQUEST_UPLOAD_FINISHED to GUI
+# - remove from self.web.receive_mode.uploads_in_progress
+
class ReceiveModeWeb:
"""
@@ -66,6 +86,8 @@ class ReceiveModeWeb:
render_template(
"receive.html",
static_url_path=self.web.static_url_path,
+ disable_text=self.web.settings.get("receive", "disable_text"),
+ disable_files=self.web.settings.get("receive", "disable_files"),
title=self.web.settings.get("general", "title"),
)
)
@@ -77,44 +99,58 @@ class ReceiveModeWeb:
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}")
+ message_received = request.includes_message
+
+ files_received = 0
+ if not self.web.settings.get("receive", "disable_files"):
+ 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"Received: {local_path}")
+
+ files_received = len(filenames)
# Send webhook if configured
if (
- self.web.settings.get("receive", "webhook_url")
+ self.web.settings.get("receive", "webhook_url") is not None
and not request.upload_error
- and len(files) > 0
+ and (message_received or files_received)
):
- 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")
+ msg = ""
+ if files_received > 0:
+ if files_received == 1:
+ msg += "1 file"
+ else:
+ msg += f"{files_received} files"
+ if message_received:
+ if msg == "":
+ msg = "A text message"
+ else:
+ msg += " and a text message"
+ self.send_webhook_notification(f"{msg} submitted to OnionShare")
if request.upload_error:
self.common.log(
@@ -142,21 +178,27 @@ class ReceiveModeWeb:
if ajax:
info_flashes = []
- if len(filenames) == 0:
- msg = "No files uploaded"
- if ajax:
- info_flashes.append(msg)
+ if files_received > 0:
+ files_msg = ""
+ for filename in filenames:
+ files_msg += f"{filename}, "
+ files_msg = files_msg.rstrip(", ")
+
+ if message_received:
+ if files_received > 0:
+ msg = f"Message submitted, uploaded {files_msg}"
else:
- flash(msg, "info")
+ msg = "Message submitted"
else:
- msg = "Sent "
- for filename in filenames:
- msg += f"{filename}, "
- msg = msg.rstrip(", ")
- if ajax:
- info_flashes.append(msg)
+ if files_received > 0:
+ msg = f"Uploaded {files_msg}"
else:
- flash(msg, "info")
+ msg = "Nothing submitted"
+
+ if ajax:
+ info_flashes.append(msg)
+ else:
+ flash(msg, "info")
if self.can_upload:
if ajax:
@@ -238,7 +280,7 @@ class ReceiveModeFile(object):
self.upload_error = False
try:
self.f = open(self.filename_in_progress, "wb+")
- except:
+ except Exception:
# 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
@@ -286,7 +328,7 @@ class ReceiveModeFile(object):
bytes_written = self.f.write(b)
self.onionshare_write_func(self.onionshare_filename, bytes_written)
- except:
+ except Exception:
self.upload_error = True
def close(self):
@@ -300,7 +342,7 @@ class ReceiveModeFile(object):
# Rename the in progress file to the final filename
os.rename(self.filename_in_progress, self.filename)
- except:
+ except Exception:
self.upload_error = True
self.onionshare_close_func(self.onionshare_filename, self.upload_error)
@@ -316,8 +358,7 @@ class ReceiveModeRequest(Request):
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
self.web = environ["web"]
self.stop_q = environ["stop_q"]
-
- self.web.common.log("ReceiveModeRequest", "__init__")
+ self.filename = None
# Prevent running the close() method more than once
self.closed = False
@@ -329,13 +370,15 @@ class ReceiveModeRequest(Request):
self.upload_request = True
if self.upload_request:
+ self.web.common.log("ReceiveModeRequest", "__init__")
+
# 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")
+ 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
)
@@ -383,6 +426,9 @@ class ReceiveModeRequest(Request):
)
self.upload_error = True
+ # Figure out the message filename, in case there is a message
+ self.message_filename = f"{self.receive_mode_dir}-message.txt"
+
# If there's an error so far, finish early
if self.upload_error:
return
@@ -400,7 +446,7 @@ class ReceiveModeRequest(Request):
# Figure out the content length
try:
self.content_length = int(self.headers["Content-Length"])
- except:
+ except Exception:
self.content_length = 0
date_str = datetime.now().strftime("%b %d, %I:%M%p")
@@ -412,6 +458,60 @@ class ReceiveModeRequest(Request):
self.previous_file = None
+ # Is there a text message?
+ self.includes_message = False
+ if not self.web.settings.get("receive", "disable_text"):
+ text_message = self.form.get("text")
+ if text_message:
+ if text_message.strip() != "":
+ self.includes_message = True
+
+ with open(self.message_filename, "w") as f:
+ f.write(text_message)
+
+ self.web.common.log(
+ "ReceiveModeRequest",
+ "__init__",
+ f"saved message to {self.message_filename}",
+ )
+ print(f"Received: {self.message_filename}")
+
+ # Tell the GUI about the message
+ self.tell_gui_request_started()
+ self.web.common.log(
+ "ReceiveModeRequest",
+ "__init__",
+ "sending REQUEST_UPLOAD_INCLUDES_MESSAGE to GUI",
+ )
+ self.web.add_request(
+ self.web.REQUEST_UPLOAD_INCLUDES_MESSAGE,
+ self.path,
+ {
+ "id": self.history_id,
+ "filename": self.message_filename,
+ },
+ )
+
+ def tell_gui_request_started(self):
+ # Tell the GUI about the request
+ if not self.told_gui_about_request:
+ self.web.common.log(
+ "ReceiveModeRequest",
+ "tell_gui_request_started",
+ "sending REQUEST_STARTED to GUI",
+ )
+ 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
+
def _get_file_stream(
self, total_content_length, content_type, filename=None, content_length=None
):
@@ -420,16 +520,7 @@ class ReceiveModeRequest(Request):
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.tell_gui_request_started()
self.filename = secure_filename(filename)
@@ -456,29 +547,47 @@ class ReceiveModeRequest(Request):
return
self.closed = True
- self.web.common.log("ReceiveModeRequest", "close")
+ if self.upload_request:
+ 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"]
+ if not self.web.stop_q.empty() or (
+ self.filename in self.progress
+ and not self.progress[self.filename]["complete"]
):
# Inform the GUI that the upload has canceled
+ self.web.common.log(
+ "ReceiveModeRequest",
+ "close",
+ "sending REQUEST_UPLOAD_CANCELED to GUI",
+ )
self.web.add_request(
- self.web.REQUEST_UPLOAD_CANCELED, self.path, {"id": history_id}
+ self.web.REQUEST_UPLOAD_CANCELED,
+ self.path,
+ {"id": history_id},
)
else:
# Inform the GUI that the upload has finished
+ self.web.common.log(
+ "ReceiveModeRequest",
+ "close",
+ "sending REQUEST_UPLOAD_FINISHED to GUI",
+ )
self.web.add_request(
- self.web.REQUEST_UPLOAD_FINISHED, self.path, {"id": history_id}
+ self.web.REQUEST_UPLOAD_FINISHED,
+ self.path,
+ {"id": history_id},
)
self.web.receive_mode.uploads_in_progress.remove(history_id)
- except AttributeError:
- pass
+ # If no files were written to self.receive_mode_dir, delete it
+ try:
+ if len(os.listdir(self.receive_mode_dir)) == 0:
+ os.rmdir(self.receive_mode_dir)
+ except Exception:
+ pass
def file_write_func(self, filename, length):
"""
@@ -496,7 +605,11 @@ class ReceiveModeRequest(Request):
size_str = self.web.common.human_readable_filesize(
self.progress[filename]["uploaded_bytes"]
)
- print(f"\r=> {size_str} {filename} ", end="")
+
+ if self.web.common.verbose:
+ print(f"=> {size_str} {filename}")
+ else:
+ print(f"\r=> {size_str} {filename} ", end="")
# Update the GUI on the upload progress
if self.told_gui_about_request:
diff --git a/cli/onionshare_cli/web/send_base_mode.py b/cli/onionshare_cli/web/send_base_mode.py
index b42c346b..c7347347 100644
--- a/cli/onionshare_cli/web/send_base_mode.py
+++ b/cli/onionshare_cli/web/send_base_mode.py
@@ -242,7 +242,7 @@ class SendBaseModeWeb:
},
)
done = False
- except:
+ except Exception:
# Looks like the download was canceled
done = True
diff --git a/cli/onionshare_cli/web/share_mode.py b/cli/onionshare_cli/web/share_mode.py
index b0b096ca..4dee0cee 100644
--- a/cli/onionshare_cli/web/share_mode.py
+++ b/cli/onionshare_cli/web/share_mode.py
@@ -44,7 +44,7 @@ def make_etag(data):
else:
break
- hash_value = binascii.hexlify(hasher.digest()).decode('utf-8')
+ hash_value = binascii.hexlify(hasher.digest()).decode("utf-8")
return '"sha256:{}"'.format(hash_value)
@@ -53,13 +53,13 @@ def parse_range_header(range_header: str, target_size: int) -> list:
if range_header is None:
return [(0, end_index)]
- bytes_ = 'bytes='
+ bytes_ = "bytes="
if not range_header.startswith(bytes_):
abort(416)
ranges = []
- for range_ in range_header[len(bytes_):].split(','):
- split = range_.split('-')
+ for range_ in range_header[len(bytes_) :].split(","):
+ split = range_.split("-")
if len(split) == 1:
try:
start = int(split[0])
@@ -194,13 +194,17 @@ class ShareModeWeb(SendBaseModeWeb):
etag = self.download_etag
# for range requests
- range_, status_code = self.get_range_and_status_code(self.filesize, etag, self.last_modified)
+ range_, status_code = self.get_range_and_status_code(
+ self.filesize, etag, self.last_modified
+ )
# Tell GUI the download started
history_id = self.cur_history_id
self.cur_history_id += 1
self.web.add_request(
- self.web.REQUEST_STARTED, request_path, {"id": history_id, "use_gzip": use_gzip}
+ self.web.REQUEST_STARTED,
+ request_path,
+ {"id": history_id, "use_gzip": use_gzip},
)
basename = os.path.basename(self.download_filename)
@@ -209,32 +213,41 @@ class ShareModeWeb(SendBaseModeWeb):
r = Response()
else:
r = Response(
- self.generate(shutdown_func, range_, file_to_download, request_path,
- history_id, self.filesize))
+ self.generate(
+ shutdown_func,
+ range_,
+ file_to_download,
+ request_path,
+ history_id,
+ self.filesize,
+ )
+ )
if use_gzip:
- r.headers.set('Content-Encoding', 'gzip')
+ r.headers.set("Content-Encoding", "gzip")
- r.headers.set('Content-Length', range_[1] - range_[0] + 1)
+ r.headers.set("Content-Length", range_[1] - range_[0] + 1)
filename_dict = {
"filename": unidecode(basename),
"filename*": "UTF-8''%s" % url_quote(basename),
}
- r.headers.set('Content-Disposition', 'attachment', **filename_dict)
+ r.headers.set("Content-Disposition", "attachment", **filename_dict)
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)
- r.headers.set('Accept-Ranges', 'bytes')
- r.headers.set('ETag', etag)
- r.headers.set('Last-Modified', http_date(self.last_modified))
+ r.headers.set("Content-Type", content_type)
+ r.headers.set("Accept-Ranges", "bytes")
+ r.headers.set("ETag", etag)
+ r.headers.set("Last-Modified", http_date(self.last_modified))
# we need to set this for range requests
- r.headers.set('Vary', 'Accept-Encoding')
+ r.headers.set("Vary", "Accept-Encoding")
if status_code == 206:
- r.headers.set('Content-Range',
- 'bytes {}-{}/{}'.format(range_[0], range_[1], self.filesize))
+ r.headers.set(
+ "Content-Range",
+ "bytes {}-{}/{}".format(range_[0], range_[1], self.filesize),
+ )
r.status_code = status_code
@@ -244,17 +257,19 @@ class ShareModeWeb(SendBaseModeWeb):
def get_range_and_status_code(cls, dl_size, etag, last_modified):
use_default_range = True
status_code = 200
- range_header = request.headers.get('Range')
+ range_header = request.headers.get("Range")
# range requests are only allowed for get
- if request.method == 'GET':
+ if request.method == "GET":
ranges = parse_range_header(range_header, dl_size)
- if not (len(ranges) == 1 and ranges[0][0] == 0 and ranges[0][1] == dl_size - 1):
+ if not (
+ len(ranges) == 1 and ranges[0][0] == 0 and ranges[0][1] == dl_size - 1
+ ):
use_default_range = False
status_code = 206
if range_header:
- if_range = request.headers.get('If-Range')
+ if_range = request.headers.get("If-Range")
if if_range and if_range != etag:
use_default_range = True
status_code = 200
@@ -266,11 +281,11 @@ class ShareModeWeb(SendBaseModeWeb):
abort(416) # We don't support multipart range requests yet
range_ = ranges[0]
- etag_header = request.headers.get('ETag')
+ etag_header = request.headers.get("ETag")
if etag_header is not None and etag_header != etag:
abort(412)
- if_unmod = request.headers.get('If-Unmodified-Since')
+ if_unmod = request.headers.get("If-Unmodified-Since")
if if_unmod:
if_date = parse_date(if_unmod)
if if_date and if_date > last_modified:
@@ -280,10 +295,12 @@ class ShareModeWeb(SendBaseModeWeb):
return range_, status_code
- def generate(self, shutdown_func, range_, file_to_download, path, history_id, filesize):
+ def generate(
+ self, shutdown_func, range_, file_to_download, path, history_id, filesize
+ ):
# The user hasn't canceled the download
self.client_cancel = False
-
+
# Starting a new download
if self.web.settings.get("share", "autostop_sharing"):
self.download_in_progress = True
@@ -326,9 +343,7 @@ class ShareModeWeb(SendBaseModeWeb):
):
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(
- self.common.human_readable_filesize(
- downloaded_bytes
- ),
+ self.common.human_readable_filesize(downloaded_bytes),
percent,
)
)
@@ -337,10 +352,14 @@ class ShareModeWeb(SendBaseModeWeb):
self.web.add_request(
self.web.REQUEST_PROGRESS,
path,
- {"id": history_id, "bytes": downloaded_bytes, 'total_bytes': filesize,},
+ {
+ "id": history_id,
+ "bytes": downloaded_bytes,
+ "total_bytes": filesize,
+ },
)
self.web.done = False
- except:
+ except Exception:
# looks like the download was canceled
self.web.done = True
canceled = True
@@ -367,10 +386,9 @@ class ShareModeWeb(SendBaseModeWeb):
if shutdown_func is None:
raise RuntimeError("Not running with the Werkzeug Server")
shutdown_func()
- except:
+ except Exception:
pass
-
def directory_listing_template(
self, path, files, dirs, breadcrumbs, breadcrumbs_leaf
):
@@ -466,7 +484,7 @@ class ShareModeWeb(SendBaseModeWeb):
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"]
- with open(self.download_filename, 'rb') as f:
+ with open(self.download_filename, "rb") as f:
self.download_etag = make_etag(f)
# Compress the file with gzip now, so we don't have to do it on each request
@@ -475,7 +493,7 @@ class ShareModeWeb(SendBaseModeWeb):
self.download_filename, self.gzip_filename, 6, processed_size_callback
)
self.gzip_filesize = os.path.getsize(self.gzip_filename)
- with open(self.gzip_filename, 'rb') as f:
+ with open(self.gzip_filename, "rb") as f:
self.gzip_etag = make_etag(f)
# Make sure the gzip file gets cleaned up when onionshare stops
@@ -502,7 +520,7 @@ class ShareModeWeb(SendBaseModeWeb):
self.zip_writer.close()
self.download_filesize = os.path.getsize(self.download_filename)
- with open(self.download_filename, 'rb') as f:
+ with open(self.download_filename, "rb") as f:
self.download_etag = make_etag(f)
# Make sure the zip file gets cleaned up when onionshare stops
diff --git a/cli/onionshare_cli/web/web.py b/cli/onionshare_cli/web/web.py
index 7c2e4256..da15c23b 100644
--- a/cli/onionshare_cli/web/web.py
+++ b/cli/onionshare_cli/web/web.py
@@ -41,6 +41,7 @@ from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveMode
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):
@@ -49,7 +50,7 @@ def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
try:
flask.cli.show_server_banner = stubbed_show_server_banner
-except:
+except Exception:
pass
@@ -63,16 +64,17 @@ class Web:
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
+ REQUEST_UPLOAD_INCLUDES_MESSAGE = 5
+ REQUEST_UPLOAD_FILE_RENAMED = 6
+ REQUEST_UPLOAD_SET_DIR = 7
+ REQUEST_UPLOAD_FINISHED = 8
+ REQUEST_UPLOAD_CANCELED = 9
+ REQUEST_INDIVIDUAL_FILE_STARTED = 10
+ REQUEST_INDIVIDUAL_FILE_PROGRESS = 11
+ REQUEST_INDIVIDUAL_FILE_CANCELED = 12
+ REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 13
+ REQUEST_OTHER = 14
+ REQUEST_INVALID_PASSWORD = 15
def __init__(self, common, is_gui, mode_settings, mode="share"):
self.common = common
@@ -327,7 +329,7 @@ class Web:
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 != "":
+ if saved_password is not None and saved_password != "":
self.password = saved_password
self.common.log(
"Web",
@@ -363,7 +365,7 @@ class Web:
if func is None and self.mode != "chat":
raise RuntimeError("Not running with the Werkzeug Server")
func()
- except:
+ except Exception:
pass
self.running = False
diff --git a/cli/poetry.lock b/cli/poetry.lock
index e507395b..798b8c8a 100644
--- a/cli/poetry.lock
+++ b/cli/poetry.lock
@@ -63,7 +63,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@@ -424,7 +424,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata]
lock-version = "1.1"
python-versions = "^3.6"
-content-hash = "27f9680e537bbe672c9dc3e65a88e3d9f19c4f849135153580a4209773bbced5"
+content-hash = "af196689bfa09fc05b61fc0829e1b0b54888b5503602ff04174bc967d688c180"
[metadata.files]
atomicwrites = [
@@ -559,20 +559,39 @@ markupsafe = [
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
packaging = [
diff --git a/cli/pyproject.toml b/cli/pyproject.toml
index ecf19e5d..201a6186 100644
--- a/cli/pyproject.toml
+++ b/cli/pyproject.toml
@@ -30,6 +30,7 @@ unidecode = "*"
urllib3 = "*"
eventlet = "*"
setuptools = "*"
+colorama = "^0.4.4"
[tool.poetry.dev-dependencies]
pytest = "*"
diff --git a/cli/setup.py b/cli/setup.py
index e6050ff8..58bc52e0 100644
--- a/cli/setup.py
+++ b/cli/setup.py
@@ -25,7 +25,12 @@ version = "2.3.1"
setuptools.setup(
name="onionshare-cli",
version=version,
- description="OnionShare lets you securely and anonymously send and receive files. It works by starting a web server, making it accessible as a Tor onion service, and generating an unguessable web address so others can download files from you, or upload files to you. It does _not_ require setting up a separate server or using a third party file-sharing service.",
+ description=(
+ "OnionShare lets you securely and anonymously send and receive files. It works by starting a web server, "
+ "making it accessible as a Tor onion service, and generating an unguessable web address so others can "
+ "download files from you, or upload files to you. It does _not_ require setting up a separate server or "
+ "using a third party file-sharing service."
+ ),
author="Micah Lee",
author_email="micah@micahflee.com",
maintainer="Micah Lee",
diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py
index 04d4112a..37b929ec 100644
--- a/cli/tests/conftest.py
+++ b/cli/tests/conftest.py
@@ -1,11 +1,4 @@
import sys
-
-# Force tests to look for resources in the source code tree
-sys.onionshare_dev_mode = True
-
-# Let OnionShare know the tests are running, to avoid colliding with settings files
-sys.onionshare_test_mode = True
-
import os
import shutil
import tempfile
@@ -14,6 +7,11 @@ import pytest
from onionshare_cli import common, web
+# Force tests to look for resources in the source code tree
+sys.onionshare_dev_mode = True
+
+# Let OnionShare know the tests are running, to avoid colliding with settings files
+sys.onionshare_test_mode = True
# The temporary directory for CLI tests
test_temp_dir = None
@@ -45,7 +43,7 @@ def temp_dir():
@pytest.fixture
def temp_dir_1024(temp_dir):
- """ Create a temporary directory that has a single file of a
+ """Create a temporary directory that has a single file of a
particular size (1024 bytes).
"""
@@ -56,10 +54,9 @@ def temp_dir_1024(temp_dir):
return new_temp_dir
-# pytest > 2.9 only needs @pytest.fixture
-@pytest.yield_fixture
+@pytest.fixture
def temp_dir_1024_delete(temp_dir):
- """ Create a temporary directory that has a single file of a
+ """Create a temporary directory that has a single file of a
particular size (1024 bytes). The temporary directory (including
the file inside) will be deleted after fixture usage.
"""
@@ -73,15 +70,14 @@ def temp_dir_1024_delete(temp_dir):
@pytest.fixture
def temp_file_1024(temp_dir):
- """ Create a temporary file of a particular size (1024 bytes). """
+ """Create a temporary file of a particular size (1024 bytes)."""
with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir) as tmp_file:
tmp_file.write(b"*" * 1024)
return tmp_file.name
-# pytest > 2.9 only needs @pytest.fixture
-@pytest.yield_fixture
+@pytest.fixture
def temp_file_1024_delete(temp_dir):
"""
Create a temporary file of a particular size (1024 bytes).
@@ -95,8 +91,7 @@ def temp_file_1024_delete(temp_dir):
yield tmp_file.name
-# pytest > 2.9 only needs @pytest.fixture
-@pytest.yield_fixture(scope="session")
+@pytest.fixture(scope="session")
def custom_zw():
zw = web.share_mode.ZipWriter(
common.Common(),
@@ -108,8 +103,7 @@ def custom_zw():
os.remove(zw.zip_filename)
-# pytest > 2.9 only needs @pytest.fixture
-@pytest.yield_fixture(scope="session")
+@pytest.fixture(scope="session")
def default_zw():
zw = web.share_mode.ZipWriter(common.Common())
yield zw
@@ -117,7 +111,7 @@ def default_zw():
tmp_dir = os.path.dirname(zw.zip_filename)
try:
shutil.rmtree(tmp_dir, ignore_errors=True)
- except:
+ except Exception:
pass
@@ -189,10 +183,3 @@ def time_strftime(monkeypatch):
@pytest.fixture
def common_obj():
return common.Common()
-
-
-@pytest.fixture
-def settings_obj(sys_onionshare_dev_mode, platform_linux):
- _common = common.Common()
- _common.version = "DUMMY_VERSION_1.2.3"
- return settings.Settings(_common)
diff --git a/cli/tests/test_cli_common.py b/cli/tests/test_cli_common.py
index ef4732be..3288e52b 100644
--- a/cli/tests/test_cli_common.py
+++ b/cli/tests/test_cli_common.py
@@ -131,7 +131,7 @@ class TestGetAvailablePort:
((random.randint(1024, 1500), random.randint(1800, 2048)) for _ in range(50)),
)
def test_returns_an_open_port(self, common_obj, port_min, port_max):
- """ get_available_port() should return an open port within the range """
+ """get_available_port() should return an open port within the range"""
port = common_obj.get_available_port(port_min, port_max)
assert port_min <= port <= port_max
@@ -191,7 +191,9 @@ class TestGetTorPaths:
@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows")
def test_get_tor_paths_windows(self, platform_windows, common_obj, sys_frozen):
base_path = os.path.join(
- os.path.dirname(os.path.dirname(common_obj.get_resource_path(""))), "tor"
+ os.path.dirname(os.path.dirname(common_obj.get_resource_path(""))),
+ "resources",
+ "tor",
)
tor_path = os.path.join(os.path.join(base_path, "Tor"), "tor.exe")
obfs4proxy_file_path = os.path.join(
diff --git a/cli/tests/test_cli_web.py b/cli/tests/test_cli_web.py
index edd838c4..ec51654c 100644
--- a/cli/tests/test_cli_web.py
+++ b/cli/tests/test_cli_web.py
@@ -7,6 +7,8 @@ import time
import zipfile
import tempfile
import base64
+import shutil
+import sys
from io import BytesIO
import pytest
@@ -42,7 +44,7 @@ RANDOM_STR_REGEX = re.compile(r"^[a-z2-7]+$")
def web_obj(temp_dir, common_obj, mode, num_files=0):
- """ Creates a Web object, in either share mode or receive mode, ready for testing """
+ """Creates a Web object, in either share mode or receive mode, ready for testing"""
common_obj.settings = Settings(common_obj)
mode_settings = ModeSettings(common_obj)
web = Web(common_obj, False, mode_settings, mode)
@@ -100,7 +102,7 @@ class TestWeb:
web = web_obj(temp_dir, common_obj, "share", 3)
web.settings.set("share", "autostop_sharing", True)
- assert web.running == True
+ assert web.running is True
with web.app.test_client() as c:
# Download the first time
@@ -112,7 +114,7 @@ class TestWeb:
or res.mimetype == "application/x-zip-compressed"
)
- assert web.running == False
+ assert web.running is False
def test_share_mode_autostop_sharing_off(
self, temp_dir, common_obj, temp_file_1024
@@ -120,7 +122,7 @@ class TestWeb:
web = web_obj(temp_dir, common_obj, "share", 3)
web.settings.set("share", "autostop_sharing", False)
- assert web.running == True
+ assert web.running is True
with web.app.test_client() as c:
# Download the first time
@@ -131,7 +133,7 @@ class TestWeb:
res.mimetype == "application/zip"
or res.mimetype == "application/x-zip-compressed"
)
- assert web.running == True
+ assert web.running is True
def test_receive_mode(self, temp_dir, common_obj):
web = web_obj(temp_dir, common_obj, "receive")
@@ -183,7 +185,135 @@ class TestWeb:
assert res.status_code == 200
assert webhook_url == "http://127.0.0.1:1337/example"
- assert webhook_data == "1 file uploaded to OnionShare"
+ assert webhook_data == "1 file submitted to OnionShare"
+
+ def test_receive_mode_message_no_files(self, temp_dir, common_obj):
+ web = web_obj(temp_dir, common_obj, "receive")
+
+ data_dir = os.path.join(temp_dir, "OnionShare")
+ os.makedirs(data_dir, exist_ok=True)
+
+ web.settings.set("receive", "data_dir", data_dir)
+
+ with web.app.test_client() as c:
+ res = c.post(
+ "/upload-ajax",
+ buffered=True,
+ content_type="multipart/form-data",
+ data={"text": "you know just sending an anonymous message"},
+ headers=self._make_auth_headers(web.password),
+ )
+ content = res.get_data()
+ assert res.status_code == 200
+ assert b"Message submitted" in content
+
+ # ~/OnionShare should have a folder for the date
+ filenames = os.listdir(data_dir)
+ assert len(filenames) == 1
+ data_dir_date = os.path.join(data_dir, filenames[0])
+
+ # The date folder should have a single message txt file, no folders
+ filenames = os.listdir(data_dir_date)
+ assert len(filenames) == 1
+ assert filenames[0].endswith("-message.txt")
+
+ shutil.rmtree(data_dir)
+
+ def test_receive_mode_message_and_files(self, temp_dir, common_obj):
+ web = web_obj(temp_dir, common_obj, "receive")
+
+ data_dir = os.path.join(temp_dir, "OnionShare")
+ os.makedirs(data_dir, exist_ok=True)
+
+ web.settings.set("receive", "data_dir", data_dir)
+
+ with web.app.test_client() as c:
+ res = c.post(
+ "/upload-ajax",
+ buffered=True,
+ content_type="multipart/form-data",
+ data={
+ "file[]": (BytesIO(b"THIS IS A TEST FILE"), "new_york.jpg"),
+ "text": "you know just sending an anonymous message",
+ },
+ headers=self._make_auth_headers(web.password),
+ )
+ content = res.get_data()
+ assert res.status_code == 200
+ assert b"Message submitted, uploaded new_york.jpg" in content
+
+ # Date folder should have a time folder with new_york.jpg, and a text message file
+ data_dir_date = os.path.join(data_dir, os.listdir(data_dir)[0])
+ filenames = os.listdir(data_dir_date)
+ assert len(filenames) == 2
+ time_str = filenames[0][0:6]
+ assert time_str in filenames
+ assert f"{time_str}-message.txt" in filenames
+ data_dir_time = os.path.join(data_dir_date, time_str)
+ assert os.path.isdir(data_dir_time)
+ assert os.path.exists(os.path.join(data_dir_time, "new_york.jpg"))
+
+ shutil.rmtree(data_dir)
+
+ def test_receive_mode_files_no_message(self, temp_dir, common_obj):
+ web = web_obj(temp_dir, common_obj, "receive")
+
+ data_dir = os.path.join(temp_dir, "OnionShare")
+ os.makedirs(data_dir, exist_ok=True)
+
+ web.settings.set("receive", "data_dir", data_dir)
+
+ with web.app.test_client() as c:
+ res = c.post(
+ "/upload-ajax",
+ buffered=True,
+ content_type="multipart/form-data",
+ data={"file[]": (BytesIO(b"THIS IS A TEST FILE"), "new_york.jpg")},
+ headers=self._make_auth_headers(web.password),
+ )
+ content = res.get_data()
+ assert res.status_code == 200
+ assert b"Uploaded new_york.jpg" in content
+
+ # Date folder should have just a time folder with new_york.jpg
+ data_dir_date = os.path.join(data_dir, os.listdir(data_dir)[0])
+ filenames = os.listdir(data_dir_date)
+ assert len(filenames) == 1
+ time_str = filenames[0][0:6]
+ assert time_str in filenames
+ assert f"{time_str}-message.txt" not in filenames
+ data_dir_time = os.path.join(data_dir_date, time_str)
+ assert os.path.isdir(data_dir_time)
+ assert os.path.exists(os.path.join(data_dir_time, "new_york.jpg"))
+
+ shutil.rmtree(data_dir)
+
+ def test_receive_mode_no_message_no_files(self, temp_dir, common_obj):
+ web = web_obj(temp_dir, common_obj, "receive")
+
+ data_dir = os.path.join(temp_dir, "OnionShare")
+ os.makedirs(data_dir, exist_ok=True)
+
+ web.settings.set("receive", "data_dir", data_dir)
+
+ with web.app.test_client() as c:
+ res = c.post(
+ "/upload-ajax",
+ buffered=True,
+ content_type="multipart/form-data",
+ data={},
+ headers=self._make_auth_headers(web.password),
+ )
+ content = res.get_data()
+ assert res.status_code == 200
+ assert b"Nothing submitted" in content
+
+ # Date folder should be empty
+ data_dir_date = os.path.join(data_dir, os.listdir(data_dir)[0])
+ filenames = os.listdir(data_dir_date)
+ assert len(filenames) == 0
+
+ shutil.rmtree(data_dir)
def test_public_mode_on(self, temp_dir, common_obj):
web = web_obj(temp_dir, common_obj, "receive")
@@ -192,7 +322,7 @@ class TestWeb:
with web.app.test_client() as c:
# Loading / should work without auth
res = c.get("/")
- data1 = res.get_data()
+ res.get_data()
assert res.status_code == 200
def test_public_mode_off(self, temp_dir, common_obj):
@@ -486,6 +616,7 @@ class TestRangeRequests:
h.add("Authorization", "Basic " + auth)
return h
+ @pytest.mark.skipif(sys.platform != "Linux", reason="requires Linux")
@check_unsupported("curl", ["--version"])
def test_curl(self, temp_dir, tmpdir, common_obj):
web = web_obj(temp_dir, common_obj, "share", 3)
@@ -510,6 +641,7 @@ class TestRangeRequests:
]
)
+ @pytest.mark.skipif(sys.platform != "Linux", reason="requires Linux")
@check_unsupported("wget", ["--version"])
def test_wget(self, temp_dir, tmpdir, common_obj):
web = web_obj(temp_dir, common_obj, "share", 3)
@@ -533,6 +665,7 @@ class TestRangeRequests:
]
)
+ @pytest.mark.skipif(sys.platform != "Linux", reason="requires Linux")
@check_unsupported("http", ["--version"])
def test_httpie(self, temp_dir, common_obj):
web = web_obj(temp_dir, common_obj, "share", 3)
diff --git a/cli/tests/test_range_request.py b/cli/tests/test_range_request.py
index 796bd6c3..83bc9c9f 100644
--- a/cli/tests/test_range_request.py
+++ b/cli/tests/test_range_request.py
@@ -1,7 +1,4 @@
import pytest
-import subprocess
-
-from tempfile import NamedTemporaryFile
from werkzeug.exceptions import RequestedRangeNotSatisfiable
from onionshare_cli.web.share_mode import parse_range_header
@@ -9,24 +6,24 @@ from onionshare_cli.web.share_mode import parse_range_header
VALID_RANGES = [
(None, 500, [(0, 499)]),
- ('bytes=0', 500, [(0, 499)]),
- ('bytes=100', 500, [(100, 499)]),
- ('bytes=100-', 500, [(100, 499)]), # not in the RFC, but how curl sends
- ('bytes=0-99', 500, [(0, 99)]),
- ('bytes=0-599', 500, [(0, 499)]),
- ('bytes=0-0', 500, [(0, 0)]),
- ('bytes=-100', 500, [(400, 499)]),
- ('bytes=0-99,100-199', 500, [(0, 199)]),
- ('bytes=0-100,100-199', 500, [(0, 199)]),
- ('bytes=0-99,101-199', 500, [(0, 99), (101, 199)]),
- ('bytes=0-199,100-299', 500, [(0, 299)]),
- ('bytes=0-99,200-299', 500, [(0, 99), (200, 299)]),
+ ("bytes=0", 500, [(0, 499)]),
+ ("bytes=100", 500, [(100, 499)]),
+ ("bytes=100-", 500, [(100, 499)]), # not in the RFC, but how curl sends
+ ("bytes=0-99", 500, [(0, 99)]),
+ ("bytes=0-599", 500, [(0, 499)]),
+ ("bytes=0-0", 500, [(0, 0)]),
+ ("bytes=-100", 500, [(400, 499)]),
+ ("bytes=0-99,100-199", 500, [(0, 199)]),
+ ("bytes=0-100,100-199", 500, [(0, 199)]),
+ ("bytes=0-99,101-199", 500, [(0, 99), (101, 199)]),
+ ("bytes=0-199,100-299", 500, [(0, 299)]),
+ ("bytes=0-99,200-299", 500, [(0, 99), (200, 299)]),
]
INVALID_RANGES = [
- 'bytes=200-100',
- 'bytes=0-100,300-200',
+ "bytes=200-100",
+ "bytes=0-100,300-200",
]
@@ -38,4 +35,4 @@ def test_parse_ranges():
for invalid in INVALID_RANGES:
with pytest.raises(RequestedRangeNotSatisfiable):
- parse_range_header(invalid, 500) \ No newline at end of file
+ parse_range_header(invalid, 500)
diff --git a/desktop/package/linux/build-appimage.py b/desktop/package/linux/build-appimage.py
index 0c64494f..dfaba1b4 100755
--- a/desktop/package/linux/build-appimage.py
+++ b/desktop/package/linux/build-appimage.py
@@ -43,4 +43,4 @@ def main():
if __name__ == "__main__":
- main() \ No newline at end of file
+ main()
diff --git a/desktop/package/windows/build.py b/desktop/package/windows/build.py
index 603b1514..5fc796d4 100644
--- a/desktop/package/windows/build.py
+++ b/desktop/package/windows/build.py
@@ -211,7 +211,7 @@ def main():
]
print(f"○ Created unsigned installer: {msi_filename}")
- print(f"○ Signing installer")
+ print("○ Signing installer")
run(
[
"signtool.exe",
diff --git a/desktop/scripts/get-tor-osx.py b/desktop/scripts/get-tor-osx.py
index f3aa6e7b..76c7a5fe 100755
--- a/desktop/scripts/get-tor-osx.py
+++ b/desktop/scripts/get-tor-osx.py
@@ -24,7 +24,6 @@ This script downloads a pre-built tor binary to bundle with OnionShare.
In order to avoid a Mac gnupg dependency, I manually verify the signature
and hard-code the sha256 hash.
"""
-
import inspect
import os
import sys
diff --git a/desktop/scripts/get-tor-windows.py b/desktop/scripts/get-tor-windows.py
index 9cb8898c..87080c4a 100644
--- a/desktop/scripts/get-tor-windows.py
+++ b/desktop/scripts/get-tor-windows.py
@@ -23,7 +23,6 @@ This script downloads a pre-built tor binary to bundle with OnionShare.
In order to avoid a Windows gnupg dependency, I manually verify the signature
and hard-code the sha256 hash.
"""
-
import inspect
import os
import sys
@@ -75,7 +74,7 @@ def main():
"e",
"-y",
exe_path,
- "Browser\TorBrowser\Tor",
+ "Browser\\TorBrowser\\Tor",
"-o%s" % os.path.join(working_path, "Tor"),
]
).wait()
@@ -85,7 +84,7 @@ def main():
"e",
"-y",
exe_path,
- "Browser\TorBrowser\Data\Tor\geoip*",
+ "Browser\\TorBrowser\\Data\\Tor\\geoip*",
"-o%s" % os.path.join(working_path, "Data"),
]
).wait()
diff --git a/desktop/src/onionshare/__init__.py b/desktop/src/onionshare/__init__.py
index 1c69ffa5..8276dae4 100644
--- a/desktop/src/onionshare/__init__.py
+++ b/desktop/src/onionshare/__init__.py
@@ -21,7 +21,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
import os
import sys
-import platform
import argparse
import signal
import json
diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py
index 7703f7a8..1a44a128 100644
--- a/desktop/src/onionshare/gui_common.py
+++ b/desktop/src/onionshare/gui_common.py
@@ -353,6 +353,10 @@ class GuiCommon:
color: #666666;
font-size: 11px;
}""",
+ "receive_message_button": """
+ QPushButton {
+ padding: 5px 10px;
+ }""",
# Settings dialog
"settings_version": """
QLabel {
diff --git a/desktop/src/onionshare/resources/images/open_message.png b/desktop/src/onionshare/resources/images/open_message.png
new file mode 100644
index 00000000..6712ecf9
--- /dev/null
+++ b/desktop/src/onionshare/resources/images/open_message.png
Binary files differ
diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json
index 7c74736a..8a69142c 100644
--- a/desktop/src/onionshare/resources/locale/en.json
+++ b/desktop/src/onionshare/resources/locale/en.json
@@ -175,6 +175,8 @@
"mode_settings_share_autostop_sharing_checkbox": "Stop sharing after files have been sent (uncheck to allow downloading individual files)",
"mode_settings_receive_data_dir_label": "Save files to",
"mode_settings_receive_data_dir_browse_button": "Browse",
+ "mode_settings_receive_disable_text_checkbox": "Disable submitting text",
+ "mode_settings_receive_disable_files_checkbox": "Disable uploading files",
"mode_settings_receive_webhook_url_checkbox": "Use notification webhook",
"mode_settings_website_disable_csp_checkbox": "Don't send Content Security Policy header (allows your website to use third-party resources)",
"gui_all_modes_transfer_finished_range": "Transferred {} - {}",
@@ -194,5 +196,6 @@
"gui_rendezvous_cleanup": "Waiting for Tor circuits to close to be sure your files have successfully transferred.\n\nThis might take a few minutes.",
"gui_rendezvous_cleanup_quit_early": "Quit Early",
"error_port_not_available": "OnionShare port not available",
+ "history_receive_read_message_button": "Read Message",
"error_tor_protocol_error": "There was an error with Tor: {}"
-}
+} \ No newline at end of file
diff --git a/desktop/src/onionshare/tab/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py
index 0cbccc51..567dc123 100644
--- a/desktop/src/onionshare/tab/mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/__init__.py
@@ -18,7 +18,7 @@ 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 PySide2 import QtCore, QtWidgets, QtGui
+from PySide2 import QtCore, QtWidgets
from onionshare_cli.common import AutoStopTimer
@@ -379,7 +379,7 @@ class Mode(QtWidgets.QWidget):
if self.server_status.status != ServerStatus.STATUS_STOPPED:
try:
self.web.stop(self.app.port)
- except:
+ except Exception:
# Probably we had no port to begin with (Onion service didn't start)
pass
self.app.cleanup()
@@ -447,12 +447,24 @@ class Mode(QtWidgets.QWidget):
"""
pass
+ def handle_request_upload_includes_message(self, event):
+ """
+ Handle REQUEST_UPLOAD_INCLUDES_MESSAGE event.
+ """
+ pass
+
def handle_request_upload_file_renamed(self, event):
"""
Handle REQUEST_UPLOAD_FILE_RENAMED event.
"""
pass
+ def handle_request_upload_message(self, event):
+ """
+ Handle REQUEST_UPLOAD_MESSAGE event.
+ """
+ pass
+
def handle_request_upload_set_dir(self, event):
"""
Handle REQUEST_UPLOAD_SET_DIR event.
diff --git a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py
index 44a6d240..7f32aebb 100644
--- a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py
@@ -18,14 +18,8 @@ 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 random
-import string
-
from PySide2 import QtCore, QtWidgets, QtGui
-from onionshare_cli.onion import *
-from onionshare_cli.common import Common
from onionshare_cli.web import Web
from .. import Mode
diff --git a/desktop/src/onionshare/tab/mode/history.py b/desktop/src/onionshare/tab/mode/history.py
index 45e37c75..795b0cd9 100644
--- a/desktop/src/onionshare/tab/mode/history.py
+++ b/desktop/src/onionshare/tab/mode/history.py
@@ -253,7 +253,7 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
try:
# If nautilus is available, open it
subprocess.Popen(["xdg-open", self.dir])
- except:
+ except Exception:
Alert(
self.common,
strings._("gui_open_folder_error").format(abs_filename),
@@ -268,6 +268,60 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
subprocess.Popen(["explorer", f"/select,{abs_filename}"])
+class ReceiveHistoryItemMessage(QtWidgets.QWidget):
+ def __init__(
+ self,
+ common,
+ ):
+ super(ReceiveHistoryItemMessage, self).__init__()
+ self.common = common
+ self.filename = None
+
+ # Read message button
+ message_pixmap = QtGui.QPixmap.fromImage(
+ QtGui.QImage(GuiCommon.get_resource_path("images/open_message.png"))
+ )
+ message_icon = QtGui.QIcon(message_pixmap)
+ self.message_button = QtWidgets.QPushButton(
+ strings._("history_receive_read_message_button")
+ )
+ self.message_button.setStyleSheet(self.common.gui.css["receive_message_button"])
+ self.message_button.clicked.connect(self.open_message)
+ self.message_button.setIcon(message_icon)
+ self.message_button.setIconSize(message_pixmap.rect().size())
+
+ # Layouts
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(self.message_button)
+ layout.addStretch()
+ self.setLayout(layout)
+
+ self.hide()
+
+ def set_filename(self, new_filename):
+ self.filename = new_filename
+ self.show()
+
+ def open_message(self):
+ """
+ Open the message in the operating system's default text editor
+ """
+ self.common.log("ReceiveHistoryItemMessage", "open_message", self.filename)
+
+ # Linux
+ if self.common.platform == "Linux" or self.common.platform == "BSD":
+ # If nautilus is available, open it
+ subprocess.Popen(["xdg-open", self.filename])
+
+ # macOS
+ elif self.common.platform == "Darwin":
+ subprocess.call(["open", self.filename])
+
+ # Windows
+ elif self.common.platform == "Windows":
+ subprocess.Popen(["notepad", self.filename])
+
+
class ReceiveHistoryItem(HistoryItem):
def __init__(self, common, id, content_length):
super(ReceiveHistoryItem, self).__init__()
@@ -277,6 +331,12 @@ class ReceiveHistoryItem(HistoryItem):
self.started = datetime.now()
self.status = HistoryItem.STATUS_STARTED
+ self.common.log(
+ "ReceiveHistoryItem",
+ "__init__",
+ f"id={self.id} content_length={self.content_length}",
+ )
+
# Label
self.label = QtWidgets.QLabel(
strings._("gui_all_modes_transfer_started").format(
@@ -295,6 +355,9 @@ class ReceiveHistoryItem(HistoryItem):
self.common.gui.css["downloads_uploads_progress_bar"]
)
+ # The message widget, if a message was included
+ self.message = ReceiveHistoryItemMessage(self.common)
+
# This layout contains file widgets
self.files_layout = QtWidgets.QVBoxLayout()
self.files_layout.setContentsMargins(0, 0, 0, 0)
@@ -305,6 +368,7 @@ class ReceiveHistoryItem(HistoryItem):
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
+ layout.addWidget(self.message)
layout.addWidget(self.progress_bar)
layout.addWidget(files_widget)
layout.addStretch()
@@ -313,6 +377,9 @@ class ReceiveHistoryItem(HistoryItem):
# We're also making a dictionary of file widgets, to make them easier to access
self.files = {}
+ def includes_message(self, message_filename):
+ self.message.set_filename(message_filename)
+
def update(self, data):
"""
Using the progress from Web, update the progress bar and file size labels
@@ -560,6 +627,13 @@ class HistoryItemList(QtWidgets.QScrollArea):
if id in self.items:
self.items[id].cancel()
+ def includes_message(self, id, message_filename):
+ """
+ Show message button for receive mode
+ """
+ if id in self.items:
+ self.items[id].includes_message(message_filename)
+
def reset(self):
"""
Reset all items, emptying the list. Override this method.
@@ -653,7 +727,7 @@ class History(QtWidgets.QWidget):
"""
Add a new item.
"""
- self.common.log("History", "add", f"id: {id}, item: {item}")
+ self.common.log("History", "add", f"id: {id}")
# Hide empty, show not empty
self.empty.hide()
@@ -674,6 +748,12 @@ class History(QtWidgets.QWidget):
"""
self.item_list.cancel(id)
+ def includes_message(self, id, message_filename):
+ """
+ Show the message button
+ """
+ self.item_list.includes_message(id, message_filename)
+
def reset(self):
"""
Reset all items.
diff --git a/desktop/src/onionshare/tab/mode/mode_settings_widget.py b/desktop/src/onionshare/tab/mode/mode_settings_widget.py
index ef59f37e..98a6a01a 100644
--- a/desktop/src/onionshare/tab/mode/mode_settings_widget.py
+++ b/desktop/src/onionshare/tab/mode/mode_settings_widget.py
@@ -234,7 +234,7 @@ class ModeSettingsWidget(QtWidgets.QWidget):
self.tab.change_title.emit(
self.tab.tab_id, strings._("gui_tab_name_chat")
)
- elif self.tab_mode == None:
+ elif self.tab_mode is None:
pass
else:
title = self.title_lineedit.text()
diff --git a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py
index b309d476..4dd2980c 100644
--- a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py
@@ -78,6 +78,25 @@ class ReceiveMode(Mode):
data_dir_layout.addWidget(data_dir_button)
self.mode_settings_widget.mode_specific_layout.addLayout(data_dir_layout)
+ # Disable text or files
+ self.disable_text_checkbox = self.settings.get("receive", "disable_files")
+ self.disable_text_checkbox = QtWidgets.QCheckBox()
+ self.disable_text_checkbox.clicked.connect(self.disable_text_checkbox_clicked)
+ self.disable_text_checkbox.setText(
+ strings._("mode_settings_receive_disable_text_checkbox")
+ )
+ self.disable_files_checkbox = self.settings.get("receive", "disable_files")
+ self.disable_files_checkbox = QtWidgets.QCheckBox()
+ self.disable_files_checkbox.clicked.connect(self.disable_files_checkbox_clicked)
+ self.disable_files_checkbox.setText(
+ strings._("mode_settings_receive_disable_files_checkbox")
+ )
+ disable_layout = QtWidgets.QHBoxLayout()
+ disable_layout.addWidget(self.disable_text_checkbox)
+ disable_layout.addWidget(self.disable_files_checkbox)
+ disable_layout.addStretch()
+ self.mode_settings_widget.mode_specific_layout.addLayout(disable_layout)
+
# Webhook URL
webhook_url = self.settings.get("receive", "webhook_url")
self.webhook_url_checkbox = QtWidgets.QCheckBox()
@@ -217,6 +236,16 @@ class ReceiveMode(Mode):
self.data_dir_lineedit.setText(selected_dir)
self.settings.set("receive", "data_dir", selected_dir)
+ def disable_text_checkbox_clicked(self):
+ self.settings.set(
+ "receive", "disable_text", self.disable_text_checkbox.isChecked()
+ )
+
+ def disable_files_checkbox_clicked(self):
+ self.settings.set(
+ "receive", "disable_files", self.disable_files_checkbox.isChecked()
+ )
+
def webhook_url_checkbox_clicked(self):
if self.webhook_url_checkbox.isChecked():
if self.settings.get("receive", "webhook_url"):
@@ -312,8 +341,11 @@ class ReceiveMode(Mode):
Handle REQUEST_STARTED event.
"""
item = ReceiveHistoryItem(
- self.common, event["data"]["id"], event["data"]["content_length"]
+ self.common,
+ event["data"]["id"],
+ event["data"]["content_length"],
)
+
self.history.add(event["data"]["id"], item)
self.toggle_history.update_indicator(True)
self.history.in_progress_count += 1
@@ -333,6 +365,12 @@ class ReceiveMode(Mode):
{"action": "progress", "progress": event["data"]["progress"]},
)
+ def handle_request_upload_includes_message(self, event):
+ """
+ Handle REQUEST_UPLOAD_INCLUDES_MESSAGE event.
+ """
+ self.history.includes_message(event["data"]["id"], event["data"]["filename"])
+
def handle_request_upload_file_renamed(self, event):
"""
Handle REQUEST_UPLOAD_FILE_RENAMED event.
diff --git a/desktop/src/onionshare/tab/mode/share_mode/__init__.py b/desktop/src/onionshare/tab/mode/share_mode/__init__.py
index 05038e20..676d34af 100644
--- a/desktop/src/onionshare/tab/mode/share_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/share_mode/__init__.py
@@ -21,7 +21,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from PySide2 import QtCore, QtWidgets, QtGui
-from onionshare_cli.onion import *
from onionshare_cli.common import Common
from onionshare_cli.web import Web
@@ -30,7 +29,7 @@ from .. import Mode
from ..file_selection import FileSelection
from ..history import History, ToggleHistory, ShareHistoryItem
from .... import strings
-from ....widgets import Alert, MinimumWidthWidget
+from ....widgets import MinimumWidthWidget
from ....gui_common import GuiCommon
diff --git a/desktop/src/onionshare/tab/mode/share_mode/threads.py b/desktop/src/onionshare/tab/mode/share_mode/threads.py
index 6f5c44ea..74d5099a 100644
--- a/desktop/src/onionshare/tab/mode/share_mode/threads.py
+++ b/desktop/src/onionshare/tab/mode/share_mode/threads.py
@@ -36,7 +36,7 @@ class CompressThread(QtCore.QThread):
# prepare files to share
def set_processed_size(self, x):
- if self.mode._zip_progress_bar != None:
+ if self.mode._zip_progress_bar is not None:
self.mode._zip_progress_bar.update_processed_size_signal.emit(x)
def run(self):
diff --git a/desktop/src/onionshare/tab/mode/website_mode/__init__.py b/desktop/src/onionshare/tab/mode/website_mode/__init__.py
index f7cfa758..10caff51 100644
--- a/desktop/src/onionshare/tab/mode/website_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/website_mode/__init__.py
@@ -19,12 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
-import random
-import string
from PySide2 import QtCore, QtWidgets, QtGui
-from onionshare_cli.onion import *
from onionshare_cli.common import Common
from onionshare_cli.web import Web
@@ -32,7 +29,7 @@ from .. import Mode
from ..file_selection import FileSelection
from ..history import History, ToggleHistory
from .... import strings
-from ....widgets import Alert, MinimumWidthWidget
+from ....widgets import MinimumWidthWidget
from ....gui_common import GuiCommon
diff --git a/desktop/src/onionshare/tab/server_status.py b/desktop/src/onionshare/tab/server_status.py
index d8266820..7ca1af09 100644
--- a/desktop/src/onionshare/tab/server_status.py
+++ b/desktop/src/onionshare/tab/server_status.py
@@ -161,7 +161,7 @@ class ServerStatus(QtWidgets.QWidget):
self.url.setText(wrapped_onion_url)
else:
self.url.setText(self.get_url())
- except:
+ except Exception:
pass
def show_url(self):
diff --git a/desktop/src/onionshare/tab/tab.py b/desktop/src/onionshare/tab/tab.py
index 2d4e164c..3d88ded5 100644
--- a/desktop/src/onionshare/tab/tab.py
+++ b/desktop/src/onionshare/tab/tab.py
@@ -540,6 +540,9 @@ class Tab(QtWidgets.QWidget):
elif event["type"] == Web.REQUEST_CANCELED:
mode.handle_request_canceled(event)
+ elif event["type"] == Web.REQUEST_UPLOAD_INCLUDES_MESSAGE:
+ mode.handle_request_upload_includes_message(event)
+
elif event["type"] == Web.REQUEST_UPLOAD_FILE_RENAMED:
mode.handle_request_upload_file_renamed(event)
diff --git a/desktop/src/onionshare/threads.py b/desktop/src/onionshare/threads.py
index 504591f6..c9a3dba4 100644
--- a/desktop/src/onionshare/threads.py
+++ b/desktop/src/onionshare/threads.py
@@ -252,7 +252,7 @@ class EventHandlerThread(QtCore.QThread):
"EventHandler", "run", f"invalid event type: {obj}"
)
- except:
+ except Exception:
pass
if self.should_quit:
diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection_dialog.py
index c3644f8b..b5c2f61c 100644
--- a/desktop/src/onionshare/tor_connection_dialog.py
+++ b/desktop/src/onionshare/tor_connection_dialog.py
@@ -166,7 +166,7 @@ class TorConnectionThread(QtCore.QThread):
else:
self.canceled_connecting_to_tor.emit()
- except BundledTorCanceled as e:
+ except BundledTorCanceled:
self.common.log(
"TorConnectionThread", "run", "caught exception: BundledTorCanceled"
)
diff --git a/desktop/src/onionshare/update_checker.py b/desktop/src/onionshare/update_checker.py
index 43c83828..08755e6e 100644
--- a/desktop/src/onionshare/update_checker.py
+++ b/desktop/src/onionshare/update_checker.py
@@ -19,7 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from PySide2 import QtCore
-import datetime, re
+import datetime
+import re
import socks
from distutils.version import LooseVersion as Version
diff --git a/desktop/src/setup.py b/desktop/src/setup.py
index c2c91d28..82a5f98a 100644
--- a/desktop/src/setup.py
+++ b/desktop/src/setup.py
@@ -25,7 +25,12 @@ version = "2.3.1"
setuptools.setup(
name="onionshare",
version=version,
- description="OnionShare lets you securely and anonymously send and receive files. It works by starting a web server, making it accessible as a Tor onion service, and generating an unguessable web address so others can download files from you, or upload files to you. It does _not_ require setting up a separate server or using a third party file-sharing service.",
+ description=(
+ "OnionShare lets you securely and anonymously send and receive files. It works by starting a web "
+ "server, making it accessible as a Tor onion service, and generating an unguessable web address so "
+ "others can download files from you, or upload files to you. It does _not_ require setting up a "
+ "separate server or using a third party file-sharing service."
+ ),
author="Micah Lee",
author_email="micah@micahflee.com",
maintainer="Micah Lee",
diff --git a/desktop/tests/conftest.py b/desktop/tests/conftest.py
index a043af0c..b17aa45c 100644
--- a/desktop/tests/conftest.py
+++ b/desktop/tests/conftest.py
@@ -1,20 +1,21 @@
import sys
-
-# Force tests to look for resources in the source code tree
-sys.onionshare_dev_mode = True
-
-# Let OnionShare know the tests are running, to avoid colliding with settings files
-sys.onionshare_test_mode = True
-
import os
import shutil
import tempfile
from datetime import datetime, timedelta
import pytest
-
from PySide2 import QtTest
+from onionshare_cli import common, web, settings
+
+
+# Force tests to look for resources in the source code tree
+sys.onionshare_dev_mode = True
+
+# Let OnionShare know the tests are running, to avoid colliding with settings files
+sys.onionshare_test_mode = True
+
@staticmethod
def qWait(t, qtapp):
@@ -36,9 +37,6 @@ sys.path.insert(
),
)
-from onionshare_cli import common, web, settings
-
-
# The temporary directory for CLI tests
test_temp_dir = None
@@ -69,7 +67,7 @@ def temp_dir():
@pytest.fixture
def temp_dir_1024(temp_dir):
- """ Create a temporary directory that has a single file of a
+ """Create a temporary directory that has a single file of a
particular size (1024 bytes).
"""
@@ -83,7 +81,7 @@ def temp_dir_1024(temp_dir):
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture
def temp_dir_1024_delete(temp_dir):
- """ Create a temporary directory that has a single file of a
+ """Create a temporary directory that has a single file of a
particular size (1024 bytes). The temporary directory (including
the file inside) will be deleted after fixture usage.
"""
@@ -97,7 +95,7 @@ def temp_dir_1024_delete(temp_dir):
@pytest.fixture
def temp_file_1024(temp_dir):
- """ Create a temporary file of a particular size (1024 bytes). """
+ """Create a temporary file of a particular size (1024 bytes)."""
with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir) as tmp_file:
tmp_file.write(b"*" * 1024)
@@ -141,7 +139,7 @@ def default_zw():
tmp_dir = os.path.dirname(zw.zip_filename)
try:
shutil.rmtree(tmp_dir, ignore_errors=True)
- except:
+ except Exception:
pass
diff --git a/desktop/tests/test_gui_receive.py b/desktop/tests/test_gui_receive.py
index 848b2f11..6e14ae67 100644
--- a/desktop/tests/test_gui_receive.py
+++ b/desktop/tests/test_gui_receive.py
@@ -48,15 +48,15 @@ class TestReceive(GuiBaseTest):
QtTest.QTest.qWait(1000, self.gui.qtapp)
- # Make sure the file is within the last 10 seconds worth of fileames
+ # Make sure the file is within the last 10 seconds worth of filenames
exists = False
now = datetime.now()
for _ in range(10):
date_dir = now.strftime("%Y-%m-%d")
if identical_files_at_once:
- time_dir = now.strftime("%H.%M.%S-1")
+ time_dir = now.strftime("%H%M%S-1")
else:
- time_dir = now.strftime("%H.%M.%S")
+ time_dir = now.strftime("%H%M%S")
receive_mode_dir = os.path.join(
tab.settings.get("receive", "data_dir"), date_dir, time_dir
)
@@ -93,6 +93,47 @@ class TestReceive(GuiBaseTest):
QtCore.QTimer.singleShot(1000, accept_dialog)
self.assertTrue("Error uploading, please inform the OnionShare user" in r.text)
+ def submit_message(self, tab, message):
+ """Test that we can submit a message"""
+
+ # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused
+ QtTest.QTest.qWait(2000, self.gui.qtapp)
+
+ url = f"http://127.0.0.1:{tab.app.port}/upload"
+ if tab.settings.get("general", "public"):
+ requests.post(url, data={"text": message})
+ else:
+ requests.post(
+ url,
+ data={"text": message},
+ auth=requests.auth.HTTPBasicAuth(
+ "onionshare", tab.get_mode().web.password
+ ),
+ )
+
+ QtTest.QTest.qWait(1000, self.gui.qtapp)
+
+ # Make sure the file is within the last 10 seconds worth of filenames
+ exists = False
+ now = datetime.now()
+ for _ in range(10):
+ date_dir = now.strftime("%Y-%m-%d")
+ time_dir = now.strftime("%H%M%S")
+ expected_filename = os.path.join(
+ tab.settings.get("receive", "data_dir"),
+ date_dir,
+ f"{time_dir}-message.txt",
+ )
+ if os.path.exists(expected_filename):
+ with open(expected_filename) as f:
+ assert f.read() == message
+
+ exists = True
+ break
+ now = now - timedelta(seconds=1)
+
+ self.assertTrue(exists)
+
def try_without_auth_in_non_public_mode(self, tab):
r = requests.post(f"http://127.0.0.1:{tab.app.port}/upload")
self.assertEqual(r.status_code, 401)
@@ -115,10 +156,9 @@ class TestReceive(GuiBaseTest):
self.have_copy_url_button(tab)
self.have_show_qr_code_button(tab)
self.server_status_indicator_says_started(tab)
- self.web_page(tab, "Select the files you want to send, then click")
def run_all_receive_mode_tests(self, tab):
- """Upload files in receive mode and stop the share"""
+ """Submit files and messages in receive mode and stop the share"""
self.run_all_receive_mode_setup_tests(tab)
if not tab.settings.get("general", "public"):
self.try_without_auth_in_non_public_mode(tab)
@@ -131,9 +171,11 @@ class TestReceive(GuiBaseTest):
self.counter_incremented(tab, 3)
self.upload_file(tab, self.tmpfile_test2, "test2.txt")
self.counter_incremented(tab, 4)
+ self.submit_message(tab, "onionshare is an interesting piece of software")
+ self.counter_incremented(tab, 5)
# Test uploading the same file twice at the same time, and make sure no collisions
self.upload_file(tab, self.tmpfile_test, "test.txt", True)
- self.counter_incremented(tab, 6)
+ self.counter_incremented(tab, 7)
self.history_indicator(tab, "2")
self.server_is_stopped(tab)
self.web_server_is_stopped(tab)