mirror of
https://github.com/onionshare/onionshare.git
synced 2024-12-28 08:49:30 -05:00
Merge branch 'master' of https://github.com/micahflee/onionshare
This commit is contained in:
commit
ebdc92bfa7
82
BUILD.md
82
BUILD.md
@ -1,51 +1,34 @@
|
||||
# Building OnionShare
|
||||
|
||||
## GNU/Linux
|
||||
|
||||
Start by getting a copy of the source code:
|
||||
Start by getting the source code:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/micahflee/onionshare.git
|
||||
cd onionshare
|
||||
```
|
||||
|
||||
*For .deb-based distros (like Debian, Ubuntu, Linux Mint):*
|
||||
## Linux
|
||||
|
||||
Then install the needed dependencies:
|
||||
Install the needed dependencies:
|
||||
|
||||
```sh
|
||||
sudo apt-get install -y python3-flask python3-stem python3-pyqt5 python-nautilus
|
||||
```
|
||||
For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-nose`
|
||||
|
||||
For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 nautilus-python`
|
||||
|
||||
After that you can try both the CLI and the GUI version of OnionShare:
|
||||
|
||||
```sh
|
||||
./install/scripts/onionshare
|
||||
./install/scripts/onionshare-gui
|
||||
./dev_scripts/onionshare
|
||||
./dev_scripts/onionshare-gui
|
||||
```
|
||||
|
||||
A script to build a .deb package and install OnionShare easily is also provided for your convenience:
|
||||
You can also build OnionShare packages to install:
|
||||
|
||||
```sh
|
||||
sudo apt-get install -y build-essential fakeroot python3-all python3-stdeb dh-python python-nautilus python3-nose
|
||||
./install/build_deb.sh
|
||||
sudo dpkg -i deb_dist/onionshare_*.deb
|
||||
```
|
||||
Note that OnionShare uses stdeb to generate Debian packages, and `python3-stdeb` is not available in Ubuntu 14.04 (Trusty). Because of this, you can't use the `build_install.sh` script to build the .deb file in versions of Ubuntu 14.04 and earlier. However, .deb files you build in later versions of Ubuntu will install and work fine in 14.04.
|
||||
Create a .deb on Debian-like distros: `./install/build_deb.sh`
|
||||
|
||||
*For .rpm-based distros (Red Hat, Fedora, CentOS):*
|
||||
Create a .rpm on Fedora-like distros: `./install/build_rpm.sh`
|
||||
|
||||
```sh
|
||||
sudo sudo dnf install -y rpm-build python3-flask python3-stem python3-qt5 nautilus-python
|
||||
./install/build_rpm.sh
|
||||
sudo yum install -y dist/onionshare-*.rpm
|
||||
```
|
||||
|
||||
Depending on your distribution, you may need to use `yum` instead of `dnf`.
|
||||
|
||||
*For ArchLinux:*
|
||||
|
||||
There is a PKBUILD available [here](https://aur.archlinux.org/packages/onionshare/) that can be used to install OnionShare.
|
||||
For ArchLinux: There is a PKBUILD available [here](https://aur.archlinux.org/packages/onionshare/) that can be used to install OnionShare.
|
||||
|
||||
## Mac OS X
|
||||
|
||||
@ -65,20 +48,22 @@ Install some dependencies using pip3:
|
||||
sudo pip3 install flask stem
|
||||
```
|
||||
|
||||
After that you can try both the CLI and the GUI version of OnionShare:
|
||||
|
||||
```sh
|
||||
./dev_scripts/onionshare
|
||||
./dev_scripts/onionshare-gui
|
||||
```
|
||||
|
||||
If you want to build a Mac OS X app bundle:
|
||||
|
||||
Install the latest development version of cx_Freeze:
|
||||
|
||||
* Download a [snapshot](https://bitbucket.org/anthony_tuininga/cx_freeze/downloads) of the latest development version of cx_Freeze, extract it, and cd into the folder you extracted it to
|
||||
* Build the package: `python3 setup.py bdist_wheel`
|
||||
* Install it with pip: `sudo pip3 install dist/cx_Freeze-5.0-cp35-cp35m-macosx_10_11_x86_64.whl`
|
||||
|
||||
Get the source code:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/micahflee/onionshare.git
|
||||
cd onionshare
|
||||
```
|
||||
|
||||
To build the .app:
|
||||
To build the app bundle:
|
||||
|
||||
```sh
|
||||
install/build_osx.sh
|
||||
@ -86,7 +71,7 @@ install/build_osx.sh
|
||||
|
||||
Now you should have `dist/OnionShare.app`.
|
||||
|
||||
To codesign and build a .pkg for distribution:
|
||||
To codesign and build a pkg for distribution:
|
||||
|
||||
```sh
|
||||
install/build_osx.sh --release
|
||||
@ -98,14 +83,23 @@ Now you should have `dist/OnionShare.pkg`.
|
||||
|
||||
### Setting up your dev environment
|
||||
|
||||
Download the latest Python 3.6.x, 32-bit (x86) from https://www.python.org/downloads/. I downloaded `python-3.6.0.exe`. When installing it, make sure to check the "Add Python 3.6 to PATH" checkbox on the first page of the installer.
|
||||
|
||||
Open a command prompt and install dependencies with pip: `pip install flask stem PyQt5`
|
||||
|
||||
Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-2.0.4-online.exe`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. I installed Qt 5.7.
|
||||
|
||||
After that you can try both the CLI and the GUI version of OnionShare:
|
||||
|
||||
```
|
||||
python dev_scripts\onionshare
|
||||
python dev_scripts\onionshare-gui
|
||||
```
|
||||
|
||||
If you want to build an .exe:
|
||||
|
||||
These instructions include adding folders to the path in Windows. To do this, go to Start and type "advanced system settings", and open "View advanced system settings" in the Control Panel. Click Environment Variables. Under "System variables" double-click on Path. From there you can add and remove folders that are available in the PATH.
|
||||
|
||||
Download the latest Python 3.5.x, 32-bit (x86) from https://www.python.org/downloads/. I downloaded `python-3.5.2.exe`. When installing it, make sure to check the "Add Python 3.5 to PATH" checkbox on the first page of the installer.
|
||||
|
||||
Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-2.0.3-1-online.exe`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x.
|
||||
|
||||
Open a command prompt and install dependencies with pip: `pip install pypiwin32 flask stem PyQt5`
|
||||
|
||||
Download and install the [Microsoft Visual C++ 2008 Redistributable Package (x86)](http://www.microsoft.com/en-us/download/details.aspx?id=29).
|
||||
|
||||
Installing cx_Freeze with support for Python 3.5 is annoying. Here are the steps (thanks https://github.com/sekrause/cx_Freeze-Wheels):
|
||||
|
20
README.md
20
README.md
@ -21,24 +21,8 @@ If you're interested in exactly what OnionShare does and does not protect agains
|
||||
|
||||
## Quick Start
|
||||
|
||||
Check out [the wiki](https://github.com/micahflee/onionshare/wiki) for information about how to use OnionShare and it's various features.
|
||||
|
||||
You can download OnionShare to install on your computer from <https://onionshare.org/>.
|
||||
|
||||
You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md).
|
||||
|
||||
## How to Use
|
||||
|
||||
Before you can share files, you need to open [Tor Browser](https://www.torproject.org/) in the background. This will provide the Tor service that OnionShare uses to start the onion service.
|
||||
|
||||
Open OnionShare and drag and drop files and folders you wish to share, and click Start Sharing. It will show you a .onion URL such as `http://asxmi4q6i7pajg2b.onion/egg-cain` and copy it to your clipboard. This is the secret URL that can be used to download the file you're sharing. If you'd like multiple people to be able to download this file, uncheck the "close automatically" checkbox.
|
||||
|
||||
Send this URL to the person you're trying to send the files to. If the files you're sending aren't secret, you can use normal means of sending the URL: emailing it, posting it to Facebook or Twitter, etc. If you're trying to send secret files then it's important to send this URL securely.
|
||||
|
||||
The person who is receiving the files doesn't need OnionShare. All they need is to open the URL you send them in Tor Browser to be able to download the file.
|
||||
|
||||
## Using the command line version
|
||||
|
||||
In Linux: Just run `onionshare` from the terminal.
|
||||
|
||||
In Windows: Add `C:\Program Files (x86)\OnionShare` to your PATH. Now you can run `onionshare.exe` in a command prompt.
|
||||
|
||||
In Mac OS X: Run `ln -s /Applications/OnionShare.app/Contents/MacOS/onionshare /usr/local/bin`. Now you can run `onionshare` from the terminal.
|
||||
|
28
dev_scripts/onionshare
Executable file
28
dev_scripts/onionshare
Executable file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
# Load onionshare module and resources from the source code tree
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.onionshare_dev_mode = True
|
||||
|
||||
import onionshare
|
||||
onionshare.main()
|
28
dev_scripts/onionshare-gui
Executable file
28
dev_scripts/onionshare-gui
Executable file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
# Load onionshare module and resources from the source code tree
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.onionshare_dev_mode = True
|
||||
|
||||
import onionshare_gui
|
||||
onionshare_gui.main()
|
@ -3,7 +3,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -3,7 +3,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -17,4 +17,195 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from .onionshare import *
|
||||
|
||||
import os, sys, time, argparse, shutil, socket, threading
|
||||
|
||||
from . import strings, helpers, web, onion
|
||||
|
||||
class OnionShare(object):
|
||||
"""
|
||||
OnionShare is the main application class. Pass in options and run
|
||||
start_onion_service and it will do the magic.
|
||||
"""
|
||||
def __init__(self, debug=False, local_only=False, stay_open=False, transparent_torification=False, stealth=False):
|
||||
self.port = None
|
||||
self.onion = None
|
||||
self.hidserv_dir = None
|
||||
self.onion_host = None
|
||||
|
||||
# files and dirs to delete on shutdown
|
||||
self.cleanup_filenames = []
|
||||
|
||||
# debug mode
|
||||
if debug:
|
||||
web.debug_mode()
|
||||
|
||||
# do not use tor -- for development
|
||||
self.local_only = local_only
|
||||
|
||||
# automatically close when download is finished
|
||||
self.stay_open = stay_open
|
||||
|
||||
# traffic automatically goes through Tor
|
||||
self.transparent_torification = transparent_torification
|
||||
|
||||
# use stealth onion service
|
||||
self.set_stealth(stealth)
|
||||
|
||||
def set_stealth(self, stealth):
|
||||
self.stealth = stealth
|
||||
if self.onion:
|
||||
self.onion.stealth = stealth
|
||||
|
||||
def choose_port(self):
|
||||
"""
|
||||
Pick an un-used port in the range 17600-17650 to bind to.
|
||||
"""
|
||||
# let the OS choose a port
|
||||
tmpsock = socket.socket()
|
||||
for port in range(17600, 17650):
|
||||
try:
|
||||
tmpsock.bind(("127.0.0.1", port))
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
self.port = tmpsock.getsockname()[1]
|
||||
tmpsock.close()
|
||||
|
||||
def start_onion_service(self):
|
||||
"""
|
||||
Start the onionshare onion service.
|
||||
"""
|
||||
if not self.port:
|
||||
self.choose_port()
|
||||
|
||||
if self.local_only:
|
||||
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
|
||||
return
|
||||
|
||||
if not self.onion:
|
||||
self.onion = onion.Onion(self.transparent_torification, self.stealth)
|
||||
|
||||
self.onion_host = self.onion.start(self.port)
|
||||
|
||||
if self.stealth:
|
||||
self.auth_string = self.onion.auth_string
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Shut everything down and clean up temporary files, etc.
|
||||
"""
|
||||
# cleanup files
|
||||
for filename in self.cleanup_filenames:
|
||||
if os.path.isfile(filename):
|
||||
os.remove(filename)
|
||||
elif os.path.isdir(filename):
|
||||
shutil.rmtree(filename)
|
||||
self.cleanup_filenames = []
|
||||
|
||||
# cleanup the onion
|
||||
if self.onion:
|
||||
self.onion.cleanup()
|
||||
|
||||
|
||||
def main(cwd=None):
|
||||
"""
|
||||
The main() function implements all of the logic that the command-line version of
|
||||
onionshare uses.
|
||||
"""
|
||||
strings.load_strings(helpers)
|
||||
print(strings._('version_string').format(helpers.get_version()))
|
||||
|
||||
# onionshare CLI in OSX needs to change current working directory (#132)
|
||||
if helpers.get_platform() == 'Darwin':
|
||||
if cwd:
|
||||
os.chdir(cwd)
|
||||
|
||||
# parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
||||
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
|
||||
parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification"))
|
||||
parser.add_argument('--stealth', action='store_true', dest='stealth', help=strings._("help_stealth"))
|
||||
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
||||
parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename'))
|
||||
args = parser.parse_args()
|
||||
|
||||
filenames = args.filename
|
||||
for i in range(len(filenames)):
|
||||
filenames[i] = os.path.abspath(filenames[i])
|
||||
|
||||
local_only = bool(args.local_only)
|
||||
debug = bool(args.debug)
|
||||
stay_open = bool(args.stay_open)
|
||||
transparent_torification = bool(args.transparent_torification)
|
||||
stealth = bool(args.stealth)
|
||||
|
||||
# validation
|
||||
valid = True
|
||||
for filename in filenames:
|
||||
if not os.path.exists(filename):
|
||||
print(strings._("not_a_file").format(filename))
|
||||
valid = False
|
||||
if not valid:
|
||||
sys.exit()
|
||||
|
||||
# start the onionshare app
|
||||
try:
|
||||
app = OnionShare(debug, local_only, stay_open, transparent_torification, stealth)
|
||||
app.choose_port()
|
||||
app.start_onion_service()
|
||||
except (onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorAutomatic, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile, onion.TorErrorAuthError) as e:
|
||||
sys.exit(e.args[0])
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
sys.exit()
|
||||
|
||||
# prepare files to share
|
||||
print(strings._("preparing_files"))
|
||||
web.set_file_info(filenames)
|
||||
app.cleanup_filenames.append(web.zip_filename)
|
||||
|
||||
# warn about sending large files over Tor
|
||||
if web.zip_filesize >= 157286400: # 150mb
|
||||
print('')
|
||||
print(strings._("large_filesize"))
|
||||
print('')
|
||||
|
||||
# start onionshare http service in new thread
|
||||
t = threading.Thread(target=web.start, args=(app.port, app.stay_open, app.transparent_torification))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
try: # Trap Ctrl-C
|
||||
# wait for hs, only if using old version of tor
|
||||
if not app.local_only and not app.onion.supports_ephemeral:
|
||||
ready = app.onion.wait_for_hs(app.onion_host)
|
||||
if not ready:
|
||||
sys.exit()
|
||||
else:
|
||||
# Wait for web.generate_slug() to finish running
|
||||
time.sleep(0.2)
|
||||
|
||||
if(stealth):
|
||||
print(strings._("give_this_url_stealth"))
|
||||
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
|
||||
print(app.auth_string)
|
||||
else:
|
||||
print(strings._("give_this_url"))
|
||||
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
|
||||
print('')
|
||||
print(strings._("ctrlc_to_stop"))
|
||||
|
||||
# wait for app to close
|
||||
while t.is_alive():
|
||||
# t.join() can't catch KeyboardInterrupt in such as Ubuntu
|
||||
t.join(0.5)
|
||||
except KeyboardInterrupt:
|
||||
web.stop(app.port)
|
||||
finally:
|
||||
# shutdown
|
||||
app.cleanup()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -34,14 +34,17 @@ def get_resource_path(filename):
|
||||
systemwide, and whether regardless of platform
|
||||
"""
|
||||
p = get_platform()
|
||||
if p == 'Linux' and sys.argv and sys.argv[0].startswith(sys.prefix):
|
||||
|
||||
if getattr(sys, 'onionshare_dev_mode', False):
|
||||
# Look for resources directory relative to python file
|
||||
resources_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'resources')
|
||||
|
||||
elif p == 'Linux' and sys.argv and sys.argv[0].startswith(sys.prefix):
|
||||
# OnionShare is installed systemwide in Linux
|
||||
resources_dir = os.path.join(sys.prefix, 'share/onionshare')
|
||||
elif getattr(sys, 'frozen', False): # Check if app is "frozen" with cx_Freeze
|
||||
# http://cx-freeze.readthedocs.io/en/latest/faq.html#using-data-files
|
||||
resources_dir = os.path.join(os.path.dirname(sys.executable), 'resources')
|
||||
else: # Look for resources directory relative to python file
|
||||
resources_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'resources')
|
||||
|
||||
return os.path.join(resources_dir, filename)
|
||||
|
||||
@ -176,19 +179,26 @@ class ZipWriter(object):
|
||||
with. If a zip_filename is not passed in, it will use the default onionshare
|
||||
filename.
|
||||
"""
|
||||
def __init__(self, zip_filename=None):
|
||||
def __init__(self, zip_filename=None, processed_size_callback=None):
|
||||
if zip_filename:
|
||||
self.zip_filename = zip_filename
|
||||
else:
|
||||
self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), random_string(4, 6))
|
||||
|
||||
self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
|
||||
self.processed_size_callback = processed_size_callback
|
||||
if self.processed_size_callback is None:
|
||||
self.processed_size_callback = lambda _: None
|
||||
self._size = 0
|
||||
self.processed_size_callback(self._size)
|
||||
|
||||
def add_file(self, filename):
|
||||
"""
|
||||
Add a file to the zip archive.
|
||||
"""
|
||||
self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
|
||||
self._size += os.path.getsize(filename)
|
||||
self.processed_size_callback(self._size)
|
||||
|
||||
def add_dir(self, filename):
|
||||
"""
|
||||
@ -201,6 +211,8 @@ class ZipWriter(object):
|
||||
if not os.path.islink(full_filename):
|
||||
arc_filename = full_filename[len(dir_to_strip):]
|
||||
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
|
||||
self._size += os.path.getsize(full_filename)
|
||||
self.processed_size_callback(self._size)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -20,15 +20,63 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from stem.control import Controller
|
||||
from stem import SocketError
|
||||
import os, sys, tempfile, shutil, urllib
|
||||
from stem.connection import MissingPassword, UnreadableCookieFile, AuthenticationFailure
|
||||
import os, sys, tempfile, shutil, urllib, platform
|
||||
|
||||
from . import socks
|
||||
from . import helpers, strings
|
||||
from .settings import Settings
|
||||
|
||||
class NoTor(Exception):
|
||||
class TorErrorAutomatic(Exception):
|
||||
"""
|
||||
This exception is raised if onionshare can't find a Tor control port
|
||||
to connect to, or if it can't find a Tor socks5 proxy to proxy though.
|
||||
OnionShare is failing to connect and authenticate to the Tor controller,
|
||||
using automatic settings that should work with Tor Browser.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorErrorInvalidSetting(Exception):
|
||||
"""
|
||||
This exception is raised if the settings just don't make sense.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorErrorSocketPort(Exception):
|
||||
"""
|
||||
OnionShare can't connect to the Tor controller using the supplied address and port.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorErrorSocketFile(Exception):
|
||||
"""
|
||||
OnionShare can't connect to the Tor controller using the supplied socket file.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorErrorMissingPassword(Exception):
|
||||
"""
|
||||
OnionShare connected to the Tor controller, but it requires a password.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorErrorUnreadableCookieFile(Exception):
|
||||
"""
|
||||
OnionShare connected to the Tor controller, but your user does not have permission
|
||||
to access the cookie file.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorErrorAuthError(Exception):
|
||||
"""
|
||||
OnionShare connected to the address and port, but can't authenticate. It's possible
|
||||
that a Tor controller isn't listening on this port.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorTooOld(Exception):
|
||||
"""
|
||||
This exception is raised if onionshare needs to use a feature of Tor or stem
|
||||
(like stealth ephemeral onion services) but the version you have installed
|
||||
is too old.
|
||||
"""
|
||||
pass
|
||||
|
||||
@ -47,48 +95,169 @@ class Onion(object):
|
||||
onion services are supported. If not, it falls back to modifying the
|
||||
Tor configuration.
|
||||
"""
|
||||
def __init__(self, transparent_torification=False):
|
||||
def __init__(self, transparent_torification=False, stealth=False, settings=False):
|
||||
self.transparent_torification = transparent_torification
|
||||
self.stealth = stealth
|
||||
|
||||
# Either use settings that are passed in, or load them from disk
|
||||
if settings:
|
||||
self.settings = settings
|
||||
else:
|
||||
self.settings = Settings()
|
||||
self.settings.load()
|
||||
|
||||
# files and dirs to delete on shutdown
|
||||
self.cleanup_filenames = []
|
||||
self.service_id = None
|
||||
|
||||
# connect to the tor controlport
|
||||
found_tor = False
|
||||
# Try to connect to Tor
|
||||
self.c = None
|
||||
env_port = os.environ.get('TOR_CONTROL_PORT')
|
||||
if env_port:
|
||||
ports = [int(env_port)]
|
||||
else:
|
||||
ports = [9151, 9153, 9051]
|
||||
for port in ports:
|
||||
|
||||
if self.settings.get('connection_type') == 'automatic':
|
||||
# Automatically try to guess the right way to connect to Tor Browser
|
||||
p = platform.system()
|
||||
|
||||
# Try connecting to control port
|
||||
found_tor = False
|
||||
|
||||
# If the TOR_CONTROL_PORT environment variable is set, use that
|
||||
env_port = os.environ.get('TOR_CONTROL_PORT')
|
||||
if env_port:
|
||||
try:
|
||||
self.c = Controller.from_port(port=int(env_port))
|
||||
found_tor = True
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
# Otherwise, try default ports for Tor Browser, Tor Messenger, and system tor
|
||||
try:
|
||||
ports = [9151, 9153, 9051]
|
||||
for port in ports:
|
||||
self.c = Controller.from_port(port=port)
|
||||
found_tor = True
|
||||
except:
|
||||
pass
|
||||
|
||||
# If this still didn't work, try guessing the default socket file path
|
||||
socket_file_path = ''
|
||||
if not found_tor:
|
||||
try:
|
||||
if p == 'Darwin':
|
||||
socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket')
|
||||
|
||||
self.c = Controller.from_socket_file(path=socket_file_path)
|
||||
found_tor = True
|
||||
except:
|
||||
pass
|
||||
|
||||
# If connecting to default control ports failed, so let's try
|
||||
# guessing the socket file name next
|
||||
if not found_tor:
|
||||
try:
|
||||
if p == 'Linux':
|
||||
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
|
||||
elif p == 'Darwin':
|
||||
# TODO: figure out the unix socket path in OS X
|
||||
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
|
||||
elif p == 'Windows':
|
||||
# Windows doesn't support unix sockets
|
||||
raise TorErrorAutomatic(strings._('settings_error_automatic'))
|
||||
|
||||
self.c = Controller.from_socket_file(path=socket_file_path)
|
||||
|
||||
except:
|
||||
raise TorErrorAutomatic(strings._('settings_error_automatic'))
|
||||
|
||||
# Try authenticating
|
||||
try:
|
||||
self.c = Controller.from_port(port=port)
|
||||
self.c.authenticate()
|
||||
found_tor = True
|
||||
break
|
||||
except SocketError:
|
||||
pass
|
||||
if not found_tor:
|
||||
raise NoTor(strings._("cant_connect_ctrlport").format(str(ports)))
|
||||
except:
|
||||
raise TorErrorAutomatic(strings._('settings_error_automatic'))
|
||||
|
||||
else:
|
||||
# Use specific settings to connect to tor
|
||||
|
||||
# Try connecting
|
||||
try:
|
||||
if self.settings.get('connection_type') == 'control_port':
|
||||
self.c = Controller.from_port(address=self.settings.get('control_port_address'), port=self.settings.get('control_port_port'))
|
||||
elif self.settings.get('connection_type') == 'socket_file':
|
||||
self.c = Controller.from_socket_file(path=self.settings.get('socket_file_path'))
|
||||
else:
|
||||
raise TorErrorInvalidSetting(strings._("settings_error_unknown"))
|
||||
|
||||
except:
|
||||
if self.settings.get('connection_type') == 'control_port':
|
||||
raise TorErrorSocketPort(strings._("settings_error_socket_port").format(self.settings.get('control_port_address'), self.settings.get('control_port_port')))
|
||||
else:
|
||||
raise TorErrorSocketFile(strings._("settings_error_socket_file").format(self.settings.get('socket_file_path')))
|
||||
|
||||
|
||||
# Try authenticating
|
||||
try:
|
||||
if self.settings.get('auth_type') == 'no_auth':
|
||||
self.c.authenticate()
|
||||
elif self.settings.get('auth_type') == 'password':
|
||||
self.c.authenticate(self.settings.get('auth_password'))
|
||||
else:
|
||||
raise TorErrorInvalidSetting(strings._("settings_error_unknown"))
|
||||
|
||||
except MissingPassword:
|
||||
raise TorErrorMissingPassword(strings._('settings_error_missing_password'))
|
||||
except UnreadableCookieFile:
|
||||
raise TorErrorUnreadableCookieFile(strings._('settings_error_unreadable_cookie_file'))
|
||||
except AuthenticationFailure:
|
||||
raise TorErrorAuthError(strings._('settings_error_auth').format(self.settings.get('control_port_address'), self.settings.get('control_port_port')))
|
||||
|
||||
# get the tor version
|
||||
self.tor_version = self.c.get_version().version_str
|
||||
|
||||
# do the versions of stem and tor that I'm using support ephemeral onion services?
|
||||
tor_version = self.c.get_version().version_str
|
||||
list_ephemeral_hidden_services = getattr(self.c, "list_ephemeral_hidden_services", None)
|
||||
self.supports_ephemeral = callable(list_ephemeral_hidden_services) and tor_version >= '0.2.7.1'
|
||||
self.supports_ephemeral = callable(list_ephemeral_hidden_services) and self.tor_version >= '0.2.7.1'
|
||||
|
||||
# do the versions of stem and tor that I'm using support stealth onion services?
|
||||
try:
|
||||
res = self.c.create_ephemeral_hidden_service({1:1}, basic_auth={'onionshare':None}, await_publication=False)
|
||||
tmp_service_id = res.content()[0][2].split('=')[1]
|
||||
self.c.remove_ephemeral_hidden_service(tmp_service_id)
|
||||
self.supports_stealth = True
|
||||
except:
|
||||
# ephemeral stealth onion services are not supported
|
||||
self.supports_stealth = False
|
||||
|
||||
def start(self, port):
|
||||
"""
|
||||
Start a onion service on port 80, pointing to the given port, and
|
||||
return the onion hostname.
|
||||
"""
|
||||
print(strings._("connecting_ctrlport").format(int(port)))
|
||||
self.auth_string = None
|
||||
if self.stealth and not self.supports_stealth:
|
||||
raise TorTooOld(strings._('error_stealth_not_supported'))
|
||||
|
||||
print(strings._("config_onion_service").format(int(port)))
|
||||
if self.supports_ephemeral:
|
||||
print(strings._('using_ephemeral'))
|
||||
res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication = True)
|
||||
|
||||
if self.stealth:
|
||||
basic_auth = {'onionshare':None}
|
||||
else:
|
||||
basic_auth = None
|
||||
|
||||
if basic_auth != None :
|
||||
res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth)
|
||||
else :
|
||||
# if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg
|
||||
res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True)
|
||||
|
||||
self.service_id = res.content()[0][2].split('=')[1]
|
||||
onion_host = self.service_id + '.onion'
|
||||
|
||||
if self.stealth:
|
||||
auth_cookie = res.content()[2][2].split('=')[1].split(':')[1]
|
||||
self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
|
||||
|
||||
return onion_host
|
||||
|
||||
else:
|
||||
|
@ -1,190 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os, sys, time, argparse, shutil, socket, threading
|
||||
|
||||
from . import strings, helpers, web, onion
|
||||
|
||||
class OnionShare(object):
|
||||
"""
|
||||
OnionShare is the main application class. Pass in options and run
|
||||
start_onion_service and it will do the magic.
|
||||
"""
|
||||
def __init__(self, debug=False, local_only=False, stay_open=False, transparent_torification=False):
|
||||
self.port = None
|
||||
self.onion = None
|
||||
self.hidserv_dir = None
|
||||
self.onion_host = None
|
||||
|
||||
# files and dirs to delete on shutdown
|
||||
self.cleanup_filenames = []
|
||||
|
||||
# debug mode
|
||||
if debug:
|
||||
web.debug_mode()
|
||||
|
||||
# do not use tor -- for development
|
||||
self.local_only = local_only
|
||||
|
||||
# automatically close when download is finished
|
||||
self.stay_open = stay_open
|
||||
|
||||
# traffic automatically goes through Tor
|
||||
self.transparent_torification = transparent_torification
|
||||
|
||||
def choose_port(self):
|
||||
"""
|
||||
Pick an un-used port in the range 17600-17650 to bind to.
|
||||
"""
|
||||
# let the OS choose a port
|
||||
tmpsock = socket.socket()
|
||||
for port in range(17600, 17650):
|
||||
try:
|
||||
tmpsock.bind(("127.0.0.1", port))
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
self.port = tmpsock.getsockname()[1]
|
||||
tmpsock.close()
|
||||
|
||||
def start_onion_service(self):
|
||||
"""
|
||||
Start the onionshare onion service.
|
||||
"""
|
||||
if not self.port:
|
||||
self.choose_port()
|
||||
|
||||
if self.local_only:
|
||||
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
|
||||
return
|
||||
|
||||
if not self.onion:
|
||||
self.onion = onion.Onion(self.transparent_torification)
|
||||
|
||||
self.onion_host = self.onion.start(self.port)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Shut everything down and clean up temporary files, etc.
|
||||
"""
|
||||
# cleanup files
|
||||
for filename in self.cleanup_filenames:
|
||||
if os.path.isfile(filename):
|
||||
os.remove(filename)
|
||||
elif os.path.isdir(filename):
|
||||
shutil.rmtree(filename)
|
||||
self.cleanup_filenames = []
|
||||
|
||||
# cleanup the onion
|
||||
if self.onion:
|
||||
self.onion.cleanup()
|
||||
|
||||
|
||||
def main(cwd=None):
|
||||
"""
|
||||
The main() function implements all of the logic that the command-line version of
|
||||
onionshare uses.
|
||||
"""
|
||||
strings.load_strings(helpers)
|
||||
print(strings._('version_string').format(helpers.get_version()))
|
||||
|
||||
# onionshare CLI in OSX needs to change current working directory (#132)
|
||||
if helpers.get_platform() == 'Darwin':
|
||||
if cwd:
|
||||
os.chdir(cwd)
|
||||
|
||||
# parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
||||
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
|
||||
parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification"))
|
||||
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
||||
parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename'))
|
||||
args = parser.parse_args()
|
||||
|
||||
filenames = args.filename
|
||||
for i in range(len(filenames)):
|
||||
filenames[i] = os.path.abspath(filenames[i])
|
||||
|
||||
local_only = bool(args.local_only)
|
||||
debug = bool(args.debug)
|
||||
stay_open = bool(args.stay_open)
|
||||
transparent_torification = bool(args.transparent_torification)
|
||||
|
||||
# validation
|
||||
valid = True
|
||||
for filename in filenames:
|
||||
if not os.path.exists(filename):
|
||||
print(strings._("not_a_file").format(filename))
|
||||
valid = False
|
||||
if not valid:
|
||||
sys.exit()
|
||||
|
||||
# start the onionshare app
|
||||
try:
|
||||
app = OnionShare(debug, local_only, stay_open, transparent_torification)
|
||||
app.choose_port()
|
||||
app.start_onion_service()
|
||||
except onion.NoTor as e:
|
||||
sys.exit(e.args[0])
|
||||
|
||||
# prepare files to share
|
||||
print(strings._("preparing_files"))
|
||||
web.set_file_info(filenames)
|
||||
app.cleanup_filenames.append(web.zip_filename)
|
||||
|
||||
# warn about sending large files over Tor
|
||||
if web.zip_filesize >= 157286400: # 150mb
|
||||
print('')
|
||||
print(strings._("large_filesize"))
|
||||
print('')
|
||||
|
||||
# start onionshare http service in new thread
|
||||
t = threading.Thread(target=web.start, args=(app.port, app.stay_open, app.transparent_torification))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
try: # Trap Ctrl-C
|
||||
# wait for hs, only if using old version of tor
|
||||
if not app.local_only and not app.onion.supports_ephemeral:
|
||||
ready = app.onion.wait_for_hs(app.onion_host)
|
||||
if not ready:
|
||||
sys.exit()
|
||||
else:
|
||||
# Wait for web.generate_slug() to finish running
|
||||
time.sleep(0.2)
|
||||
|
||||
print(strings._("give_this_url"))
|
||||
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
|
||||
print('')
|
||||
print(strings._("ctrlc_to_stop"))
|
||||
|
||||
# wait for app to close
|
||||
while t.is_alive():
|
||||
# t.join() can't catch KeyboardInterrupt in such as Ubuntu
|
||||
t.join(0.5)
|
||||
except KeyboardInterrupt:
|
||||
web.stop(app.port)
|
||||
finally:
|
||||
# shutdown
|
||||
app.cleanup()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
85
onionshare/settings.py
Normal file
85
onionshare/settings.py
Normal file
@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import platform, os, json
|
||||
|
||||
from . import strings, helpers
|
||||
|
||||
class Settings(object):
|
||||
"""
|
||||
This class stores all of the settings for OnionShare, specifically for how
|
||||
to connect to Tor. If it can't find the settings file, it uses the default,
|
||||
which is to attempt to connect automatically using default Tor Browser
|
||||
settings.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.filename = self.build_filename()
|
||||
|
||||
# These are the default settings. They will get overwritten when loading from disk
|
||||
self._settings = {
|
||||
'version': helpers.get_version(),
|
||||
'connection_type': 'automatic',
|
||||
'control_port_address': '127.0.0.1',
|
||||
'control_port_port': 9051,
|
||||
'socket_file_path': '/var/run/tor/control',
|
||||
'auth_type': 'no_auth',
|
||||
'auth_password': ''
|
||||
}
|
||||
|
||||
def build_filename(self):
|
||||
"""
|
||||
Returns the path of the settings file.
|
||||
"""
|
||||
p = platform.system()
|
||||
if p == 'Windows':
|
||||
appdata = os.environ['APPDATA']
|
||||
return '{}\\OnionShare\\onionshare.json'.format(appdata)
|
||||
elif p == 'Darwin':
|
||||
return os.path.expanduser('~/Library/Application Support/OnionShare/onionshare.json')
|
||||
else:
|
||||
return os.path.expanduser('~/.config/onionshare/onionshare.json')
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load the settings from file.
|
||||
"""
|
||||
# If the settings file exists, load it
|
||||
if os.path.exists(self.filename):
|
||||
try:
|
||||
self._settings = json.loads(open(self.filename, 'r').read())
|
||||
except:
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save settings to file.
|
||||
"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self.filename))
|
||||
except:
|
||||
pass
|
||||
open(self.filename, 'w').write(json.dumps(self._settings))
|
||||
print(strings._('settings_saved').format(self.filename))
|
||||
|
||||
def get(self, key):
|
||||
return self._settings[key]
|
||||
|
||||
def set(self, key, val):
|
||||
self._settings[key] = val
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -17,12 +17,29 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import queue, mimetypes, platform, os, sys, socket, logging, html
|
||||
from distutils.version import StrictVersion as Version
|
||||
import queue, mimetypes, platform, os, sys, socket, logging
|
||||
from urllib.request import urlopen
|
||||
|
||||
from flask import Flask, Response, request, render_template_string, abort
|
||||
from flask import __version__ as flask_version
|
||||
|
||||
from . import strings, helpers
|
||||
|
||||
|
||||
def _safe_select_jinja_autoescape(self, filename):
|
||||
if filename is None:
|
||||
return True
|
||||
return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
|
||||
|
||||
# Starting in Flask 0.11, render_template_string autoescapes template variables
|
||||
# by default. To prevent content injection through template variables in
|
||||
# earlier versions of Flask, we force autoescaping in the Jinja2 template
|
||||
# engine if we detect a Flask version with insecure default behavior.
|
||||
if Version(flask_version) < Version('0.11'):
|
||||
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
|
||||
Flask.select_jinja_autoescape = _safe_select_jinja_autoescape
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# information about the file
|
||||
@ -30,7 +47,7 @@ file_info = []
|
||||
zip_filename = None
|
||||
zip_filesize = None
|
||||
|
||||
def set_file_info(filenames):
|
||||
def set_file_info(filenames, processed_size_callback=None):
|
||||
"""
|
||||
Using the list of filenames being shared, fill in details that the web
|
||||
page will need to display. This includes zipping up the file in order to
|
||||
@ -41,11 +58,9 @@ def set_file_info(filenames):
|
||||
# build file info list
|
||||
file_info = {'files': [], 'dirs': []}
|
||||
for filename in filenames:
|
||||
# strips trailing '/' and sanitizes filename
|
||||
basename = html.escape(os.path.basename(filename.rstrip('/')))
|
||||
info = {
|
||||
'filename': filename,
|
||||
'basename': basename
|
||||
'basename': os.path.basename(filename.rstrip('/'))
|
||||
}
|
||||
if os.path.isfile(filename):
|
||||
info['size'] = os.path.getsize(filename)
|
||||
@ -55,13 +70,11 @@ def set_file_info(filenames):
|
||||
info['size'] = helpers.dir_size(filename)
|
||||
info['size_human'] = helpers.human_readable_filesize(info['size'])
|
||||
file_info['dirs'].append(info)
|
||||
|
||||
# sort list of files and directories by basename
|
||||
file_info['files'] = sorted(file_info['files'], key=lambda k: k['basename'])
|
||||
file_info['dirs'] = sorted(file_info['dirs'], key=lambda k: k['basename'])
|
||||
|
||||
# zip up the files and folders
|
||||
z = helpers.ZipWriter()
|
||||
z = helpers.ZipWriter(processed_size_callback=processed_size_callback)
|
||||
for info in file_info['files']:
|
||||
z.add_file(info['filename'])
|
||||
for info in file_info['dirs']:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -17,4 +17,445 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from .onionshare_gui import *
|
||||
from __future__ import division
|
||||
import os, sys, subprocess, inspect, platform, argparse, threading, time, math, inspect, platform
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
import onionshare
|
||||
from onionshare import strings, helpers, web
|
||||
|
||||
from .menu import Menu
|
||||
from .file_selection import FileSelection
|
||||
from .server_status import ServerStatus
|
||||
from .downloads import Downloads
|
||||
from .options import Options
|
||||
from .alert import Alert
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
"""
|
||||
This is Qt's QApplication class. It has been overridden to support threads
|
||||
and the quick keyboard shortcut.
|
||||
"""
|
||||
def __init__(self):
|
||||
platform = helpers.get_platform()
|
||||
if platform == 'Linux':
|
||||
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
|
||||
QtWidgets.QApplication.__init__(self, sys.argv)
|
||||
self.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if (event.type() == QtCore.QEvent.KeyPress and
|
||||
event.key() == QtCore.Qt.Key_Q and
|
||||
event.modifiers() == QtCore.Qt.ControlModifier):
|
||||
self.quit()
|
||||
return False
|
||||
|
||||
|
||||
class OnionShareGui(QtWidgets.QMainWindow):
|
||||
"""
|
||||
OnionShareGui is the main window for the GUI that contains all of the
|
||||
GUI elements.
|
||||
"""
|
||||
start_server_finished = QtCore.pyqtSignal()
|
||||
stop_server_finished = QtCore.pyqtSignal()
|
||||
starting_server_step2 = QtCore.pyqtSignal()
|
||||
starting_server_step3 = QtCore.pyqtSignal()
|
||||
starting_server_error = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, qtapp, app):
|
||||
super(OnionShareGui, self).__init__()
|
||||
self.qtapp = qtapp
|
||||
self.app = app
|
||||
|
||||
self.setWindowTitle('OnionShare')
|
||||
self.setWindowIcon(window_icon)
|
||||
|
||||
# the menu bar
|
||||
self.setMenuBar(Menu())
|
||||
|
||||
def send_files(self, filenames=None):
|
||||
"""
|
||||
Build the GUI in send files mode.
|
||||
Note that this is the only mode currently implemented.
|
||||
"""
|
||||
# file selection
|
||||
self.file_selection = FileSelection()
|
||||
if filenames:
|
||||
for filename in filenames:
|
||||
self.file_selection.file_list.add_file(filename)
|
||||
|
||||
# server status
|
||||
self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection)
|
||||
self.server_status.server_started.connect(self.file_selection.server_started)
|
||||
self.server_status.server_started.connect(self.start_server)
|
||||
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
|
||||
self.server_status.server_stopped.connect(self.stop_server)
|
||||
self.start_server_finished.connect(self.clear_message)
|
||||
self.start_server_finished.connect(self.server_status.start_server_finished)
|
||||
self.stop_server_finished.connect(self.server_status.stop_server_finished)
|
||||
self.file_selection.file_list.files_updated.connect(self.server_status.update)
|
||||
self.server_status.url_copied.connect(self.copy_url)
|
||||
self.server_status.hidservauth_copied.connect(self.copy_hidservauth)
|
||||
self.starting_server_step2.connect(self.start_server_step2)
|
||||
self.starting_server_step3.connect(self.start_server_step3)
|
||||
self.starting_server_error.connect(self.start_server_error)
|
||||
|
||||
# filesize warning
|
||||
self.filesize_warning = QtWidgets.QLabel()
|
||||
self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
|
||||
self.filesize_warning.hide()
|
||||
|
||||
# downloads
|
||||
self.downloads = Downloads()
|
||||
self.downloads_container = QtWidgets.QScrollArea()
|
||||
self.downloads_container.setWidget(self.downloads)
|
||||
self.downloads_container.setWidgetResizable(True)
|
||||
self.downloads_container.setMaximumHeight(200)
|
||||
self.vbar = self.downloads_container.verticalScrollBar()
|
||||
self.downloads_container.hide() # downloads start out hidden
|
||||
self.new_download = False
|
||||
|
||||
# options
|
||||
self.options = Options(web, self.app)
|
||||
|
||||
# status bar
|
||||
self.status_bar = QtWidgets.QStatusBar()
|
||||
self.status_bar.setSizeGripEnabled(False)
|
||||
version_label = QtWidgets.QLabel('v{0:s}'.format(helpers.get_version()))
|
||||
version_label.setStyleSheet('color: #666666; padding: 0 10px;')
|
||||
self.status_bar.addPermanentWidget(version_label)
|
||||
self.setStatusBar(self.status_bar)
|
||||
|
||||
# status bar, zip progress bar
|
||||
self._zip_progress_bar = None
|
||||
|
||||
# main layout
|
||||
self.layout = QtWidgets.QVBoxLayout()
|
||||
self.layout.addLayout(self.file_selection)
|
||||
self.layout.addLayout(self.server_status)
|
||||
self.layout.addWidget(self.filesize_warning)
|
||||
self.layout.addWidget(self.downloads_container)
|
||||
self.layout.addLayout(self.options)
|
||||
central_widget = QtWidgets.QWidget()
|
||||
central_widget.setLayout(self.layout)
|
||||
self.setCentralWidget(central_widget)
|
||||
self.show()
|
||||
|
||||
# check for requests frequently
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.check_for_requests)
|
||||
self.timer.start(500)
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
Start the onionshare server. This uses multiple threads to start the Tor onion
|
||||
server and the web app.
|
||||
"""
|
||||
# Reset web counters
|
||||
web.download_count = 0
|
||||
web.error404_count = 0
|
||||
web.set_gui_mode()
|
||||
|
||||
# pick an available local port for the http service to listen on
|
||||
self.app.choose_port()
|
||||
|
||||
# disable the stealth option
|
||||
self.options.set_advanced_enabled(False)
|
||||
|
||||
# start onionshare http service in new thread
|
||||
t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.app.transparent_torification))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
|
||||
time.sleep(0.2)
|
||||
|
||||
# start the onion service in a new thread
|
||||
def start_onion_service(self):
|
||||
self.status_bar.showMessage(strings._('gui_starting_server1', True))
|
||||
try:
|
||||
self.app.start_onion_service()
|
||||
self.starting_server_step2.emit()
|
||||
|
||||
except (onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorAutomatic, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile, onionshare.onion.TorErrorAuthError) as e:
|
||||
self.starting_server_error.emit(e.args[0])
|
||||
return
|
||||
|
||||
t = threading.Thread(target=start_onion_service, kwargs={'self': self})
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def start_server_step2(self):
|
||||
"""
|
||||
Step 2 in starting the onionshare server. Zipping up files.
|
||||
"""
|
||||
# add progress bar to the status bar, indicating the crunching of files.
|
||||
self._zip_progress_bar = ZipProgressBar(0)
|
||||
self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(
|
||||
self.file_selection.file_list.filenames)
|
||||
self.status_bar.clearMessage()
|
||||
self.status_bar.insertWidget(0, self._zip_progress_bar)
|
||||
|
||||
# prepare the files for sending in a new thread
|
||||
def finish_starting_server(self):
|
||||
# prepare files to share
|
||||
def _set_processed_size(x):
|
||||
if self._zip_progress_bar != None:
|
||||
self._zip_progress_bar.update_processed_size_signal.emit(x)
|
||||
web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size)
|
||||
self.app.cleanup_filenames.append(web.zip_filename)
|
||||
self.starting_server_step3.emit()
|
||||
|
||||
# wait for hs
|
||||
if not self.app.local_only and not self.app.onion.supports_ephemeral:
|
||||
self.status_bar.showMessage(strings._('gui_starting_server3', True))
|
||||
self.app.onion.wait_for_hs(self.app.onion_host)
|
||||
|
||||
# done
|
||||
self.start_server_finished.emit()
|
||||
|
||||
#self.status_bar.showMessage(strings._('gui_starting_server2', True))
|
||||
t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def start_server_step3(self):
|
||||
"""
|
||||
Step 3 in starting the onionshare server. This displays the large filesize
|
||||
warning, if applicable.
|
||||
"""
|
||||
# Remove zip progress bar
|
||||
if self._zip_progress_bar is not None:
|
||||
self.status_bar.removeWidget(self._zip_progress_bar)
|
||||
self._zip_progress_bar = None
|
||||
|
||||
# warn about sending large files over Tor
|
||||
if web.zip_filesize >= 157286400: # 150mb
|
||||
self.filesize_warning.setText(strings._("large_filesize", True))
|
||||
self.filesize_warning.show()
|
||||
|
||||
def start_server_error(self, error):
|
||||
"""
|
||||
If there's an error when trying to start the onion service
|
||||
"""
|
||||
Alert(error, QtWidgets.QMessageBox.Warning)
|
||||
self.server_status.stop_server()
|
||||
self.status_bar.clearMessage()
|
||||
|
||||
def stop_server(self):
|
||||
"""
|
||||
Stop the onionshare server.
|
||||
"""
|
||||
if self.server_status.status != self.server_status.STATUS_STOPPED:
|
||||
web.stop(self.app.port)
|
||||
self.app.cleanup()
|
||||
self.filesize_warning.hide()
|
||||
self.options.set_advanced_enabled(True)
|
||||
self.stop_server_finished.emit()
|
||||
|
||||
@staticmethod
|
||||
def _compute_total_size(filenames):
|
||||
total_size = 0
|
||||
for filename in filenames:
|
||||
if os.path.isfile(filename):
|
||||
total_size += os.path.getsize(filename)
|
||||
if os.path.isdir(filename):
|
||||
total_size += helpers.dir_size(filename)
|
||||
return total_size
|
||||
|
||||
def check_for_requests(self):
|
||||
"""
|
||||
Check for messages communicated from the web app, and update the GUI accordingly.
|
||||
"""
|
||||
self.update()
|
||||
# scroll to the bottom of the dl progress bar log pane
|
||||
# if a new download has been added
|
||||
if self.new_download:
|
||||
self.vbar.setValue(self.vbar.maximum())
|
||||
self.new_download = False
|
||||
# only check for requests if the server is running
|
||||
if self.server_status.status != self.server_status.STATUS_STARTED:
|
||||
return
|
||||
|
||||
events = []
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
try:
|
||||
r = web.q.get(False)
|
||||
events.append(r)
|
||||
except web.queue.Empty:
|
||||
done = True
|
||||
|
||||
for event in events:
|
||||
if event["type"] == web.REQUEST_LOAD:
|
||||
self.status_bar.showMessage(strings._('download_page_loaded', True))
|
||||
|
||||
elif event["type"] == web.REQUEST_DOWNLOAD:
|
||||
self.downloads_container.show() # show the downloads layout
|
||||
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
|
||||
self.new_download = True
|
||||
|
||||
elif event["type"] == web.REQUEST_RATE_LIMIT:
|
||||
self.stop_server()
|
||||
Alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical)
|
||||
|
||||
elif event["type"] == web.REQUEST_PROGRESS:
|
||||
self.downloads.update_download(event["data"]["id"], event["data"]["bytes"])
|
||||
|
||||
# is the download complete?
|
||||
if event["data"]["bytes"] == web.zip_filesize:
|
||||
# close on finish?
|
||||
if not web.get_stay_open():
|
||||
self.server_status.stop_server()
|
||||
|
||||
elif event["type"] == web.REQUEST_CANCELED:
|
||||
self.downloads.cancel_download(event["data"]["id"])
|
||||
|
||||
elif event["path"] != '/favicon.ico':
|
||||
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(web.error404_count, strings._('other_page_loaded', True), event["path"]))
|
||||
|
||||
def copy_url(self):
|
||||
"""
|
||||
When the URL gets copied to the clipboard, display this in the status bar.
|
||||
"""
|
||||
self.status_bar.showMessage(strings._('gui_copied_url', True), 2000)
|
||||
|
||||
def copy_hidservauth(self):
|
||||
"""
|
||||
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
|
||||
"""
|
||||
self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000)
|
||||
|
||||
def clear_message(self):
|
||||
"""
|
||||
Clear messages from the status bar.
|
||||
"""
|
||||
self.status_bar.clearMessage()
|
||||
|
||||
def closeEvent(self, e):
|
||||
if self.server_status.status != self.server_status.STATUS_STOPPED:
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
dialog.setWindowTitle("OnionShare")
|
||||
dialog.setText(strings._('gui_quit_warning', True))
|
||||
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
|
||||
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
|
||||
dialog.setDefaultButton(dont_quit_button)
|
||||
reply = dialog.exec_()
|
||||
|
||||
# Quit
|
||||
if reply == 0:
|
||||
self.stop_server()
|
||||
e.accept()
|
||||
# Don't Quit
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
|
||||
class ZipProgressBar(QtWidgets.QProgressBar):
|
||||
update_processed_size_signal = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, total_files_size):
|
||||
super(ZipProgressBar, self).__init__()
|
||||
self.setMaximumHeight(15)
|
||||
self.setMinimumWidth(200)
|
||||
self.setValue(0)
|
||||
self.setFormat(strings._('zip_progress_bar_format'))
|
||||
self.setStyleSheet(
|
||||
"QProgressBar::chunk { background-color: #05B8CC; } "
|
||||
)
|
||||
|
||||
self._total_files_size = total_files_size
|
||||
self._processed_size = 0
|
||||
|
||||
self.update_processed_size_signal.connect(self.update_processed_size)
|
||||
|
||||
@property
|
||||
def total_files_size(self):
|
||||
return self._total_files_size
|
||||
|
||||
@total_files_size.setter
|
||||
def total_files_size(self, val):
|
||||
self._total_files_size = val
|
||||
|
||||
@property
|
||||
def processed_size(self):
|
||||
return self._processed_size
|
||||
|
||||
@processed_size.setter
|
||||
def processed_size(self, val):
|
||||
self.update_processed_size(val)
|
||||
|
||||
def update_processed_size(self, val):
|
||||
self._processed_size = val
|
||||
if self.processed_size < self.total_files_size:
|
||||
self.setValue(int((self.processed_size * 100) / self.total_files_size))
|
||||
elif self.total_files_size != 0:
|
||||
self.setValue(100)
|
||||
else:
|
||||
self.setValue(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
The main() function implements all of the logic that the GUI version of onionshare uses.
|
||||
"""
|
||||
strings.load_strings(helpers)
|
||||
print(strings._('version_string').format(helpers.get_version()))
|
||||
|
||||
# start the Qt app
|
||||
global qtapp
|
||||
qtapp = Application()
|
||||
|
||||
# parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
||||
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
|
||||
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
||||
parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification"))
|
||||
parser.add_argument('--filenames', metavar='filenames', nargs='+', help=strings._('help_filename'))
|
||||
args = parser.parse_args()
|
||||
|
||||
filenames = args.filenames
|
||||
if filenames:
|
||||
for i in range(len(filenames)):
|
||||
filenames[i] = os.path.abspath(filenames[i])
|
||||
|
||||
local_only = bool(args.local_only)
|
||||
stay_open = bool(args.stay_open)
|
||||
debug = bool(args.debug)
|
||||
transparent_torification = bool(args.transparent_torification)
|
||||
|
||||
# create the onionshare icon
|
||||
global window_icon
|
||||
window_icon = QtGui.QIcon(helpers.get_resource_path('images/logo.png'))
|
||||
|
||||
# validation
|
||||
if filenames:
|
||||
valid = True
|
||||
for filename in filenames:
|
||||
if not os.path.exists(filename):
|
||||
Alert(strings._("not_a_file", True).format(filename))
|
||||
valid = False
|
||||
if not valid:
|
||||
sys.exit()
|
||||
|
||||
# start the onionshare app
|
||||
web.set_stay_open(stay_open)
|
||||
web.set_transparent_torification(transparent_torification)
|
||||
app = onionshare.OnionShare(debug, local_only, stay_open, transparent_torification)
|
||||
|
||||
# clean up when app quits
|
||||
def shutdown():
|
||||
app.cleanup()
|
||||
qtapp.aboutToQuit.connect(shutdown)
|
||||
|
||||
# launch the gui
|
||||
gui = OnionShareGui(qtapp, app)
|
||||
gui.send_files(filenames)
|
||||
|
||||
# all done
|
||||
sys.exit(qtapp.exec_())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
34
onionshare_gui/alert.py
Normal file
34
onionshare_gui/alert.py
Normal file
@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
from onionshare import helpers
|
||||
|
||||
class Alert(QtWidgets.QMessageBox):
|
||||
"""
|
||||
An alert box dialog.
|
||||
"""
|
||||
def __init__(self, message, icon=QtWidgets.QMessageBox.NoIcon):
|
||||
super(Alert, self).__init__(None)
|
||||
self.setWindowTitle("OnionShare")
|
||||
self.setWindowIcon(QtGui.QIcon(helpers.get_resource_path('images/logo.png')))
|
||||
self.setText(message)
|
||||
self.setIcon(icon)
|
||||
self.exec_()
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -56,7 +56,7 @@ class Download(object):
|
||||
elapsed = time.time() - self.started
|
||||
if elapsed < 10:
|
||||
# Wait a couple of seconds for the download rate to stabilize.
|
||||
# This prevents an "Windows copy dialog"-esque experience at
|
||||
# This prevents a "Windows copy dialog"-esque experience at
|
||||
# the beginning of the download.
|
||||
pb_fmt = strings._('gui_download_progress_starting').format(
|
||||
helpers.human_readable_filesize(downloaded_bytes))
|
||||
@ -77,33 +77,27 @@ class Download(object):
|
||||
self.started)
|
||||
|
||||
|
||||
class Downloads(QtWidgets.QVBoxLayout):
|
||||
class Downloads(QtWidgets.QWidget):
|
||||
"""
|
||||
The downloads chunk of the GUI. This lists all of the active download
|
||||
progress bars.
|
||||
"""
|
||||
def __init__(self):
|
||||
super(Downloads, self).__init__()
|
||||
|
||||
self.downloads = {}
|
||||
|
||||
# downloads label
|
||||
self.downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True))
|
||||
self.downloads_label.hide()
|
||||
|
||||
# add the widgets
|
||||
self.addWidget(self.downloads_label)
|
||||
self.layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def add_download(self, download_id, total_bytes):
|
||||
"""
|
||||
Add a new download progress bar.
|
||||
"""
|
||||
self.downloads_label.show()
|
||||
self.parent().show()
|
||||
|
||||
# add it to the list
|
||||
download = Download(download_id, total_bytes)
|
||||
self.downloads[download_id] = download
|
||||
self.addWidget(download.progress_bar)
|
||||
self.layout.insertWidget(-1, download.progress_bar)
|
||||
|
||||
def update_download(self, download_id, downloaded_bytes):
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
49
onionshare_gui/menu.py
Normal file
49
onionshare_gui/menu.py
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from onionshare import strings
|
||||
from .settings_dialog import SettingsDialog
|
||||
|
||||
class Menu(QtWidgets.QMenuBar):
|
||||
"""
|
||||
OnionShare's menu bar.
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super(Menu, self).__init__(parent)
|
||||
|
||||
file_menu = self.addMenu(strings._('gui_menu_file_menu', True))
|
||||
|
||||
settings_action = file_menu.addAction(strings._('gui_menu_settings_action', True))
|
||||
settings_action.triggered.connect(self.settings)
|
||||
quit_action = file_menu.addAction(strings._('gui_menu_quit_action', True))
|
||||
quit_action.triggered.connect(self.quit)
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Settings action triggered.
|
||||
"""
|
||||
SettingsDialog()
|
||||
|
||||
def quit(self):
|
||||
"""
|
||||
Quit action triggered.
|
||||
"""
|
||||
self.parent().qtapp.quit()
|
@ -1,371 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import division
|
||||
import os, sys, subprocess, inspect, platform, argparse, threading, time, math, inspect, platform
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
import onionshare
|
||||
from onionshare import strings, helpers, web
|
||||
|
||||
from .file_selection import FileSelection
|
||||
from .server_status import ServerStatus
|
||||
from .downloads import Downloads
|
||||
from .options import Options
|
||||
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
"""
|
||||
This is Qt's QApplication class. It has been overridden to support threads
|
||||
and the quick keyboard shortcut.
|
||||
"""
|
||||
def __init__(self):
|
||||
platform = helpers.get_platform()
|
||||
if platform == 'Linux':
|
||||
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
|
||||
QtWidgets.QApplication.__init__(self, sys.argv)
|
||||
self.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if (event.type() == QtCore.QEvent.KeyPress and
|
||||
event.key() == QtCore.Qt.Key_Q and
|
||||
event.modifiers() == QtCore.Qt.ControlModifier):
|
||||
self.quit()
|
||||
return False
|
||||
|
||||
|
||||
class OnionShareGui(QtWidgets.QMainWindow):
|
||||
"""
|
||||
OnionShareGui is the main window for the GUI that contains all of the
|
||||
GUI elements.
|
||||
"""
|
||||
start_server_finished = QtCore.pyqtSignal()
|
||||
stop_server_finished = QtCore.pyqtSignal()
|
||||
starting_server_step2 = QtCore.pyqtSignal()
|
||||
starting_server_step3 = QtCore.pyqtSignal()
|
||||
starting_server_error = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, qtapp, app):
|
||||
super(OnionShareGui, self).__init__()
|
||||
self.qtapp = qtapp
|
||||
self.app = app
|
||||
|
||||
self.setWindowTitle('OnionShare')
|
||||
self.setWindowIcon(window_icon)
|
||||
|
||||
def send_files(self, filenames=None):
|
||||
"""
|
||||
Build the GUI in send files mode.
|
||||
Note that this is the only mode currently implemented.
|
||||
"""
|
||||
# file selection
|
||||
self.file_selection = FileSelection()
|
||||
if filenames:
|
||||
for filename in filenames:
|
||||
self.file_selection.file_list.add_file(filename)
|
||||
|
||||
# server status
|
||||
self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection)
|
||||
self.server_status.server_started.connect(self.file_selection.server_started)
|
||||
self.server_status.server_started.connect(self.start_server)
|
||||
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
|
||||
self.server_status.server_stopped.connect(self.stop_server)
|
||||
self.start_server_finished.connect(self.clear_message)
|
||||
self.start_server_finished.connect(self.server_status.start_server_finished)
|
||||
self.stop_server_finished.connect(self.server_status.stop_server_finished)
|
||||
self.file_selection.file_list.files_updated.connect(self.server_status.update)
|
||||
self.server_status.url_copied.connect(self.copy_url)
|
||||
self.starting_server_step2.connect(self.start_server_step2)
|
||||
self.starting_server_step3.connect(self.start_server_step3)
|
||||
self.starting_server_error.connect(self.start_server_error)
|
||||
|
||||
# filesize warning
|
||||
self.filesize_warning = QtWidgets.QLabel()
|
||||
self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
|
||||
self.filesize_warning.hide()
|
||||
|
||||
# downloads
|
||||
self.downloads = Downloads()
|
||||
|
||||
# options
|
||||
self.options = Options(web, self.app)
|
||||
|
||||
# status bar
|
||||
self.status_bar = QtWidgets.QStatusBar()
|
||||
self.status_bar.setSizeGripEnabled(False)
|
||||
version_label = QtWidgets.QLabel('v{0:s}'.format(helpers.get_version()))
|
||||
version_label.setStyleSheet('color: #666666; padding: 0 10px;')
|
||||
self.status_bar.addPermanentWidget(version_label)
|
||||
self.setStatusBar(self.status_bar)
|
||||
|
||||
# main layout
|
||||
self.layout = QtWidgets.QVBoxLayout()
|
||||
self.layout.addLayout(self.file_selection)
|
||||
self.layout.addLayout(self.server_status)
|
||||
self.layout.addWidget(self.filesize_warning)
|
||||
self.layout.addLayout(self.downloads)
|
||||
self.layout.addLayout(self.options)
|
||||
central_widget = QtWidgets.QWidget()
|
||||
central_widget.setLayout(self.layout)
|
||||
self.setCentralWidget(central_widget)
|
||||
self.show()
|
||||
|
||||
# check for requests frequently
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.check_for_requests)
|
||||
self.timer.start(500)
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
Start the onionshare server. This uses multiple threads to start the Tor onion
|
||||
server and the web app.
|
||||
"""
|
||||
# Reset web counters
|
||||
web.download_count = 0
|
||||
web.error404_count = 0
|
||||
web.set_gui_mode()
|
||||
|
||||
# pick an available local port for the http service to listen on
|
||||
self.app.choose_port()
|
||||
|
||||
# start onionshare http service in new thread
|
||||
t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.app.transparent_torification))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
|
||||
time.sleep(0.2)
|
||||
|
||||
# start the onion service in a new thread
|
||||
def start_onion_service(self):
|
||||
self.status_bar.showMessage(strings._('gui_starting_server1', True))
|
||||
try:
|
||||
self.app.start_onion_service()
|
||||
self.starting_server_step2.emit()
|
||||
|
||||
except onionshare.onion.NoTor as e:
|
||||
self.starting_server_error.emit(e.args[0])
|
||||
return
|
||||
|
||||
t = threading.Thread(target=start_onion_service, kwargs={'self': self})
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def start_server_step2(self):
|
||||
"""
|
||||
Step 2 in starting the onionshare server. Prepare files for serving.
|
||||
"""
|
||||
# prepare the files for sending in a new thread
|
||||
def finish_starting_server(self):
|
||||
# prepare files to share
|
||||
web.set_file_info(self.file_selection.file_list.filenames)
|
||||
self.app.cleanup_filenames.append(web.zip_filename)
|
||||
self.starting_server_step3.emit()
|
||||
|
||||
# wait for hs
|
||||
if not self.app.local_only and not self.app.onion.supports_ephemeral:
|
||||
self.status_bar.showMessage(strings._('gui_starting_server3', True))
|
||||
self.app.onion.wait_for_hs(self.app.onion_host)
|
||||
|
||||
# done
|
||||
self.start_server_finished.emit()
|
||||
|
||||
self.status_bar.showMessage(strings._('gui_starting_server2', True))
|
||||
t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def start_server_step3(self):
|
||||
"""
|
||||
Step 3 in starting the onionshare server. This displays the large filesize
|
||||
warning, if applicable.
|
||||
"""
|
||||
# warn about sending large files over Tor
|
||||
if web.zip_filesize >= 157286400: # 150mb
|
||||
self.filesize_warning.setText(strings._("large_filesize", True))
|
||||
self.filesize_warning.show()
|
||||
|
||||
def start_server_error(self, error):
|
||||
"""
|
||||
If there's an error when trying to start the onion service
|
||||
"""
|
||||
alert(error, QtWidgets.QMessageBox.Warning)
|
||||
self.server_status.stop_server()
|
||||
self.status_bar.clearMessage()
|
||||
|
||||
def stop_server(self):
|
||||
"""
|
||||
Stop the onionshare server.
|
||||
"""
|
||||
if self.server_status.status != self.server_status.STATUS_STOPPED:
|
||||
web.stop(self.app.port)
|
||||
self.app.cleanup()
|
||||
self.filesize_warning.hide()
|
||||
self.stop_server_finished.emit()
|
||||
|
||||
def check_for_requests(self):
|
||||
"""
|
||||
Check for messages communicated from the web app, and update the GUI accordingly.
|
||||
"""
|
||||
self.update()
|
||||
# only check for requests if the server is running
|
||||
if self.server_status.status != self.server_status.STATUS_STARTED:
|
||||
return
|
||||
|
||||
events = []
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
try:
|
||||
r = web.q.get(False)
|
||||
events.append(r)
|
||||
except web.queue.Empty:
|
||||
done = True
|
||||
|
||||
for event in events:
|
||||
if event["type"] == web.REQUEST_LOAD:
|
||||
self.status_bar.showMessage(strings._('download_page_loaded', True))
|
||||
|
||||
elif event["type"] == web.REQUEST_DOWNLOAD:
|
||||
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
|
||||
|
||||
elif event["type"] == web.REQUEST_RATE_LIMIT:
|
||||
self.stop_server()
|
||||
alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical)
|
||||
|
||||
elif event["type"] == web.REQUEST_PROGRESS:
|
||||
self.downloads.update_download(event["data"]["id"], event["data"]["bytes"])
|
||||
|
||||
# is the download complete?
|
||||
if event["data"]["bytes"] == web.zip_filesize:
|
||||
# close on finish?
|
||||
if not web.get_stay_open():
|
||||
self.server_status.stop_server()
|
||||
|
||||
elif event["type"] == web.REQUEST_CANCELED:
|
||||
self.downloads.cancel_download(event["data"]["id"])
|
||||
|
||||
elif event["path"] != '/favicon.ico':
|
||||
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(web.error404_count, strings._('other_page_loaded', True), event["path"]))
|
||||
|
||||
def copy_url(self):
|
||||
"""
|
||||
When the URL gets copied to the clipboard, display this in the status bar.
|
||||
"""
|
||||
self.status_bar.showMessage(strings._('gui_copied_url', True), 2000)
|
||||
|
||||
def clear_message(self):
|
||||
"""
|
||||
Clear messages from the status bar.
|
||||
"""
|
||||
self.status_bar.clearMessage()
|
||||
|
||||
def closeEvent(self, e):
|
||||
if self.server_status.status != self.server_status.STATUS_STOPPED:
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
dialog.setWindowTitle("OnionShare")
|
||||
dialog.setText(strings._('gui_quit_warning', True))
|
||||
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
|
||||
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
|
||||
dialog.setDefaultButton(dont_quit_button)
|
||||
reply = dialog.exec_()
|
||||
|
||||
# Quit
|
||||
if reply == 0:
|
||||
self.stop_server()
|
||||
e.accept()
|
||||
# Don't Quit
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
|
||||
def alert(msg, icon=QtWidgets.QMessageBox.NoIcon):
|
||||
"""
|
||||
Pop up a message in a dialog window.
|
||||
"""
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
dialog.setWindowTitle("OnionShare")
|
||||
dialog.setWindowIcon(window_icon)
|
||||
dialog.setText(msg)
|
||||
dialog.setIcon(icon)
|
||||
dialog.exec_()
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
The main() function implements all of the logic that the GUI version of onionshare uses.
|
||||
"""
|
||||
strings.load_strings(helpers)
|
||||
print(strings._('version_string').format(helpers.get_version()))
|
||||
|
||||
# start the Qt app
|
||||
global qtapp
|
||||
qtapp = Application()
|
||||
|
||||
# parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
||||
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
|
||||
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
||||
parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification"))
|
||||
parser.add_argument('--filenames', metavar='filenames', nargs='+', help=strings._('help_filename'))
|
||||
args = parser.parse_args()
|
||||
|
||||
filenames = args.filenames
|
||||
if filenames:
|
||||
for i in range(len(filenames)):
|
||||
filenames[i] = os.path.abspath(filenames[i])
|
||||
|
||||
local_only = bool(args.local_only)
|
||||
stay_open = bool(args.stay_open)
|
||||
debug = bool(args.debug)
|
||||
transparent_torification = bool(args.transparent_torification)
|
||||
|
||||
# create the onionshare icon
|
||||
global window_icon
|
||||
window_icon = QtGui.QIcon(helpers.get_resource_path('images/logo.png'))
|
||||
|
||||
# validation
|
||||
if filenames:
|
||||
valid = True
|
||||
for filename in filenames:
|
||||
if not os.path.exists(filename):
|
||||
alert(strings._("not_a_file", True).format(filename))
|
||||
valid = False
|
||||
if not valid:
|
||||
sys.exit()
|
||||
|
||||
# start the onionshare app
|
||||
web.set_stay_open(stay_open)
|
||||
web.set_transparent_torification(transparent_torification)
|
||||
app = onionshare.OnionShare(debug, local_only, stay_open, transparent_torification)
|
||||
|
||||
# clean up when app quits
|
||||
def shutdown():
|
||||
app.cleanup()
|
||||
qtapp.aboutToQuit.connect(shutdown)
|
||||
|
||||
# launch the gui
|
||||
gui = OnionShareGui(qtapp, app)
|
||||
gui.send_files(filenames)
|
||||
|
||||
# all done
|
||||
sys.exit(qtapp.exec_())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -21,7 +21,7 @@ from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from onionshare import strings, helpers
|
||||
|
||||
class Options(QtWidgets.QHBoxLayout):
|
||||
class Options(QtWidgets.QVBoxLayout):
|
||||
"""
|
||||
The extra onionshare options in the GUI.
|
||||
"""
|
||||
@ -40,16 +40,59 @@ class Options(QtWidgets.QHBoxLayout):
|
||||
self.close_automatically.setText(strings._("close_on_finish", True))
|
||||
self.close_automatically.stateChanged.connect(self.stay_open_changed)
|
||||
|
||||
# stealth
|
||||
self.stealth = QtWidgets.QCheckBox()
|
||||
self.stealth.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.stealth.setText(strings._("gui_create_stealth", True))
|
||||
self.stealth.stateChanged.connect(self.stealth_changed)
|
||||
|
||||
# advanced options group
|
||||
self.advanced_group = QtWidgets.QGroupBox(strings._("gui_advanced_options", True))
|
||||
self.advanced_group.setCheckable(True)
|
||||
self.advanced_group.setChecked(False)
|
||||
self.advanced_group.setFlat(True)
|
||||
self.advanced_group.toggled.connect(self.advanced_options_changed)
|
||||
advanced_group_layout = QtWidgets.QVBoxLayout()
|
||||
advanced_group_layout.addWidget(self.stealth)
|
||||
self.advanced_group.setLayout(advanced_group_layout)
|
||||
|
||||
# add the widgets
|
||||
self.addWidget(self.close_automatically)
|
||||
self.addWidget(self.advanced_group)
|
||||
|
||||
def stay_open_changed(self, state):
|
||||
"""
|
||||
When the 'close automatically' checkbox is toggled, let the web app know.
|
||||
"""
|
||||
if state > 0:
|
||||
self.web.set_stay_open(False)
|
||||
self.app.stay_open = False
|
||||
else:
|
||||
if state == 0:
|
||||
self.web.set_stay_open(True)
|
||||
self.app.stay_open = True
|
||||
else:
|
||||
self.web.set_stay_open(False)
|
||||
self.app.stay_open = False
|
||||
|
||||
def advanced_options_changed(self, checked):
|
||||
"""
|
||||
When the 'advanced options' checkbox is unchecked, uncheck all advanced
|
||||
options, and let the onionshare app know.
|
||||
"""
|
||||
if not checked:
|
||||
self.stealth.setChecked(False)
|
||||
self.app.set_stealth(False)
|
||||
|
||||
def stealth_changed(self, state):
|
||||
"""
|
||||
When the 'stealth' checkbox is toggled, let the onionshare app know.
|
||||
"""
|
||||
if state == 2:
|
||||
self.app.set_stealth(True)
|
||||
else:
|
||||
self.app.set_stealth(False)
|
||||
|
||||
def set_advanced_enabled(self, enabled):
|
||||
"""
|
||||
You cannot toggle stealth after an onion service has started. This method
|
||||
disables and re-enabled the advanced options group, including the stealth
|
||||
checkbox.
|
||||
"""
|
||||
self.advanced_group.setEnabled(enabled)
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -29,6 +29,7 @@ class ServerStatus(QtWidgets.QVBoxLayout):
|
||||
server_started = QtCore.pyqtSignal()
|
||||
server_stopped = QtCore.pyqtSignal()
|
||||
url_copied = QtCore.pyqtSignal()
|
||||
hidservauth_copied = QtCore.pyqtSignal()
|
||||
|
||||
STATUS_STOPPED = 0
|
||||
STATUS_WORKING = 1
|
||||
@ -63,9 +64,12 @@ class ServerStatus(QtWidgets.QVBoxLayout):
|
||||
self.url_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
|
||||
self.copy_url_button.clicked.connect(self.copy_url)
|
||||
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
|
||||
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
|
||||
url_layout = QtWidgets.QHBoxLayout()
|
||||
url_layout.addWidget(self.url_label)
|
||||
url_layout.addWidget(self.copy_url_button)
|
||||
url_layout.addWidget(self.copy_hidservauth_button)
|
||||
|
||||
# add the widgets
|
||||
self.addLayout(server_layout)
|
||||
@ -91,12 +95,18 @@ class ServerStatus(QtWidgets.QVBoxLayout):
|
||||
self.url_label.show()
|
||||
self.copy_url_button.show()
|
||||
|
||||
if self.app.stealth:
|
||||
self.copy_hidservauth_button.show()
|
||||
else:
|
||||
self.copy_hidservauth_button.hide()
|
||||
|
||||
# resize parent widget
|
||||
p = self.parentWidget()
|
||||
p.resize(p.sizeHint())
|
||||
else:
|
||||
self.url_label.hide()
|
||||
self.copy_url_button.hide()
|
||||
self.copy_hidservauth_button.hide()
|
||||
|
||||
# button
|
||||
if self.file_selection.get_num_files() == 0:
|
||||
@ -163,3 +173,12 @@ class ServerStatus(QtWidgets.QVBoxLayout):
|
||||
clipboard.setText(url)
|
||||
|
||||
self.url_copied.emit()
|
||||
|
||||
def copy_hidservauth(self):
|
||||
"""
|
||||
Copy the HidServAuth line to the clipboard.
|
||||
"""
|
||||
clipboard = self.qtapp.clipboard()
|
||||
clipboard.setText(self.app.auth_string)
|
||||
|
||||
self.hidservauth_copied.emit()
|
||||
|
260
onionshare_gui/settings_dialog.py
Normal file
260
onionshare_gui/settings_dialog.py
Normal file
@ -0,0 +1,260 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
from onionshare import strings
|
||||
from onionshare.settings import Settings
|
||||
from onionshare.onion import Onion, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError
|
||||
|
||||
from .alert import Alert
|
||||
|
||||
class SettingsDialog(QtWidgets.QDialog):
|
||||
"""
|
||||
Settings dialog.
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super(SettingsDialog, self).__init__(parent)
|
||||
|
||||
self.setModal(True)
|
||||
self.setWindowTitle(strings._('gui_settings_window_title', True))
|
||||
|
||||
# Connection type: either automatic, control port, or socket file
|
||||
|
||||
# Automatic
|
||||
self.connection_type_automatic_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_automatic_option', True))
|
||||
self.connection_type_automatic_radio.toggled.connect(self.connection_type_automatic_toggled)
|
||||
|
||||
# Control port
|
||||
self.connection_type_control_port_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_control_port_option', True))
|
||||
self.connection_type_control_port_radio.toggled.connect(self.connection_type_control_port_toggled)
|
||||
|
||||
connection_type_control_port_extras_label = QtWidgets.QLabel(strings._('gui_settings_control_port_label', True))
|
||||
self.connection_type_control_port_extras_address = QtWidgets.QLineEdit()
|
||||
self.connection_type_control_port_extras_port = QtWidgets.QLineEdit()
|
||||
connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout()
|
||||
connection_type_control_port_extras_layout.addWidget(connection_type_control_port_extras_label)
|
||||
connection_type_control_port_extras_layout.addWidget(self.connection_type_control_port_extras_address)
|
||||
connection_type_control_port_extras_layout.addWidget(self.connection_type_control_port_extras_port)
|
||||
|
||||
self.connection_type_control_port_extras = QtWidgets.QWidget()
|
||||
self.connection_type_control_port_extras.setLayout(connection_type_control_port_extras_layout)
|
||||
self.connection_type_control_port_extras.hide()
|
||||
|
||||
# Socket file
|
||||
self.connection_type_socket_file_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_socket_file_option', True))
|
||||
self.connection_type_socket_file_radio.toggled.connect(self.connection_type_socket_file_toggled)
|
||||
|
||||
connection_type_socket_file_extras_label = QtWidgets.QLabel(strings._('gui_settings_socket_file_label', True))
|
||||
self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit()
|
||||
connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout()
|
||||
connection_type_socket_file_extras_layout.addWidget(connection_type_socket_file_extras_label)
|
||||
connection_type_socket_file_extras_layout.addWidget(self.connection_type_socket_file_extras_path)
|
||||
|
||||
self.connection_type_socket_file_extras = QtWidgets.QWidget()
|
||||
self.connection_type_socket_file_extras.setLayout(connection_type_socket_file_extras_layout)
|
||||
self.connection_type_socket_file_extras.hide()
|
||||
|
||||
# Connection type layout
|
||||
connection_type_group_layout = QtWidgets.QVBoxLayout()
|
||||
connection_type_group_layout.addWidget(self.connection_type_automatic_radio)
|
||||
connection_type_group_layout.addWidget(self.connection_type_control_port_radio)
|
||||
connection_type_group_layout.addWidget(self.connection_type_socket_file_radio)
|
||||
connection_type_group_layout.addWidget(self.connection_type_control_port_extras)
|
||||
connection_type_group_layout.addWidget(self.connection_type_socket_file_extras)
|
||||
connection_type_group = QtWidgets.QGroupBox(strings._("gui_settings_connection_type_label", True))
|
||||
connection_type_group.setLayout(connection_type_group_layout)
|
||||
|
||||
|
||||
# Authentication options
|
||||
|
||||
# No authentication
|
||||
self.authenticate_no_auth_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_no_auth_option', True))
|
||||
self.authenticate_no_auth_radio.toggled.connect(self.authenticate_no_auth_toggled)
|
||||
|
||||
# Password
|
||||
self.authenticate_password_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_password_option', True))
|
||||
self.authenticate_password_radio.toggled.connect(self.authenticate_password_toggled)
|
||||
|
||||
authenticate_password_extras_label = QtWidgets.QLabel(strings._('gui_settings_password_label', True))
|
||||
self.authenticate_password_extras_password = QtWidgets.QLineEdit('')
|
||||
authenticate_password_extras_layout = QtWidgets.QHBoxLayout()
|
||||
authenticate_password_extras_layout.addWidget(authenticate_password_extras_label)
|
||||
authenticate_password_extras_layout.addWidget(self.authenticate_password_extras_password)
|
||||
|
||||
self.authenticate_password_extras = QtWidgets.QWidget()
|
||||
self.authenticate_password_extras.setLayout(authenticate_password_extras_layout)
|
||||
self.authenticate_password_extras.hide()
|
||||
|
||||
# Authentication options layout
|
||||
authenticate_group_layout = QtWidgets.QVBoxLayout()
|
||||
authenticate_group_layout.addWidget(self.authenticate_no_auth_radio)
|
||||
authenticate_group_layout.addWidget(self.authenticate_password_radio)
|
||||
authenticate_group_layout.addWidget(self.authenticate_password_extras)
|
||||
self.authenticate_group = QtWidgets.QGroupBox(strings._("gui_settings_authenticate_label", True))
|
||||
self.authenticate_group.setLayout(authenticate_group_layout)
|
||||
|
||||
|
||||
# Buttons
|
||||
test_button = QtWidgets.QPushButton(strings._('gui_settings_button_test', True))
|
||||
test_button.clicked.connect(self.test_clicked)
|
||||
save_button = QtWidgets.QPushButton(strings._('gui_settings_button_save', True))
|
||||
save_button.clicked.connect(self.save_clicked)
|
||||
cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
|
||||
cancel_button.clicked.connect(self.cancel_clicked)
|
||||
buttons_layout = QtWidgets.QHBoxLayout()
|
||||
buttons_layout.addWidget(test_button)
|
||||
buttons_layout.addWidget(save_button)
|
||||
buttons_layout.addWidget(cancel_button)
|
||||
|
||||
# Layout
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(connection_type_group)
|
||||
layout.addWidget(self.authenticate_group)
|
||||
layout.addStretch()
|
||||
layout.addLayout(buttons_layout)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
# Load settings, and fill them in
|
||||
settings = Settings()
|
||||
settings.load()
|
||||
|
||||
connection_type = settings.get('connection_type')
|
||||
if connection_type == 'automatic':
|
||||
self.connection_type_automatic_radio.setChecked(True)
|
||||
elif connection_type == 'control_port':
|
||||
self.connection_type_control_port_radio.setChecked(True)
|
||||
elif connection_type == 'socket_file':
|
||||
self.connection_type_socket_file_radio.setChecked(True)
|
||||
self.connection_type_control_port_extras_address.setText(settings.get('control_port_address'))
|
||||
self.connection_type_control_port_extras_port.setText(str(settings.get('control_port_port')))
|
||||
self.connection_type_socket_file_extras_path.setText(settings.get('socket_file_path'))
|
||||
auth_type = settings.get('auth_type')
|
||||
if auth_type == 'no_auth':
|
||||
self.authenticate_no_auth_radio.setChecked(True)
|
||||
elif auth_type == 'password':
|
||||
self.authenticate_password_radio.setChecked(True)
|
||||
self.authenticate_password_extras_password.setText(settings.get('auth_password'))
|
||||
|
||||
# Show the dialog
|
||||
self.exec_()
|
||||
|
||||
def connection_type_automatic_toggled(self, checked):
|
||||
"""
|
||||
Connection type automatic was toggled. If checked, disable all other
|
||||
fields. If unchecked, enable all other fields.
|
||||
"""
|
||||
if checked:
|
||||
self.authenticate_group.setEnabled(False)
|
||||
else:
|
||||
self.authenticate_group.setEnabled(True)
|
||||
|
||||
def connection_type_control_port_toggled(self, checked):
|
||||
"""
|
||||
Connection type control port was toggled. If checked, show extra fields
|
||||
for Tor control address and port. If unchecked, hide those extra fields.
|
||||
"""
|
||||
if checked:
|
||||
self.connection_type_control_port_extras.show()
|
||||
else:
|
||||
self.connection_type_control_port_extras.hide()
|
||||
|
||||
|
||||
def connection_type_socket_file_toggled(self, checked):
|
||||
"""
|
||||
Connection type socket file was toggled. If checked, show extra fields
|
||||
for socket file. If unchecked, hide those extra fields.
|
||||
"""
|
||||
if checked:
|
||||
self.connection_type_socket_file_extras.show()
|
||||
else:
|
||||
self.connection_type_socket_file_extras.hide()
|
||||
|
||||
def authenticate_no_auth_toggled(self, checked):
|
||||
"""
|
||||
Authentication option no authentication was toggled.
|
||||
"""
|
||||
pass
|
||||
|
||||
def authenticate_password_toggled(self, checked):
|
||||
"""
|
||||
Authentication option password was toggled. If checked, show extra fields
|
||||
for password auth. If unchecked, hide those extra fields.
|
||||
"""
|
||||
if checked:
|
||||
self.authenticate_password_extras.show()
|
||||
else:
|
||||
self.authenticate_password_extras.hide()
|
||||
|
||||
def test_clicked(self):
|
||||
"""
|
||||
Test Settings button clicked. With the given settings, see if we can
|
||||
successfully connect and authenticate to Tor.
|
||||
"""
|
||||
settings = self.settings_from_fields()
|
||||
|
||||
try:
|
||||
onion = Onion(settings=settings)
|
||||
|
||||
# If an exception hasn't been raised yet, the Tor settings work
|
||||
Alert(strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth))
|
||||
|
||||
except (TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError) as e:
|
||||
Alert(e.args[0], QtWidgets.QMessageBox.Warning)
|
||||
|
||||
def save_clicked(self):
|
||||
"""
|
||||
Save button clicked. Save current settings to disk.
|
||||
"""
|
||||
settings = self.settings_from_fields()
|
||||
settings.save()
|
||||
self.close()
|
||||
|
||||
def cancel_clicked(self):
|
||||
"""
|
||||
Cancel button clicked.
|
||||
"""
|
||||
self.close()
|
||||
|
||||
def settings_from_fields(self):
|
||||
"""
|
||||
Return a Settings object that's full of values from the settings dialog.
|
||||
"""
|
||||
settings = Settings()
|
||||
|
||||
if self.connection_type_automatic_radio.isChecked():
|
||||
settings.set('connection_type', 'automatic')
|
||||
if self.connection_type_control_port_radio.isChecked():
|
||||
settings.set('connection_type', 'control_port')
|
||||
if self.connection_type_socket_file_radio.isChecked():
|
||||
settings.set('connection_type', 'socket_file')
|
||||
|
||||
settings.set('control_port_address', self.connection_type_control_port_extras_address.text())
|
||||
settings.set('control_port_port', int(self.connection_type_control_port_extras_port.text()))
|
||||
settings.set('socket_file_path', self.connection_type_socket_file_extras_path.text())
|
||||
|
||||
if self.authenticate_no_auth_radio.isChecked():
|
||||
settings.set('auth_type', 'no_auth')
|
||||
if self.authenticate_password_radio.isChecked():
|
||||
settings.set('auth_type', 'password')
|
||||
|
||||
settings.set('auth_password', self.authenticate_password_extras_password.text())
|
||||
|
||||
return settings
|
@ -1,4 +1,4 @@
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
@ -41,5 +41,7 @@
|
||||
"gui_please_wait": "Prosím čekejte...",
|
||||
"error_hs_dir_cannot_create": "Nejde vytvořit složka {0:s} pro onion service",
|
||||
"error_hs_dir_not_writable": "Nejde zapisovat do složky {0:s} pro onion service",
|
||||
"using_ephemeral": "Staring ephemeral Tor onion service and awaiting publication"
|
||||
"using_ephemeral": "Staring ephemeral Tor onion service and awaiting publication",
|
||||
"zip_progress_bar_format": "Crunching files: %p%"
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
{
|
||||
"connecting_ctrlport": "Connecting to Tor control port to set up onion service on port {0:d}.",
|
||||
"cant_connect_ctrlport": "Can't connect to Tor control port on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.",
|
||||
"cant_connect_socksport": "Can't connect to Tor SOCKS5 server on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.",
|
||||
"config_onion_service": "Configuring onion service on port {0:d}.",
|
||||
"preparing_files": "Preparing files to share.",
|
||||
"wait_for_hs": "Waiting for HS to be ready:",
|
||||
"wait_for_hs_trying": "Trying...",
|
||||
"wait_for_hs_nope": "Not ready yet.",
|
||||
"wait_for_hs_yup": "Ready!",
|
||||
"give_this_url": "Give this URL to the person you're sending the file to:",
|
||||
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
|
||||
"ctrlc_to_stop": "Press Ctrl-C to stop server",
|
||||
"not_a_file": "{0:s} is not a file.",
|
||||
"download_page_loaded": "Download page loaded",
|
||||
@ -17,10 +16,10 @@
|
||||
"large_filesize": "Warning: Sending large files could take hours",
|
||||
"error_tails_invalid_port": "Invalid value, port must be an integer",
|
||||
"error_tails_unknown_root": "Unknown error with Tails root process",
|
||||
"help_tails_port": "Tails only: port for opening firewall, starting onion service",
|
||||
"help_local_only": "Do not attempt to use tor: for development only",
|
||||
"help_stay_open": "Keep onion service running after download has finished",
|
||||
"help_transparent_torification": "My system is transparently torified",
|
||||
"help_stealth": "Create stealth onion service (advanced)",
|
||||
"help_debug": "Log errors to disk",
|
||||
"help_filename": "List of files or folders to share",
|
||||
"gui_drag_and_drop": "Drag and drop\nfiles here",
|
||||
@ -32,9 +31,11 @@
|
||||
"gui_start_server": "Start Sharing",
|
||||
"gui_stop_server": "Stop Sharing",
|
||||
"gui_copy_url": "Copy URL",
|
||||
"gui_copy_hidservauth": "Copy HidServAuth",
|
||||
"gui_downloads": "Downloads:",
|
||||
"gui_canceled": "Canceled",
|
||||
"gui_copied_url": "Copied URL to clipboard",
|
||||
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
|
||||
"gui_starting_server1": "Starting Tor onion service...",
|
||||
"gui_starting_server2": "Crunching files...",
|
||||
"gui_starting_server3": "Waiting for Tor onion service...",
|
||||
@ -49,5 +50,37 @@
|
||||
"gui_quit_warning": "Are you sure you want to quit?\nThe URL you are sharing won't exist anymore.",
|
||||
"gui_quit_warning_quit": "Quit",
|
||||
"gui_quit_warning_dont_quit": "Don't Quit",
|
||||
"error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL."
|
||||
"error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.",
|
||||
"zip_progress_bar_format": "Crunching files: %p%",
|
||||
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
|
||||
"gui_advanced_options": "Advanced options",
|
||||
"gui_create_stealth": "Create stealth onion service",
|
||||
"gui_menu_file_menu": "&File",
|
||||
"gui_menu_settings_action": "&Settings",
|
||||
"gui_menu_quit_action": "&Quit",
|
||||
"gui_settings_window_title": "Settings",
|
||||
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
|
||||
"gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser",
|
||||
"gui_settings_connection_type_control_port_option": "Connect using control port",
|
||||
"gui_settings_connection_type_socket_file_option": "Connect using socket file",
|
||||
"gui_settings_control_port_label": "Control port",
|
||||
"gui_settings_socket_file_label": "Socket file",
|
||||
"gui_settings_authenticate_label": "Tor authentication options",
|
||||
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
|
||||
"gui_settings_authenticate_password_option": "Password",
|
||||
"gui_settings_authenticate_cookie_option": "Cookie",
|
||||
"gui_settings_password_label": "Password",
|
||||
"gui_settings_cookie_label": "Cookie path",
|
||||
"gui_settings_button_test": "Test Settings",
|
||||
"gui_settings_button_save": "Save",
|
||||
"gui_settings_button_cancel": "Cancel",
|
||||
"settings_saved": "Settings saved to {}",
|
||||
"settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.",
|
||||
"settings_error_automatic": "Can't connect to Tor controller. Is Tor Browser running in the background? If you don't have it you can get it from:\nhttps://www.torproject.org/.",
|
||||
"settings_error_socket_port": "Can't connect to Tor controller on {}:{}.",
|
||||
"settings_error_socket_file": "Can't connect to Tor controller using socket file {}.",
|
||||
"settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?",
|
||||
"settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.",
|
||||
"settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user doesn't have permission to read the cookie file.",
|
||||
"settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}"
|
||||
}
|
||||
|
@ -41,5 +41,6 @@
|
||||
"gui_please_wait": "Bonvolu atendi...",
|
||||
"error_hs_dir_cannot_create": "Ne eblas krei hidden-service-dosierujon {0:s}",
|
||||
"error_hs_dir_not_writable": "Ne eblas konservi dosierojn en hidden-service-dosierujo {0:s}",
|
||||
"using_ephemeral": "Staring ephemeral Tor onion service and awaiting publication"
|
||||
"using_ephemeral": "Staring ephemeral Tor hidden service and awaiting publication",
|
||||
"zip_progress_bar_format": "Crunching files: %p%"
|
||||
}
|
||||
|
@ -41,5 +41,6 @@
|
||||
"gui_please_wait": "Odota...",
|
||||
"error_hs_dir_cannot_create": "Piilopalvelulle ei pystytty luomaan hakemistoa {0:s}",
|
||||
"error_hs_dir_not_writable": "Piilopalvelun hakemistoon {0:s} ei voi kirjoittaa",
|
||||
"using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua"
|
||||
"using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua",
|
||||
"zip_progress_bar_format": "Tiivistän tiedostoja: %p%"
|
||||
}
|
||||
|
@ -41,5 +41,6 @@
|
||||
"gui_please_wait": "Attendere prego...",
|
||||
"error_hs_dir_cannot_create": "Impossibile create la cartella per il servizio nascosto {0:s}",
|
||||
"error_hs_dir_not_writable": "La cartella per il servizio nascosto {0:s} non ha i permessi di scrittura",
|
||||
"using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione"
|
||||
}
|
||||
"using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione",
|
||||
"zip_progress_bar_format": "Elaborazione files: %p%"
|
||||
}
|
||||
|
@ -41,5 +41,6 @@
|
||||
"gui_please_wait": "Moment geduld...",
|
||||
"error_hs_dir_cannot_create": "Kan verborgen service map {0:s} niet aanmaken",
|
||||
"error_hs_dir_not_writable": "Verborgen service map {0:s} is niet schrijfbaar",
|
||||
"using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie"
|
||||
"using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie",
|
||||
"zip_progress_bar_format": "Bestanden verwerken: %p%"
|
||||
}
|
||||
|
@ -41,5 +41,6 @@
|
||||
"gui_please_wait": "Lütfen bekleyin...",
|
||||
"error_hs_dir_cannot_create": "Gizli hizmet klasörü {0:s} oluşturulamıyor",
|
||||
"error_hs_dir_not_writable": "Gizle hizmet klasörü {0:s} yazılabilir değil",
|
||||
"using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor"
|
||||
"using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor",
|
||||
"zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%"
|
||||
}
|
||||
|
2
setup.py
2
setup.py
@ -3,7 +3,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
OnionShare | https://onionshare.org/
|
||||
|
||||
Copyright (C) 2016 Micah Lee <micah@micahflee.com>
|
||||
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
Loading…
Reference in New Issue
Block a user