Merge pull request #7 from micahflee/weblate

Changes from micahflee/onionshare
This commit is contained in:
Micah Lee 2019-09-20 18:50:25 -07:00 committed by GitHub
commit 821f465286
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 3368 additions and 502 deletions

View File

@ -14,7 +14,7 @@ Install the needed dependencies:
For Debian-like distros: For Debian-like distros:
``` ```
apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils
``` ```
For Fedora-like distros: For Fedora-like distros:
@ -46,11 +46,11 @@ If you find that these instructions don't work for your Linux distribution or ve
Install Xcode from the Mac App Store. Once it's installed, run it for the first time to set it up. Also, run this to make sure command line tools are installed: `xcode-select --install`. And finally, open Xcode, go to Preferences > Locations, and make sure under Command Line Tools you select an installed version from the dropdown. (This is required for installing Qt5.) Install Xcode from the Mac App Store. Once it's installed, run it for the first time to set it up. Also, run this to make sure command line tools are installed: `xcode-select --install`. And finally, open Xcode, go to Preferences > Locations, and make sure under Command Line Tools you select an installed version from the dropdown. (This is required for installing Qt5.)
Download and install Python 3.7.2 from https://www.python.org/downloads/release/python-372/. I downloaded `python-3.7.2-macosx10.9.pkg`. Download and install Python 3.7.4 from https://www.python.org/downloads/release/python-374/. I downloaded `python-3.7.4-macosx10.9.pkg`.
You may also need to run the command `/Applications/Python\ 3.7/Install\ Certificates.command` to update Python 3.6's internal certificate store. Otherwise, you may find that fetching the Tor Browser .dmg file fails later due to a certificate validation error. You may also need to run the command `/Applications/Python\ 3.7/Install\ Certificates.command` to update Python 3.6's internal certificate store. Otherwise, you may find that fetching the Tor Browser .dmg file fails later due to a certificate validation error.
Install Qt 5.12.1 from https://download.qt.io/archive/qt/5.12/5.12.1/. I downloaded `qt-opensource-mac-x64-5.12.1.dmg`. In the installer, you can skip making an account, and all you need is `Qt` > `Qt 5.12.1` > `macOS`. Install Qt 5.13.0 for macOS from https://www.qt.io/offline-installers. I downloaded `qt-opensource-mac-x64-5.13.0.dmg`. In the installer, you can skip making an account, and all you need is `Qt` > `Qt 5.13.0` > `macOS`.
Now install pip dependencies. If you want to use a virtualenv, create it and activate it first: Now install pip dependencies. If you want to use a virtualenv, create it and activate it first:
@ -72,48 +72,6 @@ pip3 install -r install/requirements.txt
./dev_scripts/onionshare-gui ./dev_scripts/onionshare-gui
``` ```
#### Building PyInstaller
If you want to build an app bundle, you'll need to use PyInstaller. Recently there has been issues with installing PyInstaller using pip, so here's how to build it from source. First, make sure you don't have PyInstaller currently installed:
```sh
pip3 uninstall PyInstaller
```
Change to a folder where you keep source code, and clone the PyInstaller git repo:
```sh
git clone https://github.com/pyinstaller/pyinstaller.git
```
Verify the v3.4 git tag:
```sh
cd pyinstaller
gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv-key 0xD4AD8B9C167B757C4F08E8777B752811BF773B65
git tag -v v3.4
```
It should say `Good signature from "Hartmut Goebel <h.goebel@goebel-consult.de>`. If it verified successfully, checkout the tag:
```sh
git checkout v3.4
```
And compile the bootloader, following [these instructions](https://pyinstaller.readthedocs.io/en/stable/bootloader-building.html#building-for-mac-os-x). To compile, run this:
```sh
cd bootloader
python3 waf distclean all --target-arch=64bit
```
Finally, install the PyInstaller module into your local site-packages. If you're using a virtualenv, make sure to run this last command while your virtualenv is activated:
```sh
cd ..
python3 setup.py install
```
#### To build the app bundle #### To build the app bundle
```sh ```sh
@ -134,7 +92,7 @@ Now you should have `dist/OnionShare.pkg`.
### Setting up your dev environment ### Setting up your dev environment
Download Python 3.7.2, 32-bit (x86) from https://www.python.org/downloads/release/python-372/. I downloaded `python-3.7.2.exe`. When installing it, make sure to check the "Add Python 3.7 to PATH" checkbox on the first page of the installer. Download Python 3.7.4, 32-bit (x86) from https://www.python.org/downloads/release/python-374/. I downloaded `python-3.7.4.exe`. When installing it, make sure to check the "Add Python 3.7 to PATH" checkbox on the first page of the installer.
Open a command prompt, cd to the onionshare folder, and install dependencies with pip: Open a command prompt, cd to the onionshare folder, and install dependencies with pip:
@ -142,7 +100,7 @@ Open a command prompt, cd to the onionshare folder, and install dependencies wit
pip install -r install\requirements.txt pip install -r install\requirements.txt
``` ```
Install the Qt 5.12.1 from https://download.qt.io/archive/qt/5.12/5.12.1/. I downloaded `qt-opensource-windows-x86-5.12.1.exe`. In the installer, you can skip making an account, and all you need `Qt` > `Qt 5.12.1` > `MSVC 2017 32-bit`. Install the Qt 5.13.0 from https://www.qt.io/download-open-source/. I downloaded `qt-opensource-windows-x86-5.13.0.exe`. In the installer, you can skip making an account, and all you need `Qt` > `Qt 5.13.0` > `MSVC 2017 32-bit`.
After that you can try both the CLI and the GUI version of OnionShare: After that you can try both the CLI and the GUI version of OnionShare:
@ -400,7 +358,7 @@ To publish the release:
- Create a new release on GitHub, put the changelog in the description of the release, and upload all six files (the macOS installer, the Windows installer, the source package, and their signatures) - Create a new release on GitHub, put the changelog in the description of the release, and upload all six files (the macOS installer, the Windows installer, the source package, and their signatures)
- Upload the six release files to https://onionshare.org/dist/$VERSION/ - Upload the six release files to https://onionshare.org/dist/$VERSION/
- Copy the six release files into the OnionShare team Keybase filesystem - Copy the six release files into the OnionShare team Keybase filesystem
- Update the [onionshare-website](https://github.com/micahflee/onionshare-website) repo: - Update the [onionshare-website](https://github.com/micahflee/onionshare-website) repo:
- Edit `latest-version.txt` to match the latest version - Edit `latest-version.txt` to match the latest version
- Update the version number and download links - Update the version number and download links

View File

@ -6,8 +6,8 @@ include share/images/*
include share/locale/* include share/locale/*
include share/templates/* include share/templates/*
include share/static/* include share/static/*
include install/onionshare.desktop include install/org.onionshare.OnionShare.desktop
include install/onionshare.appdata.xml include install/org.onionshare.OnionShare.appdata.xml
include install/onionshare80.xpm include install/org.onionshare.OnionShare.svg
include install/scripts/onionshare-nautilus.py include install/scripts/onionshare-nautilus.py
include tests/*.py include tests/*.py

View File

@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2018 Micah Lee <micah@micahflee.com> --> <!-- Copyright 2018 Micah Lee <micah@micahflee.com> -->
<component type="desktop"> <component type="desktop-application">
<id>onionshare.desktop</id> <id>org.onionshare.OnionShare</id>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license> <project_license>GPL-3.0</project_license>
<name>OnionShare</name> <name>OnionShare</name>
<summary>Securely and anonymously share a file of any size</summary> <summary>Securely and anonymously share a file of any size</summary>
<description> <description>
<p> <p>
OnionShare lets you securely and anonymously send and receive files. It works by starting a OnionShare lets you securely and anonymously send and receive files. It works by starting a web server,
web server, making it accessible as a Tor onion service, and generating an unguessable web making it accessible as a Tor onion service, and generating an unguessable web address so others can
address so others can download files from you, or upload files to you. It does <em>not</em> download files from you, or upload files to you. It does <em>not</em> require setting up a separate server
require setting up a separate server or using a third party file-sharing service. or using a third party file-sharing service.
</p> </p>
<p> <p>
If you want to send files to someone, OnionShare hosts them on your own computer and uses a Tor If you want to send files to someone, OnionShare hosts them on your own computer and uses a Tor onion
onion service to make them temporarily accessible over the internet. The receiving user just service to make them temporarily accessible over the internet. The receiving user just needs to open the web
needs to open the web address in Tor Browser to download the files. If you want to receive files, address in Tor Browser to download the files. If you want to receive files, OnionShare hosts an anonymous
OnionShare hosts an anonymous dropbox directly on your computer and uses a Tor onion service to dropbox directly on your computer and uses a Tor onion service to make it temporarily accessible over the
make it temporarily accessible over the internet. Other users can upload files to you from by internet. Other users can upload files to you from by loading the web address in Tor Browser.
loading the web address in Tor Browser.
</p> </p>
</description> </description>
<launchable type="desktop-id">org.onionshare.OnionShare.desktop</launchable>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://raw.githubusercontent.com/micahflee/onionshare/master/screenshots/appdata-onionshare-share-server.png</image> <image>https://raw.githubusercontent.com/micahflee/onionshare/master/screenshots/appdata-onionshare-share-server.png</image>
@ -40,6 +40,13 @@
<caption>Uploading files to OnionShare user using Tor Browser</caption> <caption>Uploading files to OnionShare user using Tor Browser</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>
<url type="bugtracker">https://github.com/micahflee/onionshare/issues/</url>
<url type="help">https://github.com/micahflee/onionshare/wiki/</url>
<url type="homepage">https://onionshare.org/</url> <url type="homepage">https://onionshare.org/</url>
<updatecontact>micah@micahflee.com</updatecontact> <developer_name>Micah Lee</developer_name>
<update_contact>micah@micahflee.com</update_contact>
<content_rating type="oars-1.1" />
<releases>
<release type="stable" date="2019-05-07" version="2.1" />
</releases>
</component> </component>

View File

@ -7,7 +7,7 @@ Comment[de]=Teile Dateien sicher und anonym über das Tor-Netzwerk
Exec=/usr/bin/onionshare-gui Exec=/usr/bin/onionshare-gui
Terminal=false Terminal=false
Type=Application Type=Application
Icon=onionshare80 Icon=org.onionshare.OnionShare
Categories=Network;FileTransfer; Categories=Network;FileTransfer;
Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting; Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting;
Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting; Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting;

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 157 KiB

View File

@ -1,10 +1,10 @@
atomicwrites==1.3.0 atomicwrites==1.3.0
attrs==19.1.0 attrs==19.1.0
more-itertools==5.0.0 more-itertools==7.2.0
pluggy==0.9.0 pluggy==0.12.0
py==1.8.0 py==1.8.0
pytest==4.4.1 pytest==5.1.2
pytest-faulthandler==1.5.0 pytest-faulthandler==2.0.1
pytest-qt==3.2.2 pytest-qt==3.2.2
six==1.12.0 six==1.12.0
urllib3==1.24.2 urllib3==1.25.3

View File

@ -1,9 +1,9 @@
altgraph==0.16.1 altgraph==0.16.1
certifi==2019.3.9 certifi==2019.6.16
chardet==3.0.4 chardet==3.0.4
Click==7.0 Click==7.0
Flask==1.0.2 Flask==1.1.1
Flask-HTTPAuth==3.2.4 Flask-HTTPAuth==3.3.0
future==0.17.1 future==0.17.1
idna==2.8 idna==2.8
itsdangerous==1.1.0 itsdangerous==1.1.0
@ -11,11 +11,12 @@ Jinja2==2.10.1
macholib==1.11 macholib==1.11
MarkupSafe==1.1.1 MarkupSafe==1.1.1
pefile==2019.4.18 pefile==2019.4.18
pycryptodome==3.8.1 pycryptodome==3.9.0
PyQt5==5.12.1 PyInstaller==3.5
PyQt5-sip==4.19.15 PyQt5==5.13.0
PySocks==1.6.8 PyQt5-sip==4.19.18
requests==2.21.0 PySocks==1.7.0
requests==2.22.0
stem==1.7.1 stem==1.7.1
urllib3==1.24.2 urllib3==1.25.3
Werkzeug==0.15.2 Werkzeug==0.15.5

View File

@ -88,13 +88,14 @@ def main(cwd=None):
else: else:
mode = 'share' mode = 'share'
# Make sure filenames given if not using receiver mode # In share an website mode, you must supply a list of filenames
if mode == 'share' and len(filenames) == 0: if mode == 'share' or mode == 'website':
parser.print_help() # Make sure filenames given if not using receiver mode
sys.exit() if len(filenames) == 0:
parser.print_help()
sys.exit()
# Validate filenames # Validate filenames
if mode == 'share':
valid = True valid = True
for filename in filenames: for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename): if not os.path.isfile(filename) and not os.path.isdir(filename):
@ -109,6 +110,8 @@ def main(cwd=None):
# Re-load settings, if a custom config was passed in # Re-load settings, if a custom config was passed in
if config: if config:
common.load_settings(config) common.load_settings(config)
else:
common.load_settings()
# Verbose mode? # Verbose mode?
common.verbose = verbose common.verbose = verbose
@ -260,12 +263,12 @@ def main(cwd=None):
if not app.autostop_timer_thread.is_alive(): if not app.autostop_timer_thread.is_alive():
if mode == 'share' or (mode == 'website'): if mode == 'share' or (mode == 'website'):
# If there were no attempts to download the share, or all downloads are done, we can stop # If there were no attempts to download the share, or all downloads are done, we can stop
if web.share_mode.download_count == 0 or web.done: if web.share_mode.cur_history_id == 0 or web.done:
print("Stopped because auto-stop timer ran out") print("Stopped because auto-stop timer ran out")
web.stop(app.port) web.stop(app.port)
break break
if mode == 'receive': if mode == 'receive':
if web.receive_mode.upload_count == 0 or not web.receive_mode.uploads_in_progress: if web.receive_mode.cur_history_id == 0 or not web.receive_mode.uploads_in_progress:
print("Stopped because auto-stop timer ran out") print("Stopped because auto-stop timer ran out")
web.stop(app.port) web.stop(app.port)
break break

View File

@ -41,7 +41,7 @@ class Common(object):
# The platform OnionShare is running on # The platform OnionShare is running on
self.platform = platform.system() self.platform = platform.system()
if self.platform.endswith('BSD'): if self.platform.endswith('BSD') or self.platform == 'DragonFly':
self.platform = 'BSD' self.platform = 'BSD'
# The current version of OnionShare # The current version of OnionShare
@ -203,7 +203,7 @@ class Common(object):
border: 0px; border: 0px;
}""", }""",
# Common styles between ShareMode and ReceiveMode and their child widgets # Common styles between modes and their child widgets
'mode_info_label': """ 'mode_info_label': """
QLabel { QLabel {
font-size: 12px; font-size: 12px;
@ -310,6 +310,21 @@ class Common(object):
width: 10px; width: 10px;
}""", }""",
'history_individual_file_timestamp_label': """
QLabel {
color: #666666;
}""",
'history_individual_file_status_code_label_2xx': """
QLabel {
color: #008800;
}""",
'history_individual_file_status_code_label_4xx': """
QLabel {
color: #cc0000;
}""",
# Share mode and child widget styles # Share mode and child widget styles
'share_zip_progess_bar': """ 'share_zip_progess_bar': """
QProgressBar { QProgressBar {

View File

@ -438,6 +438,10 @@ class Onion(object):
return the onion hostname. return the onion hostname.
""" """
self.common.log('Onion', 'start_onion_service') self.common.log('Onion', 'start_onion_service')
# Settings may have changed in the frontend but not updated in our settings object,
# such as persistence. Reload the settings now just to be sure.
self.settings.load()
self.auth_string = None self.auth_string = None
if not self.supports_ephemeral: if not self.supports_ephemeral:

View File

@ -8,7 +8,7 @@ from werkzeug.utils import secure_filename
from .. import strings from .. import strings
class ReceiveModeWeb(object): class ReceiveModeWeb:
""" """
All of the web logic for receive mode All of the web logic for receive mode
""" """
@ -18,13 +18,12 @@ class ReceiveModeWeb(object):
self.web = web self.web = web
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.can_upload = True self.can_upload = True
self.upload_count = 0
self.uploads_in_progress = [] self.uploads_in_progress = []
# This tracks the history id
self.cur_history_id = 0
self.define_routes() self.define_routes()
def define_routes(self): def define_routes(self):
@ -33,8 +32,15 @@ class ReceiveModeWeb(object):
""" """
@self.web.app.route("/") @self.web.app.route("/")
def index(): def index():
history_id = self.cur_history_id
self.cur_history_id += 1
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
'id': history_id,
'status_code': 200
})
self.web.add_request(self.web.REQUEST_LOAD, request.path) self.web.add_request(self.web.REQUEST_LOAD, request.path)
r = make_response(render_template('receive.html', r = make_response(render_template('receive.html',
static_url_path=self.web.static_url_path)) static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r) return self.web.add_security_headers(r)
@ -55,7 +61,7 @@ class ReceiveModeWeb(object):
# Tell the GUI the receive mode directory for this file # Tell the GUI the receive mode directory for this file
self.web.add_request(self.web.REQUEST_UPLOAD_SET_DIR, request.path, { self.web.add_request(self.web.REQUEST_UPLOAD_SET_DIR, request.path, {
'id': request.upload_id, 'id': request.history_id,
'filename': basename, 'filename': basename,
'dir': request.receive_mode_dir 'dir': request.receive_mode_dir
}) })
@ -275,10 +281,9 @@ class ReceiveModeRequest(Request):
# Prevent new uploads if we've said so (timer expired) # Prevent new uploads if we've said so (timer expired)
if self.web.receive_mode.can_upload: if self.web.receive_mode.can_upload:
# Create an upload_id, attach it to the request # Create an history_id, attach it to the request
self.upload_id = self.web.receive_mode.upload_count self.history_id = self.web.receive_mode.cur_history_id
self.web.receive_mode.cur_history_id += 1
self.web.receive_mode.upload_count += 1
# Figure out the content length # Figure out the content length
try: try:
@ -305,10 +310,10 @@ class ReceiveModeRequest(Request):
if not self.told_gui_about_request: if not self.told_gui_about_request:
# Tell the GUI about the request # Tell the GUI about the request
self.web.add_request(self.web.REQUEST_STARTED, self.path, { self.web.add_request(self.web.REQUEST_STARTED, self.path, {
'id': self.upload_id, 'id': self.history_id,
'content_length': self.content_length 'content_length': self.content_length
}) })
self.web.receive_mode.uploads_in_progress.append(self.upload_id) self.web.receive_mode.uploads_in_progress.append(self.history_id)
self.told_gui_about_request = True self.told_gui_about_request = True
@ -340,19 +345,19 @@ class ReceiveModeRequest(Request):
try: try:
if self.told_gui_about_request: if self.told_gui_about_request:
upload_id = self.upload_id 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 not self.progress[self.filename]['complete']:
# Inform the GUI that the upload has canceled # Inform the GUI that the upload has canceled
self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, { self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, {
'id': upload_id 'id': history_id
}) })
else: else:
# Inform the GUI that the upload has finished # Inform the GUI that the upload has finished
self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, {
'id': upload_id 'id': history_id
}) })
self.web.receive_mode.uploads_in_progress.remove(upload_id) self.web.receive_mode.uploads_in_progress.remove(history_id)
except AttributeError: except AttributeError:
pass pass
@ -378,7 +383,7 @@ class ReceiveModeRequest(Request):
# Update the GUI on the upload progress # Update the GUI on the upload progress
if self.told_gui_about_request: if self.told_gui_about_request:
self.web.add_request(self.web.REQUEST_PROGRESS, self.path, { self.web.add_request(self.web.REQUEST_PROGRESS, self.path, {
'id': self.upload_id, 'id': self.history_id,
'progress': self.progress 'progress': self.progress
}) })

View File

@ -0,0 +1,276 @@
import os
import sys
import tempfile
import mimetypes
import gzip
from flask import Response, request, render_template, make_response
from .. import strings
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 "Stop After First Download" is checked (stay_open == False), 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, '/{}'.format(path), {
'id': history_id,
'method': request.method,
'status_code': 200
})
breadcrumbs = [('', '/')]
parts = path.split('/')[:-1]
for i in range(len(parts)):
breadcrumbs.append(('{}'.format(parts[i]), '/{}/'.format('/'.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
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, path, {
'id': history_id,
'filesize': filesize
})
# Only GET requests are allowed, any other method should fail
if request.method != "GET":
return self.web.error405()
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

View File

@ -3,55 +3,35 @@ import sys
import tempfile import tempfile
import zipfile import zipfile
import mimetypes import mimetypes
import gzip
from flask import Response, request, render_template, make_response from flask import Response, request, render_template, make_response
from .send_base_mode import SendBaseModeWeb
from .. import strings from .. import strings
class ShareModeWeb(object): class ShareModeWeb(SendBaseModeWeb):
""" """
All of the web logic for share mode All of the web logic for share mode
""" """
def __init__(self, common, web): def init(self):
self.common = common self.common.log('ShareModeWeb', 'init')
self.common.log('ShareModeWeb', '__init__')
self.web = web # Allow downloading individual files if "Stop sharing after files have been sent" is unchecked
self.download_individual_files = not self.common.settings.get('close_after_first_download')
# Information about the file to be shared
self.file_info = []
self.is_zipped = False
self.download_filename = None
self.download_filesize = None
self.gzip_filename = None
self.gzip_filesize = None
self.zip_writer = None
self.download_count = 0
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
self.download_in_progress = False
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.define_routes()
def define_routes(self): def define_routes(self):
""" """
The web app routes for sharing files The web app routes for sharing files
""" """
@self.web.app.route("/") @self.web.app.route('/', defaults={'path': ''})
def index(): @self.web.app.route('/<path:path>')
def index(path):
""" """
Render the template for the onionshare landing page. Render the template for the onionshare landing page.
""" """
self.web.add_request(self.web.REQUEST_LOAD, request.path) self.web.add_request(self.web.REQUEST_LOAD, request.path)
# Deny new downloads if "Stop After First Download" is checked and there is # Deny new downloads if "Stop sharing after files have been sent" is checked and there is
# currently a download # currently a download
deny_download = not self.web.stay_open and self.download_in_progress deny_download = not self.web.stay_open and self.download_in_progress
if deny_download: if deny_download:
@ -65,15 +45,7 @@ class ShareModeWeb(object):
else: else:
self.filesize = self.download_filesize self.filesize = self.download_filesize
r = make_response(render_template( return self.render_logic(path)
'send.html',
file_info=self.file_info,
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))
return self.web.add_security_headers(r)
@self.web.app.route("/download") @self.web.app.route("/download")
def download(): def download():
@ -88,10 +60,6 @@ class ShareModeWeb(object):
static_url_path=self.web.static_url_path)) static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r) return self.web.add_security_headers(r)
# Each download has a unique id
download_id = self.download_count
self.download_count += 1
# Prepare some variables to use inside generate() function below # Prepare some variables to use inside generate() function below
# which is outside of the request context # which is outside of the request context
shutdown_func = request.environ.get('werkzeug.server.shutdown') shutdown_func = request.environ.get('werkzeug.server.shutdown')
@ -109,8 +77,10 @@ class ShareModeWeb(object):
self.filesize = self.download_filesize self.filesize = self.download_filesize
# Tell GUI the download started # 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, { self.web.add_request(self.web.REQUEST_STARTED, path, {
'id': download_id, 'id': history_id,
'use_gzip': use_gzip 'use_gzip': use_gzip
}) })
@ -130,7 +100,7 @@ class ShareModeWeb(object):
# The user has canceled the download, so stop serving the file # The user has canceled the download, so stop serving the file
if not self.web.stop_q.empty(): if not self.web.stop_q.empty():
self.web.add_request(self.web.REQUEST_CANCELED, path, { self.web.add_request(self.web.REQUEST_CANCELED, path, {
'id': download_id 'id': history_id
}) })
break break
@ -152,7 +122,7 @@ class ShareModeWeb(object):
sys.stdout.flush() sys.stdout.flush()
self.web.add_request(self.web.REQUEST_PROGRESS, path, { self.web.add_request(self.web.REQUEST_PROGRESS, path, {
'id': download_id, 'id': history_id,
'bytes': downloaded_bytes 'bytes': downloaded_bytes
}) })
self.web.done = False self.web.done = False
@ -163,7 +133,7 @@ class ShareModeWeb(object):
# tell the GUI the download has canceled # tell the GUI the download has canceled
self.web.add_request(self.web.REQUEST_CANCELED, path, { self.web.add_request(self.web.REQUEST_CANCELED, path, {
'id': download_id 'id': history_id
}) })
fp.close() fp.close()
@ -198,19 +168,73 @@ class ShareModeWeb(object):
r.headers.set('Content-Type', content_type) r.headers.set('Content-Type', content_type)
return r return r
def set_file_info(self, filenames, processed_size_callback=None): def directory_listing_template(self, path, files, dirs, breadcrumbs, breadcrumbs_leaf):
""" return make_response(render_template(
Using the list of filenames being shared, fill in details that the web 'send.html',
page will need to display. This includes zipping up the file in order to file_info=self.file_info,
get the zip file's name and size. files=files,
""" dirs=dirs,
self.common.log("ShareModeWeb", "set_file_info") 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.web.cancel_compression = False
self.build_zipfile_list(filenames, processed_size_callback)
self.cleanup_filenames = [] def render_logic(self, path=''):
if path in self.files:
filesystem_path = self.files[path]
# build file info list # If it's a directory
self.file_info = {'files': [], 'dirs': []} 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: for filename in filenames:
info = { info = {
'filename': filename, 'filename': filename,
@ -267,33 +291,6 @@ class ShareModeWeb(object):
return True return True
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()
class ZipWriter(object): class ZipWriter(object):
""" """

View File

@ -10,7 +10,7 @@ from distutils.version import LooseVersion as Version
from urllib.request import urlopen from urllib.request import urlopen
import flask import flask
from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version from flask import Flask, request, render_template, abort, make_response, send_file, __version__ as flask_version
from flask_httpauth import HTTPBasicAuth from flask_httpauth import HTTPBasicAuth
from .. import strings from .. import strings
@ -30,22 +30,25 @@ except:
pass pass
class Web(object): class Web:
""" """
The Web object is the OnionShare web server, powered by flask The Web object is the OnionShare web server, powered by flask
""" """
REQUEST_LOAD = 0 REQUEST_LOAD = 0
REQUEST_STARTED = 1 REQUEST_STARTED = 1
REQUEST_PROGRESS = 2 REQUEST_PROGRESS = 2
REQUEST_OTHER = 3 REQUEST_CANCELED = 3
REQUEST_CANCELED = 4 REQUEST_RATE_LIMIT = 4
REQUEST_RATE_LIMIT = 5 REQUEST_UPLOAD_FILE_RENAMED = 5
REQUEST_UPLOAD_FILE_RENAMED = 6 REQUEST_UPLOAD_SET_DIR = 6
REQUEST_UPLOAD_SET_DIR = 7 REQUEST_UPLOAD_FINISHED = 7
REQUEST_UPLOAD_FINISHED = 8 REQUEST_UPLOAD_CANCELED = 8
REQUEST_UPLOAD_CANCELED = 9 REQUEST_INDIVIDUAL_FILE_STARTED = 9
REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10 REQUEST_INDIVIDUAL_FILE_PROGRESS = 10
REQUEST_INVALID_PASSWORD = 11 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='share'): def __init__(self, common, is_gui, mode='share'):
self.common = common self.common = common
@ -116,13 +119,35 @@ class Web(object):
# Create the mode web object, which defines its own routes # Create the mode web object, which defines its own routes
self.share_mode = None self.share_mode = None
self.receive_mode = None self.receive_mode = None
if self.mode == 'receive': self.website_mode = None
if self.mode == 'share':
self.share_mode = ShareModeWeb(self.common, self)
elif self.mode == 'receive':
self.receive_mode = ReceiveModeWeb(self.common, self) self.receive_mode = ReceiveModeWeb(self.common, self)
elif self.mode == 'website': elif self.mode == 'website':
self.website_mode = WebsiteModeWeb(self.common, self) self.website_mode = WebsiteModeWeb(self.common, self)
elif self.mode == 'share':
self.share_mode = ShareModeWeb(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
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 = '/static_{}'.format(self.common.random_string(16))
self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(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): def define_common_routes(self):
""" """
@ -152,7 +177,10 @@ class Web(object):
@self.app.errorhandler(404) @self.app.errorhandler(404)
def not_found(e): def not_found(e):
return self.error404() 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") @self.app.route("/<password_candidate>/shutdown")
def shutdown(password_candidate): def shutdown(password_candidate):
@ -164,6 +192,11 @@ class Web(object):
return "" return ""
abort(404) abort(404)
if self.mode != 'website':
@self.app.route("/favicon.ico")
def favicon():
return send_file('{}/img/favicon.ico'.format(self.common.get_resource_path('static')))
def error401(self): def error401(self):
auth = request.authorization auth = request.authorization
if auth: if auth:
@ -182,15 +215,23 @@ class Web(object):
r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401) r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401)
return self.add_security_headers(r) return self.add_security_headers(r)
def error404(self): 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, '{}'.format(request.path), {
'id': history_id,
'status_code': 404
})
self.add_request(Web.REQUEST_OTHER, request.path) self.add_request(Web.REQUEST_OTHER, request.path)
r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404) r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404)
return self.add_security_headers(r) return self.add_security_headers(r)
def error403(self): def error405(self):
self.add_request(Web.REQUEST_OTHER, request.path) r = make_response(render_template('405.html', static_url_path=self.static_url_path), 405)
r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403)
return self.add_security_headers(r) return self.add_security_headers(r)
def add_security_headers(self, r): def add_security_headers(self, r):
@ -225,18 +266,6 @@ class Web(object):
self.password = self.common.build_password() self.password = self.common.build_password()
self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password)) self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password))
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 = '/static_{}'.format(self.common.random_string(16))
self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(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 verbose_mode(self): def verbose_mode(self):
""" """
Turn on verbose mode, which will log flask errors to a file. Turn on verbose mode, which will log flask errors to a file.

View File

@ -2,35 +2,23 @@ import os
import sys import sys
import tempfile import tempfile
import mimetypes import mimetypes
from flask import Response, request, render_template, make_response, send_from_directory from flask import Response, request, render_template, make_response
from .send_base_mode import SendBaseModeWeb
from .. import strings from .. import strings
class WebsiteModeWeb(object): class WebsiteModeWeb(SendBaseModeWeb):
""" """
All of the web logic for share mode All of the web logic for website mode
""" """
def __init__(self, common, web): def init(self):
self.common = common pass
self.common.log('WebsiteModeWeb', '__init__')
self.web = web
# Dictionary mapping file paths to filenames on disk
self.files = {}
self.visit_count = 0
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.define_routes()
def define_routes(self): def define_routes(self):
""" """
The web app routes for sharing a website The web app routes for sharing a website
""" """
@self.web.app.route('/', defaults={'path': ''}) @self.web.app.route('/', defaults={'path': ''})
@self.web.app.route('/<path:path>') @self.web.app.route('/<path:path>')
def path_public(path): def path_public(path):
@ -40,142 +28,69 @@ class WebsiteModeWeb(object):
""" """
Render the onionshare website. Render the onionshare website.
""" """
return self.render_logic(path)
# Each download has a unique id def directory_listing_template(self, path, files, dirs, breadcrumbs, breadcrumbs_leaf):
visit_id = self.visit_count return make_response(render_template('listing.html',
self.visit_count += 1
# Tell GUI the page has been visited
self.web.add_request(self.web.REQUEST_STARTED, path, {
'id': visit_id,
'action': 'visit'
})
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
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
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(path, filenames, filesystem_path)
# If it's a file
elif os.path.isfile(filesystem_path):
dirname = os.path.dirname(filesystem_path)
basename = os.path.basename(filesystem_path)
return send_from_directory(dirname, basename)
# If it's not a directory or file, throw a 404
else:
return self.web.error404()
else:
# Special case loading /
if path == '':
index_path = 'index.html'
if index_path in self.files:
# Render it
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
else:
# Root directory listing
filenames = list(self.root_files)
filenames.sort()
return self.directory_listing(path, filenames)
else:
# If the path isn't found, throw a 404
return self.web.error404()
def directory_listing(self, path, filenames, filesystem_path=None):
# If filesystem_path is None, this is the root directory listing
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
})
r = make_response(render_template('listing.html',
path=path, path=path,
files=files, files=files,
dirs=dirs, dirs=dirs,
breadcrumbs=breadcrumbs,
breadcrumbs_leaf=breadcrumbs_leaf,
static_url_path=self.web.static_url_path)) static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r)
def set_file_info(self, filenames): def set_file_info_custom(self, filenames, processed_size_callback):
""" self.common.log("WebsiteModeWeb", "set_file_info_custom")
Build a data structure that describes the list of files that make up self.web.cancel_compression = True
the static website.
"""
self.common.log("WebsiteModeWeb", "set_file_info")
# This is a dictionary that maps HTTP routes to filenames on disk def render_logic(self, path=''):
self.files = {} if path in self.files:
filesystem_path = self.files[path]
# This is only the root files and dirs, as opposed to all of them # If it's a directory
self.root_files = {} 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])
# If there's just one folder, replace filenames with a list of files inside that folder else:
if len(filenames) == 1 and os.path.isdir(filenames[0]): # Otherwise, render directory listing
filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])] 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)
# Loop through the files # If it's a file
for filename in filenames: elif os.path.isfile(filesystem_path):
basename = os.path.basename(filename.rstrip('/')) return self.stream_individual_file(filesystem_path)
# If it's a filename, add it # If it's not a directory or file, throw a 404
if os.path.isfile(filename): else:
self.files[basename] = filename history_id = self.cur_history_id
self.root_files[basename] = filename self.cur_history_id += 1
return self.web.error404(history_id)
else:
# Special case loading /
# If it's a directory, add it recursively if path == '':
elif os.path.isdir(filename): index_path = 'index.html'
self.root_files[basename + '/'] = filename 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)
for root, _, nested_filenames in os.walk(filename): else:
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder", # If the path isn't found, throw a 404
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar". history_id = self.cur_history_id
# The normalized_root should be "some_folder/foobar" self.cur_history_id += 1
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/') return self.web.error404(history_id)
# 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)
return True

View File

@ -22,6 +22,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings from onionshare import strings
from onionshare.common import AutoStopTimer from onionshare.common import AutoStopTimer
from .history import IndividualFileHistoryItem
from ..server_status import ServerStatus from ..server_status import ServerStatus
from ..threads import OnionThread from ..threads import OnionThread
from ..threads import AutoStartTimer from ..threads import AutoStartTimer
@ -29,7 +31,7 @@ from ..widgets import Alert
class Mode(QtWidgets.QWidget): class Mode(QtWidgets.QWidget):
""" """
The class that ShareMode and ReceiveMode inherit from. The class that all modes inherit from
""" """
start_server_finished = QtCore.pyqtSignal() start_server_finished = QtCore.pyqtSignal()
stop_server_finished = QtCore.pyqtSignal() stop_server_finished = QtCore.pyqtSignal()
@ -417,3 +419,32 @@ class Mode(QtWidgets.QWidget):
Handle REQUEST_UPLOAD_CANCELED event. Handle REQUEST_UPLOAD_CANCELED event.
""" """
pass pass
def handle_request_individual_file_started(self, event):
"""
Handle REQUEST_INDVIDIDUAL_FILES_STARTED event.
Used in both Share and Website modes, so implemented here.
"""
self.toggle_history.update_indicator(True)
self.history.requests_count += 1
self.history.update_requests()
item = IndividualFileHistoryItem(self.common, event["data"], event["path"])
self.history.add(event["data"]["id"], item)
def handle_request_individual_file_progress(self, event):
"""
Handle REQUEST_INDVIDIDUAL_FILES_PROGRESS event.
Used in both Share and Website modes, so implemented here.
"""
self.history.update(event["data"]["id"], event["data"]["bytes"])
if self.server_status.status == self.server_status.STATUS_STOPPED:
self.history.cancel(event["data"]["id"])
def handle_request_individual_file_canceled(self, event):
"""
Handle REQUEST_INDVIDIDUAL_FILES_CANCELED event.
Used in both Share and Website modes, so implemented here.
"""
self.history.cancel(event["data"]["id"])

View File

@ -237,6 +237,7 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
elif self.common.platform == 'Windows': elif self.common.platform == 'Windows':
subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)]) subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
class ReceiveHistoryItem(HistoryItem): class ReceiveHistoryItem(HistoryItem):
def __init__(self, common, id, content_length): def __init__(self, common, id, content_length):
super(ReceiveHistoryItem, self).__init__() super(ReceiveHistoryItem, self).__init__()
@ -341,35 +342,108 @@ class ReceiveHistoryItem(HistoryItem):
self.label.setText(self.get_canceled_label_text(self.started)) self.label.setText(self.get_canceled_label_text(self.started))
class VisitHistoryItem(HistoryItem): class IndividualFileHistoryItem(HistoryItem):
""" """
Download history item, for share mode Individual file history item, for share mode viewing of individual files
""" """
def __init__(self, common, id, total_bytes): def __init__(self, common, data, path):
super(VisitHistoryItem, self).__init__() super(IndividualFileHistoryItem, self).__init__()
self.status = HistoryItem.STATUS_STARTED self.status = HistoryItem.STATUS_STARTED
self.common = common self.common = common
self.id = id self.id = id
self.visited = time.time() self.path = path
self.visited_dt = datetime.fromtimestamp(self.visited) self.total_bytes = 0
self.downloaded_bytes = 0
self.started = time.time()
self.started_dt = datetime.fromtimestamp(self.started)
self.status = HistoryItem.STATUS_STARTED
# Label self.directory_listing = 'directory_listing' in data
self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.visited_dt.strftime("%b %d, %I:%M%p")))
# Labels
self.timestamp_label = QtWidgets.QLabel(self.started_dt.strftime("%b %d, %I:%M%p"))
self.timestamp_label.setStyleSheet(self.common.css['history_individual_file_timestamp_label'])
self.path_label = QtWidgets.QLabel("{}".format(self.path))
self.status_code_label = QtWidgets.QLabel()
# Progress bar
self.progress_bar = QtWidgets.QProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
# Text layout
labels_layout = QtWidgets.QHBoxLayout()
labels_layout.addWidget(self.timestamp_label)
labels_layout.addWidget(self.path_label)
labels_layout.addWidget(self.status_code_label)
labels_layout.addStretch()
# Layout # Layout
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label) layout.addLayout(labels_layout)
layout.addWidget(self.progress_bar)
self.setLayout(layout) self.setLayout(layout)
def update(self): # Is a status code already sent?
self.label.setText(self.get_finished_label_text(self.started_dt)) if 'status_code' in data:
self.status = HistoryItem.STATUS_FINISHED self.status_code_label.setText("{}".format(data['status_code']))
if data['status_code'] >= 200 and data['status_code'] < 300:
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
if data['status_code'] >= 400 and data['status_code'] < 500:
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_4xx'])
self.status = HistoryItem.STATUS_FINISHED
self.progress_bar.hide()
return
else:
self.total_bytes = data['filesize']
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(data['filesize'])
self.progress_bar.total_bytes = data['filesize']
# Start at 0
self.update(0)
def update(self, downloaded_bytes):
self.downloaded_bytes = downloaded_bytes
self.progress_bar.setValue(downloaded_bytes)
if downloaded_bytes == self.progress_bar.total_bytes:
self.status_code_label.setText("200")
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
self.progress_bar.hide()
self.status = HistoryItem.STATUS_FINISHED
else:
elapsed = time.time() - self.started
if elapsed < 10:
# Wait a couple of seconds for the download rate to stabilize.
# This prevents a "Windows copy dialog"-esque experience at
# the beginning of the download.
pb_fmt = strings._('gui_all_modes_progress_starting').format(
self.common.human_readable_filesize(downloaded_bytes))
else:
pb_fmt = strings._('gui_all_modes_progress_eta').format(
self.common.human_readable_filesize(downloaded_bytes),
self.estimated_time_remaining)
self.progress_bar.setFormat(pb_fmt)
def cancel(self): def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled')) self.progress_bar.setFormat(strings._('gui_canceled'))
self.status = HistoryItem.STATUS_CANCELED self.status = HistoryItem.STATUS_CANCELED
@property
def estimated_time_remaining(self):
return self.common.estimated_time_remaining(self.downloaded_bytes,
self.total_bytes,
self.started)
class HistoryItemList(QtWidgets.QScrollArea): class HistoryItemList(QtWidgets.QScrollArea):
""" """
List of items List of items
@ -452,26 +526,30 @@ class History(QtWidgets.QWidget):
# In progress and completed counters # In progress and completed counters
self.in_progress_count = 0 self.in_progress_count = 0
self.completed_count = 0 self.completed_count = 0
self.requests_count = 0
# In progress and completed labels # In progress, completed, and requests labels
self.in_progress_label = QtWidgets.QLabel() self.in_progress_label = QtWidgets.QLabel()
self.in_progress_label.setStyleSheet(self.common.css['mode_info_label']) self.in_progress_label.setStyleSheet(self.common.css['mode_info_label'])
self.completed_label = QtWidgets.QLabel() self.completed_label = QtWidgets.QLabel()
self.completed_label.setStyleSheet(self.common.css['mode_info_label']) self.completed_label.setStyleSheet(self.common.css['mode_info_label'])
self.requests_label = QtWidgets.QLabel()
self.requests_label.setStyleSheet(self.common.css['mode_info_label'])
# Header # Header
self.header_label = QtWidgets.QLabel(header_text) self.header_label = QtWidgets.QLabel(header_text)
self.header_label.setStyleSheet(self.common.css['downloads_uploads_label']) self.header_label.setStyleSheet(self.common.css['downloads_uploads_label'])
clear_button = QtWidgets.QPushButton(strings._('gui_all_modes_clear_history')) self.clear_button = QtWidgets.QPushButton(strings._('gui_all_modes_clear_history'))
clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) self.clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
clear_button.setFlat(True) self.clear_button.setFlat(True)
clear_button.clicked.connect(self.reset) self.clear_button.clicked.connect(self.reset)
header_layout = QtWidgets.QHBoxLayout() header_layout = QtWidgets.QHBoxLayout()
header_layout.addWidget(self.header_label) header_layout.addWidget(self.header_label)
header_layout.addStretch() header_layout.addStretch()
header_layout.addWidget(self.in_progress_label) header_layout.addWidget(self.in_progress_label)
header_layout.addWidget(self.completed_label) header_layout.addWidget(self.completed_label)
header_layout.addWidget(clear_button) header_layout.addWidget(self.requests_label)
header_layout.addWidget(self.clear_button)
# When there are no items # When there are no items
self.empty_image = QtWidgets.QLabel() self.empty_image = QtWidgets.QLabel()
@ -549,14 +627,18 @@ class History(QtWidgets.QWidget):
self.completed_count = 0 self.completed_count = 0
self.update_completed() self.update_completed()
# Reset web requests counter
self.requests_count = 0
self.update_requests()
def update_completed(self): def update_completed(self):
""" """
Update the 'completed' widget. Update the 'completed' widget.
""" """
if self.completed_count == 0: if self.completed_count == 0:
image = self.common.get_resource_path('images/share_completed_none.png') image = self.common.get_resource_path('images/history_completed_none.png')
else: else:
image = self.common.get_resource_path('images/share_completed.png') image = self.common.get_resource_path('images/history_completed.png')
self.completed_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.completed_count)) self.completed_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.completed_count))
self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count)) self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count))
@ -564,14 +646,25 @@ class History(QtWidgets.QWidget):
""" """
Update the 'in progress' widget. Update the 'in progress' widget.
""" """
if self.mode != 'website': if self.in_progress_count == 0:
if self.in_progress_count == 0: image = self.common.get_resource_path('images/history_in_progress_none.png')
image = self.common.get_resource_path('images/share_in_progress_none.png') else:
else: image = self.common.get_resource_path('images/history_in_progress.png')
image = self.common.get_resource_path('images/share_in_progress.png')
self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count)) self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count)) self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count))
def update_requests(self):
"""
Update the 'web requests' widget.
"""
if self.requests_count == 0:
image = self.common.get_resource_path('images/history_requests_none.png')
else:
image = self.common.get_resource_path('images/history_requests.png')
self.requests_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.requests_count))
self.requests_label.setToolTip(strings._('history_requests_tooltip').format(self.requests_count))
class ToggleHistory(QtWidgets.QPushButton): class ToggleHistory(QtWidgets.QPushButton):
@ -604,7 +697,7 @@ class ToggleHistory(QtWidgets.QPushButton):
def update_indicator(self, increment=False): def update_indicator(self, increment=False):
""" """
Update the display of the indicator count. If increment is True, then Update the display of the indicator count. If increment is True, then
only increment the counter if Downloads is hidden. only increment the counter if History is hidden.
""" """
if increment and not self.history_widget.isVisible(): if increment and not self.history_widget.isVisible():
self.indicator_count += 1 self.indicator_count += 1

View File

@ -97,7 +97,7 @@ class ReceiveMode(Mode):
The auto-stop timer expired, should we stop the server? Returns a bool The auto-stop timer expired, should we stop the server? Returns a bool
""" """
# If there were no attempts to upload files, or all uploads are done, we can stop # If there were no attempts to upload files, or all uploads are done, we can stop
if self.web.receive_mode.upload_count == 0 or not self.web.receive_mode.uploads_in_progress: if self.web.receive_mode.cur_history_id == 0 or not self.web.receive_mode.uploads_in_progress:
self.server_status.stop_server() self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer')) self.server_status_label.setText(strings._('close_on_autostop_timer'))
return True return True
@ -112,7 +112,7 @@ class ReceiveMode(Mode):
Starting the server. Starting the server.
""" """
# Reset web counters # Reset web counters
self.web.receive_mode.upload_count = 0 self.web.receive_mode.cur_history_id = 0
self.web.reset_invalid_passwords() self.web.reset_invalid_passwords()
# Hide and reset the uploads if we have previously shared # Hide and reset the uploads if we have previously shared
@ -212,6 +212,8 @@ class ReceiveMode(Mode):
Set the info counters back to zero. Set the info counters back to zero.
""" """
self.history.reset() self.history.reset()
self.toggle_history.indicator_count = 0
self.toggle_history.update_indicator()
def update_primary_action(self): def update_primary_action(self):
self.common.log('ReceiveMode', 'update_primary_action') self.common.log('ReceiveMode', 'update_primary_action')

View File

@ -132,7 +132,7 @@ class ShareMode(Mode):
The auto-stop timer expired, should we stop the server? Returns a bool The auto-stop timer expired, should we stop the server? Returns a bool
""" """
# If there were no attempts to download the share, or all downloads are done, we can stop # If there were no attempts to download the share, or all downloads are done, we can stop
if self.web.share_mode.download_count == 0 or self.web.done: if self.web.share_mode.cur_history_id == 0 or self.web.done:
self.server_status.stop_server() self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer')) self.server_status_label.setText(strings._('close_on_autostop_timer'))
return True return True
@ -146,7 +146,7 @@ class ShareMode(Mode):
Starting the server. Starting the server.
""" """
# Reset web counters # Reset web counters
self.web.share_mode.download_count = 0 self.web.share_mode.cur_history_id = 0
self.web.reset_invalid_passwords() self.web.reset_invalid_passwords()
# Hide and reset the downloads if we have previously shared # Hide and reset the downloads if we have previously shared
@ -225,12 +225,6 @@ class ShareMode(Mode):
""" """
self.primary_action.hide() self.primary_action.hide()
def handle_request_load(self, event):
"""
Handle REQUEST_LOAD event.
"""
self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_page_loaded_message'))
def handle_request_started(self, event): def handle_request_started(self, event):
""" """
Handle REQUEST_STARTED event. Handle REQUEST_STARTED event.
@ -325,6 +319,8 @@ class ShareMode(Mode):
Set the info counters back to zero. Set the info counters back to zero.
""" """
self.history.reset() self.history.reset()
self.toggle_history.indicator_count = 0
self.toggle_history.update_indicator()
@staticmethod @staticmethod
def _compute_total_size(filenames): def _compute_total_size(filenames):

View File

@ -41,12 +41,8 @@ class CompressThread(QtCore.QThread):
self.mode.common.log('CompressThread', 'run') self.mode.common.log('CompressThread', 'run')
try: try:
if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size)
self.success.emit() self.success.emit()
else:
# Cancelled
pass
self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames
except OSError as e: except OSError as e:
self.error.emit(e.strerror) self.error.emit(e.strerror)

View File

@ -30,7 +30,7 @@ from onionshare.web import Web
from ..file_selection import FileSelection from ..file_selection import FileSelection
from .. import Mode from .. import Mode
from ..history import History, ToggleHistory, VisitHistoryItem from ..history import History, ToggleHistory
from ...widgets import Alert from ...widgets import Alert
class WebsiteMode(Mode): class WebsiteMode(Mode):
@ -80,6 +80,8 @@ class WebsiteMode(Mode):
strings._('gui_all_modes_history'), strings._('gui_all_modes_history'),
'website' 'website'
) )
self.history.in_progress_label.hide()
self.history.completed_label.hide()
self.history.hide() self.history.hide()
# Info label # Info label
@ -165,12 +167,8 @@ class WebsiteMode(Mode):
Step 3 in starting the server. Display large filesize Step 3 in starting the server. Display large filesize
warning, if applicable. warning, if applicable.
""" """
self.web.website_mode.set_file_info(self.filenames)
if self.web.website_mode.set_file_info(self.filenames): self.success.emit()
self.success.emit()
else:
# Cancelled
pass
def start_server_error_custom(self): def start_server_error_custom(self):
""" """
@ -208,21 +206,6 @@ class WebsiteMode(Mode):
""" """
self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_loaded_message')) self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_loaded_message'))
def handle_request_started(self, event):
"""
Handle REQUEST_STARTED event.
"""
if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ):
item = VisitHistoryItem(self.common, event["data"]["id"], 0)
self.history.add(event["data"]["id"], item)
self.toggle_history.update_indicator(True)
self.history.completed_count += 1
self.history.update_completed()
self.system_tray.showMessage(strings._('systray_website_started_title'), strings._('systray_website_started_message'))
def on_reload_settings(self): def on_reload_settings(self):
""" """
If there were some files listed for sharing, we should be ok to re-enable If there were some files listed for sharing, we should be ok to re-enable
@ -262,6 +245,8 @@ class WebsiteMode(Mode):
Set the info counters back to zero. Set the info counters back to zero.
""" """
self.history.reset() self.history.reset()
self.toggle_history.indicator_count = 0
self.toggle_history.update_indicator()
@staticmethod @staticmethod
def _compute_total_size(filenames): def _compute_total_size(filenames):

View File

@ -383,7 +383,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode.server_status.autostart_timer_container.hide() self.share_mode.server_status.autostart_timer_container.hide()
self.receive_mode.server_status.autostart_timer_container.hide() self.receive_mode.server_status.autostart_timer_container.hide()
self.website_mode.server_status.autostart_timer_container.hide() self.website_mode.server_status.autostart_timer_container.hide()
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only) d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
d.settings_saved.connect(reload_settings) d.settings_saved.connect(reload_settings)
d.exec_() d.exec_()
@ -470,6 +470,15 @@ class OnionShareGui(QtWidgets.QMainWindow):
elif event["type"] == Web.REQUEST_UPLOAD_CANCELED: elif event["type"] == Web.REQUEST_UPLOAD_CANCELED:
mode.handle_request_upload_canceled(event) mode.handle_request_upload_canceled(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED:
mode.handle_request_individual_file_started(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS:
mode.handle_request_individual_file_progress(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED:
mode.handle_request_individual_file_canceled(event)
if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE: if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE:
Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"])) Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"]))
@ -478,7 +487,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.status_bar.showMessage('{0:s}: {1:s}'.format(strings._('other_page_loaded'), event["path"])) self.status_bar.showMessage('{0:s}: {1:s}'.format(strings._('other_page_loaded'), event["path"]))
if event["type"] == Web.REQUEST_INVALID_PASSWORD: if event["type"] == Web.REQUEST_INVALID_PASSWORD:
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_passwords_count, strings._('invalid_password_guess'), event["data"])) self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_passwords_count, strings._('incorrect_password'), event["data"]))
mode.timer_callback() mode.timer_callback()

View File

@ -240,6 +240,9 @@ class ServerStatus(QtWidgets.QWidget):
""" """
# Set the URL fields # Set the URL fields
if self.status == self.STATUS_STARTED: if self.status == self.STATUS_STARTED:
# The backend Onion may have saved new settings, such as the private key.
# Reload the settings before saving new ones.
self.common.settings.load()
self.show_url() self.show_url()
if self.common.settings.get('save_private_key'): if self.common.settings.get('save_private_key'):

View File

@ -18,7 +18,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5 import QtCore, QtWidgets, QtGui
import sys, platform, datetime, re import sys
import platform
import datetime
import re
import os
from onionshare import strings, common from onionshare import strings, common
from onionshare.settings import Settings from onionshare.settings import Settings
@ -28,6 +32,7 @@ from .widgets import Alert
from .update_checker import * from .update_checker import *
from .tor_connection_dialog import TorConnectionDialog from .tor_connection_dialog import TorConnectionDialog
class SettingsDialog(QtWidgets.QDialog): class SettingsDialog(QtWidgets.QDialog):
""" """
Settings dialog. Settings dialog.
@ -52,6 +57,9 @@ class SettingsDialog(QtWidgets.QDialog):
self.system = platform.system() self.system = platform.system()
# If ONIONSHARE_HIDE_TOR_SETTINGS=1, hide Tor settings in the dialog
self.hide_tor_settings = os.environ.get('ONIONSHARE_HIDE_TOR_SETTINGS') == "1"
# General settings # General settings
# Use a password or not ('public mode') # Use a password or not ('public mode')
@ -204,10 +212,12 @@ class SettingsDialog(QtWidgets.QDialog):
self.close_after_first_download_checkbox = QtWidgets.QCheckBox() self.close_after_first_download_checkbox = QtWidgets.QCheckBox()
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked) self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked)
self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option")) self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option"))
individual_downloads_label = QtWidgets.QLabel(strings._("gui_settings_individual_downloads_label"))
# Sharing options layout # Sharing options layout
sharing_group_layout = QtWidgets.QVBoxLayout() sharing_group_layout = QtWidgets.QVBoxLayout()
sharing_group_layout.addWidget(self.close_after_first_download_checkbox) sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
sharing_group_layout.addWidget(individual_downloads_label)
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label")) sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label"))
sharing_group.setLayout(sharing_group_layout) sharing_group.setLayout(sharing_group_layout)
@ -484,7 +494,8 @@ class SettingsDialog(QtWidgets.QDialog):
col_layout = QtWidgets.QHBoxLayout() col_layout = QtWidgets.QHBoxLayout()
col_layout.addLayout(left_col_layout) col_layout.addLayout(left_col_layout)
col_layout.addLayout(right_col_layout) if not self.hide_tor_settings:
col_layout.addLayout(right_col_layout)
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addLayout(col_layout) layout.addLayout(col_layout)
@ -629,12 +640,13 @@ class SettingsDialog(QtWidgets.QDialog):
self.connect_to_tor_label.show() self.connect_to_tor_label.show()
self.onion_settings_widget.hide() self.onion_settings_widget.hide()
def connection_type_bundled_toggled(self, checked): def connection_type_bundled_toggled(self, checked):
""" """
Connection type bundled was toggled. If checked, hide authentication fields. Connection type bundled was toggled. If checked, hide authentication fields.
""" """
self.common.log('SettingsDialog', 'connection_type_bundled_toggled') self.common.log('SettingsDialog', 'connection_type_bundled_toggled')
if self.hide_tor_settings:
return
if checked: if checked:
self.authenticate_group.hide() self.authenticate_group.hide()
self.connection_type_socks.hide() self.connection_type_socks.hide()
@ -644,6 +656,8 @@ class SettingsDialog(QtWidgets.QDialog):
""" """
'No bridges' option was toggled. If checked, enable other bridge options. 'No bridges' option was toggled. If checked, enable other bridge options.
""" """
if self.hide_tor_settings:
return
if checked: if checked:
self.tor_bridges_use_custom_textbox_options.hide() self.tor_bridges_use_custom_textbox_options.hide()
@ -651,6 +665,8 @@ class SettingsDialog(QtWidgets.QDialog):
""" """
obfs4 bridges option was toggled. If checked, disable custom bridge options. obfs4 bridges option was toggled. If checked, disable custom bridge options.
""" """
if self.hide_tor_settings:
return
if checked: if checked:
self.tor_bridges_use_custom_textbox_options.hide() self.tor_bridges_use_custom_textbox_options.hide()
@ -658,6 +674,8 @@ class SettingsDialog(QtWidgets.QDialog):
""" """
meek_lite_azure bridges option was toggled. If checked, disable custom bridge options. meek_lite_azure bridges option was toggled. If checked, disable custom bridge options.
""" """
if self.hide_tor_settings:
return
if checked: if checked:
self.tor_bridges_use_custom_textbox_options.hide() self.tor_bridges_use_custom_textbox_options.hide()
# Alert the user about meek's costliness if it looks like they're turning it on # Alert the user about meek's costliness if it looks like they're turning it on
@ -668,6 +686,8 @@ class SettingsDialog(QtWidgets.QDialog):
""" """
Custom bridges option was toggled. If checked, show custom bridge options. Custom bridges option was toggled. If checked, show custom bridge options.
""" """
if self.hide_tor_settings:
return
if checked: if checked:
self.tor_bridges_use_custom_textbox_options.show() self.tor_bridges_use_custom_textbox_options.show()
@ -676,6 +696,8 @@ class SettingsDialog(QtWidgets.QDialog):
Connection type automatic was toggled. If checked, hide authentication fields. Connection type automatic was toggled. If checked, hide authentication fields.
""" """
self.common.log('SettingsDialog', 'connection_type_automatic_toggled') self.common.log('SettingsDialog', 'connection_type_automatic_toggled')
if self.hide_tor_settings:
return
if checked: if checked:
self.authenticate_group.hide() self.authenticate_group.hide()
self.connection_type_socks.hide() self.connection_type_socks.hide()
@ -687,6 +709,8 @@ class SettingsDialog(QtWidgets.QDialog):
for Tor control address and port. If unchecked, hide those extra fields. for Tor control address and port. If unchecked, hide those extra fields.
""" """
self.common.log('SettingsDialog', 'connection_type_control_port_toggled') self.common.log('SettingsDialog', 'connection_type_control_port_toggled')
if self.hide_tor_settings:
return
if checked: if checked:
self.authenticate_group.show() self.authenticate_group.show()
self.connection_type_control_port_extras.show() self.connection_type_control_port_extras.show()
@ -702,6 +726,8 @@ class SettingsDialog(QtWidgets.QDialog):
for socket file. If unchecked, hide those extra fields. for socket file. If unchecked, hide those extra fields.
""" """
self.common.log('SettingsDialog', 'connection_type_socket_file_toggled') self.common.log('SettingsDialog', 'connection_type_socket_file_toggled')
if self.hide_tor_settings:
return
if checked: if checked:
self.authenticate_group.show() self.authenticate_group.show()
self.connection_type_socket_file_extras.show() self.connection_type_socket_file_extras.show()

View File

@ -63,9 +63,9 @@ classifiers = [
"Environment :: Web Environment" "Environment :: Web Environment"
] ]
data_files=[ data_files=[
(os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']), (os.path.join(sys.prefix, 'share/applications'), ['install/org.onionshare.OnionShare.desktop']),
(os.path.join(sys.prefix, 'share/metainfo'), ['install/onionshare.appdata.xml']), (os.path.join(sys.prefix, 'share/icons/hicolor/scalable/apps'), ['install/org.onionshare.OnionShare.svg']),
(os.path.join(sys.prefix, 'share/pixmaps'), ['install/onionshare80.xpm']), (os.path.join(sys.prefix, 'share/metainfo'), ['install/org.onionshare.OnionShare.appdata.xml']),
(os.path.join(sys.prefix, 'share/onionshare'), file_list('share')), (os.path.join(sys.prefix, 'share/onionshare'), file_list('share')),
(os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')), (os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')),
(os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')), (os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')),
@ -74,7 +74,7 @@ data_files=[
(os.path.join(sys.prefix, 'share/onionshare/static/img'), file_list('share/static/img')), (os.path.join(sys.prefix, 'share/onionshare/static/img'), file_list('share/static/img')),
(os.path.join(sys.prefix, 'share/onionshare/static/js'), file_list('share/static/js')) (os.path.join(sys.prefix, 'share/onionshare/static/js'), file_list('share/static/js'))
] ]
if platform.system() != 'OpenBSD': if not platform.system().endswith('BSD') and platform.system() != 'DragonFly':
data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py'])) data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py']))
setup( setup(

View File

Before

Width:  |  Height:  |  Size: 646 B

After

Width:  |  Height:  |  Size: 646 B

View File

Before

Width:  |  Height:  |  Size: 437 B

After

Width:  |  Height:  |  Size: 437 B

View File

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 638 B

View File

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

View File

@ -3,7 +3,7 @@
"not_a_readable_file": "{0:s} is not a readable file.", "not_a_readable_file": "{0:s} is not a readable file.",
"no_available_port": "Could not find an available port to start the onion service", "no_available_port": "Could not find an available port to start the onion service",
"other_page_loaded": "Address loaded", "other_page_loaded": "Address loaded",
"invalid_password_guess": "Invalid password guess", "incorrect_password": "Incorrect password",
"close_on_autostop_timer": "Stopped because auto-stop timer ran out", "close_on_autostop_timer": "Stopped because auto-stop timer ran out",
"closing_automatically": "Stopped because transfer is complete", "closing_automatically": "Stopped because transfer is complete",
"large_filesize": "Warning: Sending a large share could take hours", "large_filesize": "Warning: Sending a large share could take hours",
@ -52,6 +52,7 @@
"gui_settings_onion_label": "Onion settings", "gui_settings_onion_label": "Onion settings",
"gui_settings_sharing_label": "Sharing settings", "gui_settings_sharing_label": "Sharing settings",
"gui_settings_close_after_first_download_option": "Stop sharing after files have been sent", "gui_settings_close_after_first_download_option": "Stop sharing after files have been sent",
"gui_settings_individual_downloads_label": "Uncheck to allow downloading individual files",
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
"gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare", "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare",
"gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser", "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser",
@ -97,20 +98,20 @@
"error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_tor_protocol_error_unknown": "There was an unknown error with Tor",
"connecting_to_tor": "Connecting to the Tor network", "connecting_to_tor": "Connecting to the Tor network",
"update_available": "New OnionShare out. <a href='{}'>Click here</a> to get it.<br><br>You are using {} and the latest is {}.", "update_available": "New OnionShare out. <a href='{}'>Click here</a> to get it.<br><br>You are using {} and the latest is {}.",
"update_error_check_error": "Could not check for new versions: The OnionShare website is saying the latest version is the unrecognizable '{}'…", "update_error_check_error": "Could not check for new version: The OnionShare website is saying the latest version is the unrecognizable '{}'…",
"update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?", "update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?",
"update_not_available": "You are running the latest OnionShare.", "update_not_available": "You are running the latest OnionShare.",
"gui_tor_connection_ask": "Open the settings to sort out connection to Tor?", "gui_tor_connection_ask": "Open the settings to sort out connection to Tor?",
"gui_tor_connection_ask_open_settings": "Yes", "gui_tor_connection_ask_open_settings": "Yes",
"gui_tor_connection_ask_quit": "Quit", "gui_tor_connection_ask_quit": "Quit",
"gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.", "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.",
"gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.", "gui_tor_connection_canceled": "Could not connect to Tor.\n\nMake sure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.",
"gui_tor_connection_lost": "Disconnected from Tor.", "gui_tor_connection_lost": "Disconnected from Tor.",
"gui_server_started_after_autostop_timer": "The auto-stop timer ran out before the server started. Please make a new share.", "gui_server_started_after_autostop_timer": "The auto-stop timer ran out before the server started. Please make a new share.",
"gui_server_autostop_timer_expired": "The auto-stop timer already ran out. Please update it to start sharing.", "gui_server_autostop_timer_expired": "The auto-stop timer already ran out. Please adjust it to start sharing.",
"gui_server_autostart_timer_expired": "The scheduled time has already passed. Please update it to start sharing.", "gui_server_autostart_timer_expired": "The scheduled time has already passed. Please adjust it to start sharing.",
"gui_autostop_timer_cant_be_earlier_than_autostart_timer": "The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing.", "gui_autostop_timer_cant_be_earlier_than_autostart_timer": "The auto-stop time can't be the same or earlier than the auto-start time. Please adjust it to start sharing.",
"share_via_onionshare": "OnionShare it", "share_via_onionshare": "Share via OnionShare",
"gui_connect_to_tor_for_onion_settings": "Connect to Tor to see onion service settings", "gui_connect_to_tor_for_onion_settings": "Connect to Tor to see onion service settings",
"gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses",
"gui_save_private_key_checkbox": "Use a persistent address", "gui_save_private_key_checkbox": "Use a persistent address",
@ -133,6 +134,7 @@
"gui_file_info_single": "{} file, {}", "gui_file_info_single": "{} file, {}",
"history_in_progress_tooltip": "{} in progress", "history_in_progress_tooltip": "{} in progress",
"history_completed_tooltip": "{} completed", "history_completed_tooltip": "{} completed",
"history_requests_tooltip": "{} web requests",
"error_cannot_create_data_dir": "Could not create OnionShare data folder: {}", "error_cannot_create_data_dir": "Could not create OnionShare data folder: {}",
"gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>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.</b>", "gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>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.</b>",
"gui_mode_share_button": "Share Files", "gui_mode_share_button": "Share Files",
@ -144,12 +146,12 @@
"gui_settings_public_mode_checkbox": "Public mode", "gui_settings_public_mode_checkbox": "Public mode",
"gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}", "gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}",
"gui_settings_language_label": "Preferred language", "gui_settings_language_label": "Preferred language",
"gui_settings_language_changed_notice": "Restart OnionShare for your change in language to take effect.", "gui_settings_language_changed_notice": "Restart OnionShare for the new language to be applied.",
"systray_menu_exit": "Quit", "systray_menu_exit": "Quit",
"systray_page_loaded_title": "Page Loaded", "systray_page_loaded_title": "Page Loaded",
"systray_page_loaded_message": "OnionShare address loaded", "systray_page_loaded_message": "OnionShare address loaded",
"systray_site_loaded_title": "Site Loaded", "systray_site_loaded_title": "Website Loaded",
"systray_site_loaded_message": "OnionShare site loaded", "systray_site_loaded_message": "OnionShare website loaded",
"systray_share_started_title": "Sharing Started", "systray_share_started_title": "Sharing Started",
"systray_share_started_message": "Starting to send files to someone", "systray_share_started_message": "Starting to send files to someone",
"systray_share_completed_title": "Sharing Complete", "systray_share_completed_title": "Sharing Complete",
@ -160,6 +162,8 @@
"systray_receive_started_message": "Someone is sending files to you", "systray_receive_started_message": "Someone is sending files to you",
"systray_website_started_title": "Starting sharing website", "systray_website_started_title": "Starting sharing website",
"systray_website_started_message": "Someone is visiting your website", "systray_website_started_message": "Someone is visiting your website",
"systray_individual_file_downloaded_title": "Individual file loaded",
"systray_individual_file_downloaded_message": "Individual file {} viewed",
"gui_all_modes_history": "History", "gui_all_modes_history": "History",
"gui_all_modes_clear_history": "Clear All", "gui_all_modes_clear_history": "Clear All",
"gui_all_modes_transfer_started": "Started {}", "gui_all_modes_transfer_started": "Started {}",

View File

@ -56,6 +56,10 @@ header .right ul li {
cursor: pointer; cursor: pointer;
} }
a.button:visited {
color: #ffffff;
}
.close-button { .close-button {
color: #ffffff; color: #ffffff;
background-color: #c90c0c; background-color: #c90c0c;
@ -70,6 +74,30 @@ header .right ul li {
bottom: 10px; bottom: 10px;
} }
ul.breadcrumbs {
display: block;
list-style: none;
margin: 10px 0;
padding: 0;
}
ul.breadcrumbs li {
display: inline-block;
list-style: none;
margin: 0;
padding: 5px;
color: #999999;
}
ul.breadcrumbs li span.sep {
padding-left: 5px;
}
ul.breadcrumbs li a:link, ul.breadcrumbs li a:visited {
color: #666666;
border-bottom: 1px solid #666666;
}
table.file-list { table.file-list {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
@ -222,3 +250,12 @@ li.info {
color: #666666; color: #666666;
margin: 0 0 20px 0; margin: 0 0 20px 0;
} }
a {
text-decoration: none;
color: #1c1ca0;
}
a:visited {
color: #601ca0;
}

19
share/templates/405.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare: 405 Method Not Allowed</title>
<link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head>
<body>
<div class="info-wrapper">
<div class="info">
<p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
<p class="info-header">405 Method Not Allowed</p>
</div>
</div>
</body>
</html>

View File

@ -12,6 +12,12 @@
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>
{% if breadcrumbs %}
<ul class="breadcrumbs">
{% for breadcrumb in breadcrumbs %}<li><a href="{{ breadcrumb[1] }}">{{ breadcrumb[0] }}</a> <span class="sep">&#8227;</span></li>{% endfor %}<li>{{ breadcrumbs_leaf }}</li>
</ul>
{% endif %}
<table class="file-list" id="file-list"> <table class="file-list" id="file-list">
<tr> <tr>
<th id="filename-header">Filename</th> <th id="filename-header">Filename</th>

View File

@ -22,30 +22,43 @@
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>
{% if breadcrumbs %}
<ul class="breadcrumbs">
{% for breadcrumb in breadcrumbs %}<li><a href="{{ breadcrumb[1] }}">{{ breadcrumb[0] }}</a> <span class="sep">&#8227;</span></li>{% endfor %}<li>{{ breadcrumbs_leaf }}</li>
</ul>
{% endif %}
<table class="file-list" id="file-list"> <table class="file-list" id="file-list">
<tr> <tr>
<th id="filename-header">Filename</th> <th id="filename-header">Filename</th>
<th id="size-header">Size</th> <th id="size-header">Size</th>
<th></th> <th></th>
</tr> </tr>
{% for info in file_info.dirs %} {% for info in dirs %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" />
{{ info.basename }} <a href="{{ info.basename }}">
{{ info.basename }}
</a>
</td> </td>
<td>{{ info.size_human }}</td> <td>&mdash;</td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
{% for info in file_info.files %}
{% for info in files %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" />
{% if download_individual_files %}
<a href="{{ info.basename }}">
{{ info.basename }}
</a>
{% else %}
{{ info.basename }} {{ info.basename }}
{% endif %}
</td> </td>
<td>{{ info.size_human }}</td> <td>{{ info.size_human }}</td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View File

@ -1,6 +1,6 @@
[DEFAULT] [DEFAULT]
Package3: onionshare Package3: onionshare
Depends3: python3, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python-nautilus, tor, obfs4proxy Depends3: python3, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-distutils, python-nautilus, tor, obfs4proxy
Build-Depends: python3, python3-pytest, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-requests, python-nautilus, tor, obfs4proxy Build-Depends: python3, python3-all, python3-pytest, python3-requests
Suite: cosmic Suite: cosmic
X-Python3-Version: >= 3.5.3 X-Python3-Version: >= 3.5.3

View File

@ -14,6 +14,7 @@ from onionshare.web import Web
from onionshare_gui import Application, OnionShare, OnionShareGui from onionshare_gui import Application, OnionShare, OnionShareGui
from onionshare_gui.mode.share_mode import ShareMode from onionshare_gui.mode.share_mode import ShareMode
from onionshare_gui.mode.receive_mode import ReceiveMode from onionshare_gui.mode.receive_mode import ReceiveMode
from onionshare_gui.mode.website_mode import WebsiteMode
class GuiBaseTest(object): class GuiBaseTest(object):
@ -103,6 +104,9 @@ class GuiBaseTest(object):
if type(mode) == ShareMode: if type(mode) == ShareMode:
QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) self.assertTrue(self.gui.mode, self.gui.MODE_SHARE)
if type(mode) == WebsiteMode:
QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE)
def click_toggle_history(self, mode): def click_toggle_history(self, mode):
@ -112,7 +116,7 @@ class GuiBaseTest(object):
self.assertEqual(mode.history.isVisible(), not currently_visible) self.assertEqual(mode.history.isVisible(), not currently_visible)
def history_indicator(self, mode, public_mode): def history_indicator(self, mode, public_mode, indicator_count="1"):
'''Test that we can make sure the history is toggled off, do an action, and the indiciator works''' '''Test that we can make sure the history is toggled off, do an action, and the indiciator works'''
# Make sure history is toggled off # Make sure history is toggled off
if mode.history.isVisible(): if mode.history.isVisible():
@ -143,7 +147,7 @@ class GuiBaseTest(object):
# Indicator should be visible, have a value of "1" # Indicator should be visible, have a value of "1"
self.assertTrue(mode.toggle_history.indicator_label.isVisible()) self.assertTrue(mode.toggle_history.indicator_label.isVisible())
self.assertEqual(mode.toggle_history.indicator_label.text(), "1") self.assertEqual(mode.toggle_history.indicator_label.text(), indicator_count)
# Toggle history back on, indicator should be hidden again # Toggle history back on, indicator should be hidden again
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
@ -166,6 +170,9 @@ class GuiBaseTest(object):
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(mode.server_status.status, 1) self.assertEqual(mode.server_status.status, 1)
def toggle_indicator_is_reset(self, mode):
self.assertEqual(mode.toggle_history.indicator_count, 0)
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
def server_status_indicator_says_starting(self, mode): def server_status_indicator_says_starting(self, mode):
'''Test that the Server Status indicator shows we are Starting''' '''Test that the Server Status indicator shows we are Starting'''
@ -198,6 +205,9 @@ class GuiBaseTest(object):
else: else:
self.assertIsNone(mode.server_status.web.password, r'(\w+)-(\w+)') self.assertIsNone(mode.server_status.web.password, r'(\w+)-(\w+)')
def add_button_visible(self, mode):
'''Test that the add button should be visible'''
self.assertTrue(mode.server_status.file_selection.add_button.isVisible())
def url_description_shown(self, mode): def url_description_shown(self, mode):
'''Test that the URL label is showing''' '''Test that the URL label is showing'''
@ -249,7 +259,7 @@ class GuiBaseTest(object):
def server_is_stopped(self, mode, stay_open): def server_is_stopped(self, mode, stay_open):
'''Test that the server stops when we click Stop''' '''Test that the server stops when we click Stop'''
if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open): if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open) or (type(mode) == WebsiteMode):
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(mode.server_status.status, 0) self.assertEqual(mode.server_status.status, 0)
@ -275,6 +285,10 @@ class GuiBaseTest(object):
else: else:
self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically')) self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically'))
def clear_all_history_items(self, mode, count):
if count == 0:
QtTest.QTest.mouseClick(mode.history.clear_button, QtCore.Qt.LeftButton)
self.assertEquals(len(mode.history.item_list.items.keys()), count)
# Auto-stop timer tests # Auto-stop timer tests
def set_timeout(self, mode, timeout): def set_timeout(self, mode, timeout):

View File

@ -66,31 +66,6 @@ class GuiReceiveTest(GuiBaseTest):
r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port)) r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port))
self.assertEqual(r.status_code, 401) self.assertEqual(r.status_code, 401)
def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode):
'''If you submit the receive mode form without selecting any files, the UI shouldn't get updated'''
url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
# What were the counts before submitting the form?
before_in_progress_count = mode.history.in_progress_count
before_completed_count = mode.history.completed_count
before_number_of_history_items = len(mode.history.item_list.items)
# Click submit without including any files a few times
if public_mode:
r = requests.post(url, files={})
r = requests.post(url, files={})
r = requests.post(url, files={})
else:
auth = requests.auth.HTTPBasicAuth('onionshare', mode.web.password)
r = requests.post(url, files={}, auth=auth)
r = requests.post(url, files={}, auth=auth)
r = requests.post(url, files={}, auth=auth)
# The counts shouldn't change
self.assertEqual(mode.history.in_progress_count, before_in_progress_count)
self.assertEqual(mode.history.completed_count, before_completed_count)
self.assertEqual(len(mode.history.item_list.items), before_number_of_history_items)
# 'Grouped' tests follow from here # 'Grouped' tests follow from here
def run_all_receive_mode_setup_tests(self, public_mode): def run_all_receive_mode_setup_tests(self, public_mode):
@ -127,14 +102,13 @@ class GuiReceiveTest(GuiBaseTest):
# Test uploading the same file twice at the same time, and make sure no collisions # Test uploading the same file twice at the same time, and make sure no collisions
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt', True) self.upload_file(public_mode, '/tmp/test.txt', 'test.txt', True)
self.counter_incremented(self.gui.receive_mode, 6) self.counter_incremented(self.gui.receive_mode, 6)
self.uploading_zero_files_shouldnt_change_ui(self.gui.receive_mode, public_mode) self.history_indicator(self.gui.receive_mode, public_mode, "2")
self.history_indicator(self.gui.receive_mode, public_mode)
self.server_is_stopped(self.gui.receive_mode, False) self.server_is_stopped(self.gui.receive_mode, False)
self.web_server_is_stopped() self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.receive_mode, False) self.server_status_indicator_says_closed(self.gui.receive_mode, False)
self.server_working_on_start_button_pressed(self.gui.receive_mode) self.server_working_on_start_button_pressed(self.gui.receive_mode)
self.server_is_started(self.gui.receive_mode) self.server_is_started(self.gui.receive_mode)
self.history_indicator(self.gui.receive_mode, public_mode) self.history_indicator(self.gui.receive_mode, public_mode, "2")
def run_all_receive_mode_unwritable_dir_tests(self, public_mode): def run_all_receive_mode_unwritable_dir_tests(self, public_mode):
'''Attempt to upload (unwritable) files in receive mode and stop the share''' '''Attempt to upload (unwritable) files in receive mode and stop the share'''
@ -153,3 +127,12 @@ class GuiReceiveTest(GuiBaseTest):
self.autostop_timer_widget_hidden(self.gui.receive_mode) self.autostop_timer_widget_hidden(self.gui.receive_mode)
self.server_timed_out(self.gui.receive_mode, 15000) self.server_timed_out(self.gui.receive_mode, 15000)
self.web_server_is_stopped() self.web_server_is_stopped()
def run_all_clear_all_button_tests(self, public_mode):
"""Test the Clear All history button"""
self.run_all_receive_mode_setup_tests(public_mode)
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
self.history_widgets_present(self.gui.receive_mode)
self.clear_all_history_items(self.gui.receive_mode, 0)
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
self.clear_all_history_items(self.gui.receive_mode, 2)

View File

@ -44,7 +44,7 @@ class GuiShareTest(GuiBaseTest):
self.file_selection_widget_has_files(0) self.file_selection_widget_has_files(0)
def file_selection_widget_readd_files(self): def file_selection_widget_read_files(self):
'''Re-add some files to the list so we can share''' '''Re-add some files to the list so we can share'''
self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt')
@ -81,6 +81,40 @@ class GuiShareTest(GuiBaseTest):
QtTest.QTest.qWait(2000) QtTest.QTest.qWait(2000)
self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8')) self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
def individual_file_is_viewable_or_not(self, public_mode, stay_open):
'''Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)'''
url = "http://127.0.0.1:{}".format(self.gui.app.port)
download_file_url = "http://127.0.0.1:{}/test.txt".format(self.gui.app.port)
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
if stay_open:
self.assertTrue('a href="test.txt"' in r.text)
if public_mode:
r = requests.get(download_file_url)
else:
r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
tmp_file = tempfile.NamedTemporaryFile()
with open(tmp_file.name, 'wb') as f:
f.write(r.content)
with open(tmp_file.name, 'r') as f:
self.assertEqual('onionshare', f.read())
else:
self.assertFalse('a href="/test.txt"' in r.text)
if public_mode:
r = requests.get(download_file_url)
else:
r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
self.assertEqual(r.status_code, 404)
self.download_share(public_mode)
QtTest.QTest.qWait(2000)
def hit_401(self, public_mode): def hit_401(self, public_mode):
'''Test that the server stops after too many 401s, or doesn't when in public_mode''' '''Test that the server stops after too many 401s, or doesn't when in public_mode'''
url = "http://127.0.0.1:{}/".format(self.gui.app.port) url = "http://127.0.0.1:{}/".format(self.gui.app.port)
@ -101,11 +135,6 @@ class GuiShareTest(GuiBaseTest):
self.web_server_is_stopped() self.web_server_is_stopped()
def add_button_visible(self):
'''Test that the add button should be visible'''
self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible())
# 'Grouped' tests follow from here # 'Grouped' tests follow from here
def run_all_share_mode_setup_tests(self): def run_all_share_mode_setup_tests(self):
@ -117,7 +146,7 @@ class GuiShareTest(GuiBaseTest):
self.history_is_visible(self.gui.share_mode) self.history_is_visible(self.gui.share_mode)
self.deleting_all_files_hides_delete_button() self.deleting_all_files_hides_delete_button()
self.add_a_file_and_delete_using_its_delete_widget() self.add_a_file_and_delete_using_its_delete_widget()
self.file_selection_widget_readd_files() self.file_selection_widget_read_files()
def run_all_share_mode_started_tests(self, public_mode, startup_time=2000): def run_all_share_mode_started_tests(self, public_mode, startup_time=2000):
@ -142,11 +171,24 @@ class GuiShareTest(GuiBaseTest):
self.server_is_stopped(self.gui.share_mode, stay_open) self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped() self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
self.add_button_visible() self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode) self.server_working_on_start_button_pressed(self.gui.share_mode)
self.toggle_indicator_is_reset(self.gui.share_mode)
self.server_is_started(self.gui.share_mode) self.server_is_started(self.gui.share_mode)
self.history_indicator(self.gui.share_mode, public_mode) self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, 'Total size', public_mode)
self.individual_file_is_viewable_or_not(public_mode, stay_open)
self.history_widgets_present(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_is_started(self.gui.share_mode)
self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_tests(self, public_mode, stay_open): def run_all_share_mode_tests(self, public_mode, stay_open):
"""End-to-end share tests""" """End-to-end share tests"""
@ -154,6 +196,21 @@ class GuiShareTest(GuiBaseTest):
self.run_all_share_mode_started_tests(public_mode) self.run_all_share_mode_started_tests(public_mode)
self.run_all_share_mode_download_tests(public_mode, stay_open) self.run_all_share_mode_download_tests(public_mode, stay_open)
def run_all_clear_all_button_tests(self, public_mode, stay_open):
"""Test the Clear All history button"""
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(public_mode)
self.individual_file_is_viewable_or_not(public_mode, stay_open)
self.history_widgets_present(self.gui.share_mode)
self.clear_all_history_items(self.gui.share_mode, 0)
self.individual_file_is_viewable_or_not(public_mode, stay_open)
self.clear_all_history_items(self.gui.share_mode, 2)
def run_all_share_mode_individual_file_tests(self, public_mode, stay_open):
"""Tests in share mode when viewing an individual file"""
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(public_mode)
self.run_all_share_mode_individual_file_download_tests(public_mode, stay_open)
def run_all_large_file_tests(self, public_mode, stay_open): def run_all_large_file_tests(self, public_mode, stay_open):
"""Same as above but with a larger file""" """Same as above but with a larger file"""

100
tests/GuiWebsiteTest.py Normal file
View File

@ -0,0 +1,100 @@
import json
import os
import requests
import socks
import zipfile
import tempfile
from PyQt5 import QtCore, QtTest
from onionshare import strings
from onionshare.common import Common
from onionshare.settings import Settings
from onionshare.onion import Onion
from onionshare.web import Web
from onionshare_gui import Application, OnionShare, OnionShareGui
from .GuiShareTest import GuiShareTest
class GuiWebsiteTest(GuiShareTest):
@staticmethod
def set_up(test_settings):
'''Create GUI with given settings'''
# Create our test file
testfile = open('/tmp/index.html', 'w')
testfile.write('<html><body><p>This is a test website hosted by OnionShare</p></body></html>')
testfile.close()
common = Common()
common.settings = Settings(common)
common.define_css()
strings.load_strings(common)
# Get all of the settings in test_settings
test_settings['data_dir'] = '/tmp/OnionShare'
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
# Start the Onion
testonion = Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
open('/tmp/settings.json', 'w').write(json.dumps(test_settings))
gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/index.html'], '/tmp/settings.json', True)
return gui
@staticmethod
def tear_down():
'''Clean up after tests'''
try:
os.remove('/tmp/index.html')
os.remove('/tmp/settings.json')
except:
pass
def view_website(self, public_mode):
'''Test that we can download the share'''
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.website_mode.server_status.web.password))
QtTest.QTest.qWait(2000)
self.assertTrue('This is a test website hosted by OnionShare' in r.text)
def run_all_website_mode_setup_tests(self):
"""Tests in website mode prior to starting a share"""
self.click_mode(self.gui.website_mode)
self.file_selection_widget_has_files(1)
self.history_is_not_visible(self.gui.website_mode)
self.click_toggle_history(self.gui.website_mode)
self.history_is_visible(self.gui.website_mode)
def run_all_website_mode_started_tests(self, public_mode, startup_time=2000):
"""Tests in website mode after starting a share"""
self.server_working_on_start_button_pressed(self.gui.website_mode)
self.server_status_indicator_says_starting(self.gui.website_mode)
self.add_delete_buttons_hidden()
self.settings_button_is_hidden()
self.server_is_started(self.gui.website_mode, startup_time)
self.web_server_is_running()
self.have_a_password(self.gui.website_mode, public_mode)
self.url_description_shown(self.gui.website_mode)
self.have_copy_url_button(self.gui.website_mode, public_mode)
self.server_status_indicator_says_started(self.gui.website_mode)
def run_all_website_mode_download_tests(self, public_mode):
"""Tests in website mode after viewing the site"""
self.run_all_website_mode_setup_tests()
self.run_all_website_mode_started_tests(public_mode, startup_time=2000)
self.view_website(public_mode)
self.history_widgets_present(self.gui.website_mode)
self.server_is_stopped(self.gui.website_mode, False)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.website_mode, False)
self.add_button_visible(self.gui.website_mode)

View File

@ -67,7 +67,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
self.server_is_stopped(self.gui.share_mode, stay_open) self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped() self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
self.add_button_visible() self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode) self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_is_started(self.gui.share_mode, startup_time=45000) self.server_is_started(self.gui.share_mode, startup_time=45000)
self.history_indicator(self.gui.share_mode, public_mode) self.history_indicator(self.gui.share_mode, public_mode)

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeClearAllButtonTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_clear_all_button_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeClearAllButtonTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False,
}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_clear_all_button_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeIndividualFileViewStayOpenTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False,
}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_individual_file_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeIndividualFileViewTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": True,
}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_individual_file_tests(False, False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiWebsiteTest import GuiWebsiteTest
class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
cls.gui = GuiWebsiteTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiWebsiteTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self):
#self.run_all_common_setup_tests()
self.run_all_website_mode_download_tests(False)
if __name__ == "__main__":
unittest.main()