mirror of
https://github.com/onionshare/onionshare.git
synced 2024-10-01 01:35:40 -04:00
Merge branch 'master' of https://github.com/micahflee/onionshare into plugin-changes
Conflicts: onionshare/onionshare.py
This commit is contained in:
commit
467977195c
@ -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
10
bin/onionshare-gui
Executable 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()
|
@ -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"
|
||||
}}
|
||||
|
1
onionshare_gui/__init__.py
Normal file
1
onionshare_gui/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from onionshare_gui import *
|
14
onionshare_gui/html/index.html
Normal file
14
onionshare_gui/html/index.html
Normal 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>
|
4
onionshare_gui/html/jquery-1.11.1.min.js
vendored
Normal file
4
onionshare_gui/html/jquery-1.11.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
onionshare_gui/html/loader.gif
Normal file
BIN
onionshare_gui/html/loader.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
24
onionshare_gui/html/onionshare.js
Normal file
24
onionshare_gui/html/onionshare.js
Normal 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());
|
||||
}
|
86
onionshare_gui/html/style.css
Normal file
86
onionshare_gui/html/style.css
Normal 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;
|
||||
}
|
133
onionshare_gui/onionshare_gui.py
Normal file
133
onionshare_gui/onionshare_gui.py
Normal 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
81
onionshare_gui/webgui.py
Normal 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()
|
6
setup.py
6
setup.py
@ -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
119
test/onionshare_test.py
Normal 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 == {}
|
Loading…
Reference in New Issue
Block a user