diff --git a/git-hooks/README.md b/git-hooks/README.md new file mode 100644 index 00000000..0d8f80c9 --- /dev/null +++ b/git-hooks/README.md @@ -0,0 +1,3 @@ +To use these hooks, cp any of them to onionshare's `.git/hooks`. + +* `pre-push` runs the test suite, and will push if the tests pass. diff --git a/git-hooks/pre-push b/git-hooks/pre-push new file mode 100755 index 00000000..0ffd106c --- /dev/null +++ b/git-hooks/pre-push @@ -0,0 +1,6 @@ +#!/bin/bash + +# Pre-push hook. If you want to test with a different version of firefox, put +# the path in the CFX_FIREFOX environment variable. + +nosetests test diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index b8f7e299..7de1ad60 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -6,14 +6,20 @@ from functools import wraps from stem.control import Controller from stem import SocketError -from flask import Flask, Markup, Response, request, make_response, send_from_directory, render_template_string +from flask import Flask, Markup, Response, request, make_response, send_from_directory, render_template_string, abort + +# Flask depends on itsdangerous, which needs constant time string comparison +# for the HMAC values in secure cookies. Since we know itsdangerous is +# available, we just use its function. +from itsdangerous import constant_time_compare class NoTor(Exception): pass def random_string(num_bytes): b = os.urandom(num_bytes) - return base64.b32encode(b).lower().replace('=','') + h = hashlib.sha256(b).digest()[:16] + return base64.b32encode(h).lower().replace('=','') def get_platform(): p = platform.system() @@ -37,6 +43,19 @@ def set_stay_open(new_stay_open): app = Flask(__name__) +def debug_mode(): + import logging + global app + + if platform.system() == 'Windows': + temp_dir = os.environ['Temp'].replace('\\', '/') + else: + temp_dir = '/tmp/' + + log_handler = logging.FileHandler('{0}/onionshare_server.log'.format(temp_dir)) + log_handler.setLevel(logging.WARNING) + app.logger.addHandler(log_handler) + # get path of onioshare directory if get_platform() == 'Darwin': onionshare_dir = os.path.dirname(__file__) @@ -73,9 +92,13 @@ def human_readable_filesize(b): u += 1 return '{0} {1}'.format(round(b, 1), units[u]) -@app.route("/{0}".format(slug)) -def index(): +@app.route("/") +def index(slug_candidate): global filename, filesize, filehash, slug, strings, REQUEST_LOAD, onionshare_dir + + if not constant_time_compare(slug.encode('ascii'), slug_candidate.encode('ascii')): + abort(404) + add_request(REQUEST_LOAD, request.path) return render_template_string( open('{0}/index.html'.format(onionshare_dir)).read(), @@ -87,11 +110,14 @@ def index(): strings=strings ) -@app.route("/{0}/download".format(slug)) -def download(): +@app.route("//download") +def download(slug_candidate): global filename, filesize, q, download_count global REQUEST_DOWNLOAD, REQUEST_PROGRESS + if not constant_time_compare(slug.encode('ascii'), slug_candidate.encode('ascii')): + abort(404) + # each download has a unique id download_id = download_count download_count += 1 @@ -159,7 +185,7 @@ def tails_open_port(port): def tails_close_port(port): if get_platform() == 'Tails': print translated("closing_hole") - subprocess.call(['/sbin/iptables', '-I', 'OUTPUT', '-o', 'lo', '-p', 'tcp', '--dport', str(port), '-j', 'REJECT']) + subprocess.call(['/sbin/iptables', '-D', 'OUTPUT', '-o', 'lo', '-p', 'tcp', '--dport', str(port), '-j', 'ACCEPT']) def load_strings(default="en"): global strings @@ -260,14 +286,19 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--local-only', action='store_true', dest='local_only', help='Do not attempt to use tor: for development only') parser.add_argument('--stay-open', action='store_true', dest='stay_open', help='Keep hidden service running after download has finished') + parser.add_argument('--debug', action='store_true', dest='debug', help='Log errors to disk') parser.add_argument('filename', nargs=1, help='File to share') args = parser.parse_args() filename = os.path.abspath(args.filename[0]) - local_only = args.local_only + local_only = bool(args.local_only) + debug = bool(args.debug) + + if debug: + debug_mode() global stay_open - stay_open = args.stay_open + stay_open = bool(args.stay_open) if not (filename and os.path.isfile(filename)): sys.exit(translated("not_a_file").format(filename)) diff --git a/onionshare/strings.json b/onionshare/strings.json index d9063bc2..9589fac2 100644 --- a/onionshare/strings.json +++ b/onionshare/strings.json @@ -100,7 +100,17 @@ "ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen", "not_a_file": "{0} is geen bestand.", "filesize": "Bestandsgrootte", - "sha1_checksum": "SHA1 controlecijfer" + "sha1_checksum": "SHA1 controlecijfer", + "copied_url": "URL gekopieerd naar klembord", + "download_page_loaded": "Download pagina geladen", + "download_started": "Download gestart", + "download_finished": "Download voltooid", + "other_page_loaded": "Andere pagina is geladen", + "tails_requires_root": "Je moet OnionShare als root draaien in Tails", + "close_on_finish": "Sluit automatisch", + "close_countdown": "Sluit in {0} seconden...", + "choose_file": "Kies betsand om te delen", + "copy_url": "Kopieer URL" }, "pt": { "punching_a_hole": "Abrindo um buraco no firewall.", "closing_hole": "Fechando buraco no firewall.", diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index fbb6478e..f6c78702 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -96,7 +96,7 @@ def main(): args = parser.parse_args() filename = args.filename - local_only = args.local_only + local_only = bool(args.local_only) stay_open = bool(args.stay_open) debug = bool(args.debug) @@ -134,6 +134,7 @@ def main(): else: webapp.onion_host = local_host if debug: + onionshare.debug_mode() webapp.debug_mode() # run the web app in a new thread diff --git a/onionshare_gui/static/helpers.js b/onionshare_gui/static/helpers.js index 951320ba..8936c3e5 100644 --- a/onionshare_gui/static/helpers.js +++ b/onionshare_gui/static/helpers.js @@ -9,3 +9,52 @@ function human_readable_filesize(bytes, si) { } while(bytes >= thresh); return bytes.toFixed(1)+' '+units[u]; }; + +function htmlspecialchars(string, quote_style, charset, double_encode) { + var optTemp = 0, + i = 0, + noquotes = false; + if (typeof quote_style === 'undefined' || quote_style === null) { + quote_style = 2; + } + string = string.toString(); + if (double_encode !== false) { + // Put this first to avoid double-encoding + string = string.replace(/&/g, '&'); + } + string = string.replace(//g, '>'); + + var OPTS = { + 'ENT_NOQUOTES': 0, + 'ENT_HTML_QUOTE_SINGLE': 1, + 'ENT_HTML_QUOTE_DOUBLE': 2, + 'ENT_COMPAT': 2, + 'ENT_QUOTES': 3, + 'ENT_IGNORE': 4 + }; + if (quote_style === 0) { + noquotes = true; + } + if (typeof quote_style !== 'number') { + // Allow for a single string or an array of string flags + quote_style = [].concat(quote_style); + for (i = 0; i < quote_style.length; i++) { + // Resolve string input to bitwise e.g. 'ENT_IGNORE' becomes 4 + if (OPTS[quote_style[i]] === 0) { + noquotes = true; + } else if (OPTS[quote_style[i]]) { + optTemp = optTemp | OPTS[quote_style[i]]; + } + } + quote_style = optTemp; + } + if (quote_style & OPTS.ENT_HTML_QUOTE_SINGLE) { + string = string.replace(/'/g, '''); + } + if (!noquotes) { + string = string.replace(/"/g, '"'); + } + + return string; +} diff --git a/onionshare_gui/static/onionshare.js b/onionshare_gui/static/onionshare.js index 0e13f445..05e796bf 100644 --- a/onionshare_gui/static/onionshare.js +++ b/onionshare_gui/static/onionshare.js @@ -65,7 +65,7 @@ $(function(){ } } else { if(r.path != '/favicon.ico') - update($('').addClass('weblog-error').html(onionshare.strings['other_page_loaded']+': '+r.path)); + update($('').addClass('weblog-error').html(onionshare.strings['other_page_loaded']+': '+htmlspecialchars(r.path))); } } } diff --git a/onionshare_gui/webapp.py b/onionshare_gui/webapp.py index 60583402..5d9247d3 100644 --- a/onionshare_gui/webapp.py +++ b/onionshare_gui/webapp.py @@ -1,4 +1,5 @@ -from flask import Flask, render_template +from flask import Flask, render_template, make_response +from functools import wraps import threading, json, os, time, platform, sys onionshare = None @@ -22,15 +23,40 @@ def debug_mode(): else: temp_dir = '/tmp/' - log_handler = logging.FileHandler('{0}/onionshare.web.log'.format(temp_dir)) + log_handler = logging.FileHandler('{0}/onionshare_gui.log'.format(temp_dir)) log_handler.setLevel(logging.WARNING) app.logger.addHandler(log_handler) +def add_response_headers(headers={}): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + resp = make_response(f(*args, **kwargs)) + h = resp.headers + for header, value in headers.items(): + h[header] = value + return resp + return decorated_function + return decorator + +def csp(f): + @wraps(f) + # disable inline js, external js + @add_response_headers({'Content-Security-Policy': "default-src 'self'; connect-src 'self'"}) + # ugh, webkit embedded in Qt4 is stupid old + # TODO: remove webkit, build GUI with normal Qt widgets + @add_response_headers({'X-WebKit-CSP': "default-src 'self'; connect-src 'self'"}) + def decorated_function(*args, **kwargs): + return f(*args, **kwargs) + return decorated_function + @app.route("/") +@csp def index(): return render_template('index.html') @app.route("/init_info") +@csp def init_info(): global onionshare, filename, stay_open basename = os.path.basename(filename) @@ -42,6 +68,7 @@ def init_info(): }) @app.route("/start_onionshare") +@csp def start_onionshare(): global onionshare, onionshare_port, filename, onion_host, url @@ -62,6 +89,7 @@ def start_onionshare(): }) @app.route("/copy_url") +@csp def copy_url(): if platform.system() == 'Windows': # Qt's QClipboard isn't working in Windows @@ -82,16 +110,19 @@ def copy_url(): return '' @app.route("/stay_open_true") +@csp def stay_open_true(): global onionshare onionshare.set_stay_open(True) @app.route("/stay_open_false") +@csp def stay_open_false(): global onionshare onionshare.set_stay_open(False) @app.route("/heartbeat") +@csp def check_for_requests(): global onionshare events = [] @@ -107,6 +138,7 @@ def check_for_requests(): return json.dumps(events) @app.route("/close") +@csp def close(): global qtapp time.sleep(1) diff --git a/setup/onionshare-launcher.py b/setup/onionshare-launcher.py index e8b4c0f5..07cd2436 100644 --- a/setup/onionshare-launcher.py +++ b/setup/onionshare-launcher.py @@ -1,6 +1,6 @@ # import stuff for pyinstaller to find -import os, sys, subprocess, time, hashlib, platform, json, locale, socket, argparse, Queue, inspect, base64, random, functools -import PyQt4.QtCore, PyQt4.QtGui, PyQt4.QtWebKit +import os, sys, subprocess, time, hashlib, platform, json, locale, socket, argparse, Queue, inspect, base64, random, functools, logging +from PyQt4 import QtCore, QtGui, QtWebKit import stem, stem.control, flask import onionshare, onionshare_gui diff --git a/setup/onionshare.nsi b/setup/onionshare.nsi index ba27b911..8fd368f3 100644 --- a/setup/onionshare.nsi +++ b/setup/onionshare.nsi @@ -3,10 +3,10 @@ !define ABOUTURL "https://github.com/micahflee/onionshare" # change these with each release -!define INSTALLSIZE 46094 +!define INSTALLSIZE 46124 !define VERSIONMAJOR 0 -!define VERSIONMINOR 3 -!define VERSIONSTRING "0.3dev" +!define VERSIONMINOR 4 +!define VERSIONSTRING "0.4" RequestExecutionLevel admin diff --git a/version b/version index be586341..bd73f470 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.3 +0.4