Merge branch 'master' of https://github.com/micahflee/onionshare into plugin-changes

Conflicts:
	onionshare/onionshare.py
This commit is contained in:
Joshua Thayer 2014-06-05 17:17:13 -07:00
commit 467977195c
13 changed files with 487 additions and 3 deletions

View File

@ -2,3 +2,4 @@ include LICENSE
include README.md
include onionshare/*.html
include onionshare/strings.json
include onionshare_gui/html/*

10
bin/onionshare-gui Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
import sys, os
try:
import onionshare_gui
except ImportError:
sys.path.append(os.path.abspath(os.path.dirname(__file__)+'/..'))
import onionshare_gui
onionshare_gui.main()

View File

@ -53,4 +53,15 @@
"not_a_file": "{0} non è un file.",
"filesize": "Grandezza del file",
"sha1_checksum": "firma SHA1"
}, "nl": {
"punching_a_hole": "Een doorgang aan het maken in de firewall.",
"closing_hole": "Doorgang in de firewall sluiten.",
"calculating_sha1": "SHA1 controlecijfer berekenen.",
"connecting_ctrlport": "Verbinden met de Tor controle port om een verborgen service op te zetten op poort {0}.",
"cant_connect_ctrlport": "Kan niet verbinden met de Tor controle poort op poorten {0}. Draait Tor?",
"give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:",
"ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen",
"not_a_file": "{0} is geen bestand.",
"filesize": "Bestandsgrootte",
"sha1_checksum": "SHA1 controlecijfer"
}}

View File

@ -0,0 +1 @@
from onionshare_gui import *

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="style.css" />
</head>
<body>
<h1><span id="basename"></span></h1>
<div id="output"></div>
<button class="button" id="copy-button">Copy URL</button>
<script src="jquery-1.11.1.min.js"></script>
<script src="onionshare.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,24 @@
function send(msg) {
document.title = "null";
document.title = msg;
}
function init(basename, strings) {
$('#basename').html(basename).show();
}
function url_is_set() {
$('#copy-button')
.click(function(){
send('copy_url');
})
.show();
}
function update(msg) {
var $line = $('<p></p>').append(msg);
$('#output').append($line);
// scroll to bottom
$('#output').scrollTop($('#output').height());
}

View File

@ -0,0 +1,86 @@
body {
background-color: #ffffff;
color: #000000;
font-family: arial;
width: 550px;
height: 300px;
margin: 0 auto;
padding: 0;
}
h1 {
background-color: #222222;
color: #ffffff;
text-align: center;
font-size: 20px;
margin: 0;
padding: 10px;
height: 20px;
}
p {
padding: 0;
margin: 0 0 .3em 0;
}
#output {
padding: 10px;
word-wrap: break-word;
height: 260px;
overflow: auto;
font-family: monospace;
}
.button {
-moz-box-shadow:inset 0px 1px 0px 0px #f29c93;
-webkit-box-shadow:inset 0px 1px 0px 0px #f29c93;
box-shadow:inset 0px 1px 0px 0px #f29c93;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #fe1a00), color-stop(1, #ce0100) );
background:-moz-linear-gradient( center top, #fe1a00 5%, #ce0100 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fe1a00', endColorstr='#ce0100');
background-color:#fe1a00;
-webkit-border-top-left-radius:0px;
-moz-border-radius-topleft:0px;
border-top-left-radius:0px;
-webkit-border-top-right-radius:0px;
-moz-border-radius-topright:0px;
border-top-right-radius:0px;
-webkit-border-bottom-right-radius:0px;
-moz-border-radius-bottomright:0px;
border-bottom-right-radius:0px;
-webkit-border-bottom-left-radius:0px;
-moz-border-radius-bottomleft:0px;
border-bottom-left-radius:0px;
text-indent:0;
border:1px solid #d83526;
display:inline-block;
color:#ffffff;
font-family:Arial;
font-size:13px;
font-weight:bold;
font-style:normal;
text-decoration:none;
text-align:center;
text-shadow:1px 1px 0px #b23e35;
}
.button:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ce0100), color-stop(1, #fe1a00) );
background:-moz-linear-gradient( center top, #ce0100 5%, #fe1a00 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ce0100', endColorstr='#fe1a00');
background-color:#ce0100;
}
#copy-button {
display: none;
position: absolute;
right: 20px;
bottom: 10px;
}
.loader {
width: 21px;
height: 20px;
background-image: url('loader.gif');
background-position: top left;
background-repeat: no-repeat;
}

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python
import onionshare, webgui
import os, sys, time, json, gtk, gobject, thread
url = None
class Global(object):
quit = False
@classmethod
def set_quit(cls, *args, **kwargs):
cls.quit = True
def alert(msg, type=gtk.MESSAGE_INFO):
dialog = gtk.MessageDialog(
parent=None,
flags=gtk.DIALOG_MODAL,
type=type,
buttons=gtk.BUTTONS_OK,
message_format=msg)
response = dialog.run()
dialog.destroy()
def select_file(strings):
# get filename, either from argument or file chooser dialog
if len(sys.argv) == 2:
filename = sys.argv[1]
else:
canceled = False
chooser = gtk.FileChooserDialog(
title="Choose a file to share",
action=gtk.FILE_CHOOSER_ACTION_OPEN,
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
response = chooser.run()
if response == gtk.RESPONSE_OK:
filename = chooser.get_filename()
elif response == gtk.RESPONSE_CANCEL:
canceled = True
chooser.destroy()
if canceled:
return False, False
# validate filename
if not os.path.isfile(filename):
alert(strings["not_a_file"].format(filename), gtk.MESSAGE_ERROR)
return False, False
filename = os.path.abspath(filename)
basename = os.path.basename(filename)
return filename, basename
def main():
global url
strings = onionshare.load_strings()
# try starting hidden service
port = onionshare.choose_port()
try:
onion_host = onionshare.start_hidden_service(port)
except onionshare.NoTor as e:
alert(e.args[0], gtk.MESSAGE_ERROR)
return
onionshare.tails_open_port(port)
# select file to share
filename, basename = select_file(strings)
if not filename:
return
# open the window, launching webkit browser
webgui.start_gtk_thread()
browser, web_recv, web_send = webgui.sync_gtk_msg(webgui.launch_window)(
title="OnionShare | {0}".format(basename),
quit_function=Global.set_quit,
echo=False)
# clipboard
clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
def set_clipboard():
global url
clipboard.set_text(url)
web_send("update('{0}')".format('Copied secret URL to clipboard.'))
# the async nature of things requires startup to be split into multiple functions
def startup_async():
global url
filehash, filesize = onionshare.file_crunching(filename)
onionshare.set_file_info(filename, filehash, filesize)
url = 'http://{0}/{1}'.format(onion_host, onionshare.slug)
web_send("update('{0}')".format(strings['give_this_url'].replace('\'', '\\\'')))
web_send("update('<strong>{0}</strong>')".format(url))
web_send("url_is_set()")
# clipboard needs a bit of time before copying url
gobject.timeout_add(500, set_clipboard)
def startup_sync():
web_send("init('{0}', {1});".format(basename, json.dumps(strings)))
web_send("update('{0}')".format(strings['calculating_sha1']))
# run other startup in the background
thread_crunch = thread.start_new_thread(startup_async, ())
# start the web server
thread_web = thread.start_new_thread(onionshare.app.run, (), {"port": port})
gobject.timeout_add(500, startup_sync)
# main loop
last_second = time.time()
uptime_seconds = 1
clicks = 0
while not Global.quit:
current_time = time.time()
again = False
msg = web_recv()
if msg:
again = True
# check msg for messages from the browser
if msg == 'copy_url':
set_clipboard()
if not again:
time.sleep(0.1)
# shutdown
onionshare.tails_close_port(port)
if __name__ == '__main__':
main()

81
onionshare_gui/webgui.py Normal file
View File

@ -0,0 +1,81 @@
#!/usr/bin/env python
import time, Queue, thread, gtk, gobject, os, webkit
def async_gtk_msg(fun):
def worker((function, args, kwargs)):
apply(function, args, kwargs)
def fun2(*args, **kwargs):
gobject.idle_add(worker, (fun, args, kwargs))
return fun2
def sync_gtk_msg(fun):
class NoResult: pass
def worker((R, function, args, kwargs)):
R.result = apply(function, args, kwargs)
def fun2(*args, **kwargs):
class R: result = NoResult
gobject.idle_add(worker, (R, fun, args, kwargs))
while R.result is NoResult: time.sleep(0.01)
return R.result
return fun2
def launch_window(title='OnionShare', quit_function=None, echo=True):
window = gtk.Window()
window.set_title(title)
browser = webkit.WebView()
box = gtk.VBox(homogeneous=False, spacing=0)
window.add(box)
if quit_function is not None:
window.connect('destroy', quit_function)
box.pack_start(browser, expand=True, fill=True, padding=0)
window.set_default_size(600, 600)
window.set_resizable(False)
window.show_all()
message_queue = Queue.Queue()
def callback_wrapper(widget, frame, title):
if title != 'null':
message_queue.put(title)
browser.connect('title-changed', callback_wrapper)
browser.open('file://'+os.path.abspath(os.path.dirname(__file__))+'/html/index.html')
def web_recv():
if message_queue.empty():
return None
else:
msg = message_queue.get()
if echo: print '>>>', msg
return msg
def web_send(msg):
if echo: print '<<<', msg
async_gtk_msg(browser.execute_script)(msg)
return browser, web_recv, web_send
def start_gtk_thread():
# Start GTK in its own thread:
gtk.gdk.threads_init()
thread.start_new_thread(gtk.main, ())
def kill_gtk_thread():
async_gtk_msg(gtk.main_quit)()
def main():
if not select_file():
return
launch_browser()

View File

@ -14,7 +14,7 @@ if sys.argv[-1] == 'publish':
setup(
name='onionshare',
version='0.1',
version='0.2',
description='OnionShare lets you securely and anonymously share a file of any size with someone. It works by starting a web server, making it accessible as a Tor hidden service, and generating an unguessable URL to access and download the file.',
long_description="""OnionShare lets you securely and anonymously share a file of any size with someone. It works by starting a web server, making it accessible as a Tor hidden service, and generating an unguessable URL to access and download the file. It doesn't require setting up a server on the internet somewhere or using a third party filesharing service. You host the file on your own computer and use a Tor hidden service to make it temporarily accessible over the internet. The other user just needs to use Tor Browser to download the file from you.""",
author='Micah Lee',
@ -27,6 +27,6 @@ setup(
],
license="GPL v3",
keywords='onion, share, onionshare, tor, anonymous, web server',
packages=['onionshare'],
scripts=['bin/onionshare']
packages=['onionshare', 'onionshare_gui'],
scripts=['bin/onionshare', 'bin/onionshare-gui']
)

119
test/onionshare_test.py Normal file
View File

@ -0,0 +1,119 @@
from onionshare import *
def test_get_platform_returns_env_var():
"get_platform() returns ONIONSHARE_PLATFORM from the environment"
os.environ['ONIONSHARE_PLATFORM'] = 'TI-83+'
assert get_platform() == 'TI-83+'
def test_get_platform_returns_platform_system():
"""
get_platform() returns platform.system() when
ONIONSHARE_PLATFORM is not defined
"""
os.environ.pop('ONIONSHARE_PLATFORM', None)
onionshare.platform.system = lambda: 'Sega Saturn'
assert get_platform() == 'Sega Saturn'
def test_tails_appends_to_path():
"adds '/../tails/lib' to the path when running on Tails"
original_path = sys.path
onionshare.platform.system = lambda: 'Tails'
append_lib_on_tails()
assert sys.path[-1][-13:] == '/../tails/lib'
def test_get_hidden_service_dir_windows_with_temp():
"""
get_hidden_service_dir() uses a temporary directory from the
Windows environment when defined
"""
onionshare.platform.system = lambda: 'Windows'
os.environ['Temp'] = "C:\Internet Explorer\Secrets"
expected_path = "C:/Internet Explorer/Secrets/onionshare_hidden_service_port"
assert get_hidden_service_dir('port') == expected_path
def test_get_hidden_service_dir_windows_default():
"get_hidden_service_dir() uses C:/tmp by default on Windows"
onionshare.get_platform = lambda: 'Windows'
os.environ.pop('Temp', None)
expected_path = "C:/tmp/onionshare_hidden_service_port"
assert get_hidden_service_dir('port') == expected_path
def test_get_hidden_service_dir_posix():
"get_hidden_service_dir() uses /tmp by default on POSIX"
onionshare.get_platform = lambda: 'Not Windows'
expected_path = "/tmp/onionshare_hidden_service_port"
assert get_hidden_service_dir('port') == expected_path
class MockSubprocess():
def __init__(self):
self.last_call = None
def call(self, args):
self.last_call = args
def last_call_args(self):
return self.last_call
def test_tails_open_port():
"tails_open_port() calls iptables with ACCEPT arg"
onionshare.get_platform = lambda: 'Tails'
onionshare.strings = {'punching_a_hole': ''}
mock_subprocess = MockSubprocess()
onionshare.subprocess = mock_subprocess
onionshare.tails_open_port('port')
expected_call = [
'/sbin/iptables', '-I', 'OUTPUT',
'-o', 'lo', '-p',
'tcp', '--dport', 'port', '-j', 'ACCEPT'
]
actual_call = mock_subprocess.last_call_args()
assert actual_call == expected_call
def test_load_strings_defaults_to_english():
"load_strings() loads English by default"
locale.getdefaultlocale = lambda: ('en_US', 'UTF-8')
load_strings()
assert onionshare.strings['calculating_sha1'] == "Calculating SHA1 checksum."
def test_load_strings_loads_other_languages():
"load_strings() loads other languages in different locales"
locale.getdefaultlocale = lambda: ('fr_FR', 'UTF-8')
load_strings("fr")
print onionshare.strings
assert onionshare.strings['calculating_sha1'] == "Calculer un hachage SHA-1."
def test_tails_close_port():
"tails_close_port() calls iptables with REJECT arg"
onionshare.get_platform = lambda: 'Tails'
onionshare.strings = {'closing_hole': ''}
mock_subprocess = MockSubprocess()
onionshare.subprocess = mock_subprocess
onionshare.tails_close_port('port')
expected_call = [
'/sbin/iptables', '-I', 'OUTPUT',
'-o', 'lo', '-p',
'tcp', '--dport', 'port', '-j', 'REJECT'
]
actual_call = mock_subprocess.last_call_args()
assert actual_call == expected_call
def test_generate_slug_length():
"generates a 32-character slug"
assert len(slug) == 32
def test_generate_slug_characters():
"generates a hex slug"
def is_hex(string):
hex_alphabet = "01234556789abcdef"
return all(char in hex_alphabet for char in string)
assert is_hex(slug)
def test_starts_with_empty_strings():
"creates an empty strings dict by default"
assert strings == {}