From 170811f450b801c03111b1a6ae59ed9808d1bead Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 12 Feb 2016 14:34:19 -0800 Subject: [PATCH] Porting onionshare from python2 to python3 (#261). This commit only ports the CLI version, not the GUI. Has not been tested in Fedora, Windows, or OSX. Removed hack to make unicode filenames work because hack does not work in python3. Replaced constant_time_compare function with a new one that works in python3. Tweaked hidden service checking code because urllib is different in python3. --- BUILD.md | 4 +-- install/build_deb.sh | 2 +- install/linux_scripts/onionshare | 2 +- install/linux_scripts/onionshare-gui | 2 +- onionshare/__init__.py | 2 +- onionshare/helpers.py | 38 +++++++++++----------------- onionshare/hs.py | 35 +++++++++++++------------ onionshare/onionshare.py | 21 +++++++-------- onionshare/strings.py | 8 +++--- onionshare/web.py | 16 +++++++----- setup.py | 8 +++--- stdeb.cfg | 6 ++--- 12 files changed, 71 insertions(+), 73 deletions(-) diff --git a/BUILD.md b/BUILD.md index 1c02c870..4604a6e2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -11,10 +11,10 @@ cd onionshare *For .deb-based distros (like Debian, Ubuntu, Linux Mint):* -Note that python-stem appears in Debian wheezy and newer, and it appears in Ubuntu 13.10 and newer. Older versions of Debian and Ubuntu aren't supported. +Note that python3-stem appears in Debian wheezy and newer, and it appears in Ubuntu 13.10 and newer. Older versions of Debian and Ubuntu aren't supported. ```sh -sudo apt-get install -y build-essential fakeroot python-all python-stdeb python-flask python-stem python-qt4 dh-python +sudo apt-get install -y build-essential fakeroot python3-all python3-stdeb python3-flask python3-stem python3-pyqt5 dh-python ./install/build_deb.sh sudo dpkg -i deb_dist/onionshare_*.deb ``` diff --git a/install/build_deb.sh b/install/build_deb.sh index dc2fbaa2..cd4ec427 100755 --- a/install/build_deb.sh +++ b/install/build_deb.sh @@ -9,7 +9,7 @@ VERSION=`cat version` rm -r deb_dist >/dev/null 2>&1 # build binary package -python setup.py --command-packages=stdeb.command bdist_deb +python3 setup.py --command-packages=stdeb.command bdist_deb # return install instructions if onionshare builds properly echo "" diff --git a/install/linux_scripts/onionshare b/install/linux_scripts/onionshare index c563b1c8..a8a7445a 100755 --- a/install/linux_scripts/onionshare +++ b/install/linux_scripts/onionshare @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ OnionShare | https://onionshare.org/ diff --git a/install/linux_scripts/onionshare-gui b/install/linux_scripts/onionshare-gui index b8d78191..8a72bf47 100755 --- a/install/linux_scripts/onionshare-gui +++ b/install/linux_scripts/onionshare-gui @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ OnionShare | https://onionshare.org/ diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 6535d589..f9c1daad 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -17,4 +17,4 @@ 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 . """ -from onionshare import * +from .onionshare import * diff --git a/onionshare/helpers.py b/onionshare/helpers.py index 43dd7819..58c9a8da 100644 --- a/onionshare/helpers.py +++ b/onionshare/helpers.py @@ -17,15 +17,7 @@ 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 . """ -import os, inspect, hashlib, base64, hmac, platform, zipfile, tempfile -from itertools import izip -import math -import time - -# hack to make unicode filenames work (#141) -import sys -reload(sys) -sys.setdefaultencoding("utf-8") +import sys, os, inspect, hashlib, base64, hmac, platform, zipfile, tempfile, math, time def get_platform(): @@ -82,20 +74,20 @@ def get_version(): def constant_time_compare(val1, val2): """ - Compares two values in constant time. - """ - _builtin_constant_time_compare = getattr(hmac, 'compare_digest', None) - if _builtin_constant_time_compare is not None: - return _builtin_constant_time_compare(val1, val2) + Returns True if the two strings are equal, False otherwise. - len_eq = len(val1) == len(val2) - if len_eq: - result = 0 - left = val1 - else: - result = 1 - left = val2 - for x, y in izip(bytearray(left), bytearray(val2)): + The time taken is independent of the number of characters that match. + + For the sake of simplicity, this function executes in constant time only + when the two strings have the same length. It short-circuits when they + have different lengths. + + From: http://www.levigross.com/2014/02/07/constant-time-comparison-functions-in...-python-haskell-clojure-and-java/ + """ + if len(val1) != len(val2): + return False + result = 0 + for x, y in zip(val1, val2): result |= x ^ y return result == 0 @@ -106,7 +98,7 @@ def random_string(num_bytes, output_len=None): """ b = os.urandom(num_bytes) h = hashlib.sha256(b).digest()[:16] - s = base64.b32encode(h).lower().replace('=', '') + s = base64.b32encode(h).lower().replace(b'=', b'').decode('utf-8') if not output_len: return s return s[:output_len] diff --git a/onionshare/hs.py b/onionshare/hs.py index c770ddfd..dc9355f1 100644 --- a/onionshare/hs.py +++ b/onionshare/hs.py @@ -19,10 +19,11 @@ along with this program. If not, see . """ from stem.control import Controller -import os, sys, tempfile, shutil, urllib2, httplib -import socks +from stem import SocketError +import os, sys, tempfile, shutil, urllib -import helpers, strings +from . import socks +from . import helpers, strings class NoTor(Exception): """ @@ -62,17 +63,19 @@ class HS(object): self.service_id = None # connect to the tor controlport + found_tor = False self.c = None ports = [9151, 9153, 9051] for port in ports: try: self.c = Controller.from_port(port=port) self.c.authenticate() + found_tor = True break - except: + except SocketError: pass - if not self.c: - raise NoTor(strings._("cant_connect_ctrlport").format(ports)) + if not found_tor: + raise NoTor(strings._("cant_connect_ctrlport").format(str(ports))) # do the versions of stem and tor that I'm using support ephemeral hidden services? tor_version = self.c.get_version().version_str @@ -84,9 +87,9 @@ class HS(object): Start a hidden service on port 80, pointing to the given port, and return the onion hostname. """ - print strings._("connecting_ctrlport").format(int(port)) + print(strings._("connecting_ctrlport").format(int(port))) if self.supports_ephemeral: - print strings._('using_ephemeral') + print(strings._('using_ephemeral')) res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication = False) self.service_id = res.content()[0][2].split('=')[1] onion_host = res.content()[0][2].split('=')[1] + '.onion' @@ -102,7 +105,7 @@ class HS(object): path = '/tmp/onionshare' try: if not os.path.exists(path): - os.makedirs(path, 0700) + os.makedirs(path, 0o700) except: raise HSDirError(strings._("error_hs_dir_cannot_create").format(path)) if not os.access(path, os.W_OK): @@ -139,7 +142,7 @@ class HS(object): successfully connects.. """ # legacy only, this function is no longer required with ephemeral hidden services - print strings._('wait_for_hs') + print(strings._('wait_for_hs')) ready = False while not ready: @@ -149,7 +152,7 @@ class HS(object): if self.transparent_torification: # no need to set the socks5 proxy - urllib2.urlopen('http://{0:s}'.format(onion_host)) + urllib.request.urlopen('http://{0:s}'.format(onion_host)) else: tor_exists = False ports = [9150, 9152, 9050] @@ -164,17 +167,17 @@ class HS(object): except socks.ProxyConnectionError: pass if not tor_exists: - raise NoTor(strings._("cant_connect_socksport").format(tor_socks_ports)) + raise NoTor(strings._("cant_connect_socksport").format(str(ports))) ready = True sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_yup'))) + except socks.GeneralProxyError: + sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope'))) + sys.stdout.flush() except socks.SOCKS5Error: sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope'))) sys.stdout.flush() - except urllib2.HTTPError: # torification error - sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope'))) - sys.stdout.flush() - except httplib.BadStatusLine: # torification (with bridge) error + except urllib.error.HTTPError: # torification error sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope'))) sys.stdout.flush() except KeyboardInterrupt: diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index e10fe65d..bfd36864 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -19,7 +19,8 @@ along with this program. If not, see . """ import os, sys, subprocess, time, argparse, inspect, shutil, socket, threading -import strings, helpers, web, hs + +from . import strings, helpers, web, hs class OnionShare(object): """ @@ -97,7 +98,7 @@ def main(cwd=None): onionshare uses. """ strings.load_strings() - print strings._('version_string').format(helpers.get_version()) + print(strings._('version_string').format(helpers.get_version())) # onionshare CLI in OSX needs to change current working directory (#132) if helpers.get_platform() == 'Darwin': @@ -142,15 +143,15 @@ def main(cwd=None): sys.exit(e.args[0]) # prepare files to share - print strings._("preparing_files") + 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 '' + print('') + print(strings._("large_filesize")) + print('') # start onionshare service in new thread t = threading.Thread(target=web.start, args=(app.port, app.stay_open, app.transparent_torification)) @@ -164,10 +165,10 @@ def main(cwd=None): if not ready: sys.exit() - print strings._("give_this_url") - print 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) - print '' - print strings._("ctrlc_to_stop") + 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(): diff --git a/onionshare/strings.py b/onionshare/strings.py index 29eab589..c211668b 100644 --- a/onionshare/strings.py +++ b/onionshare/strings.py @@ -18,7 +18,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ import json, locale, sys, os -import helpers + +from . import helpers strings = {} @@ -62,9 +63,6 @@ def translated(k, gui=False): """ Returns a translated string. """ - if gui: - return strings[k].encode("utf-8").decode('utf-8', 'replace') - else: - return strings[k].encode("utf-8") + return strings[k] _ = translated diff --git a/onionshare/web.py b/onionshare/web.py index 800e2be2..ed9bb786 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -17,10 +17,12 @@ 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 . """ -import Queue, mimetypes, platform, os, sys, urllib2 +import queue, mimetypes, platform, os, sys +from urllib.request import urlopen from flask import Flask, Response, request, render_template_string, abort from functools import wraps -import strings, helpers + +from . import strings, helpers app = Flask(__name__) @@ -72,7 +74,7 @@ REQUEST_DOWNLOAD = 1 REQUEST_PROGRESS = 2 REQUEST_OTHER = 3 REQUEST_CANCELED = 4 -q = Queue.Queue() +q = queue.Queue() def add_request(request_type, path, data=None): @@ -154,7 +156,7 @@ def index(slug_candidate): open(helpers.get_html_path('index.html')).read(), slug=slug, file_info=file_info, - filename=os.path.basename(zip_filename).decode("utf-8"), + filename=os.path.basename(zip_filename), filesize=zip_filesize, filesize_human=helpers.human_readable_filesize(zip_filesize) ) @@ -192,7 +194,7 @@ def download(slug_candidate): canceled = False while not done: chunk = fp.read(chunk_size) - if chunk == '': + if chunk == b'': done = True else: try: @@ -224,7 +226,7 @@ def download(slug_candidate): # download is finished, close the server if not stay_open and not canceled: - print strings._("closing_automatically") + print(strings._("closing_automatically")) if shutdown_func is None: raise RuntimeError('Not running with the Werkzeug Server') shutdown_func() @@ -292,6 +294,6 @@ def stop(port): s.connect(('127.0.0.1', port)) s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(shutdown_slug)) else: - urllib2.urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, shutdown_slug)).read() + urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, shutdown_slug)).read() except: pass diff --git a/setup.py b/setup.py index 21ef0906..ecc1f1aa 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ OnionShare | https://onionshare.org/ @@ -83,9 +83,11 @@ if system == 'Linux': url='https://github.com/micahflee/onionshare', license="GPL v3", keywords='onion, share, onionshare, tor, anonymous, web server', - packages=['onionshare', 'onionshare_gui'], + #packages=['onionshare', 'onionshare_gui'], + packages=['onionshare'], include_package_data=True, - scripts=['install/linux_scripts/onionshare', 'install/linux_scripts/onionshare-gui'], + #scripts=['install/linux_scripts/onionshare', 'install/linux_scripts/onionshare-gui'], + scripts=['install/linux_scripts/onionshare'], data_files=[ (os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']), (os.path.join(sys.prefix, 'share/appdata'), ['install/onionshare.appdata.xml']), diff --git a/stdeb.cfg b/stdeb.cfg index 7028f47f..5645322b 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,5 +1,5 @@ [DEFAULT] -Package: onionshare -Depends: python-flask, python-stem, python-qt4 -Build-Depends: dh-python +Package3: onionshare +Depends3: python3-flask, python3-stem, python3-pyqt5 +Build-Depends3: python3-stdeb, python3-nose Suite: trusty