mirror of
https://github.com/onionshare/onionshare.git
synced 2025-01-11 15:29:38 -05:00
Merge branch 'gui' into ecmendenhall-write-tests
This commit is contained in:
commit
1485734462
@ -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()
|
@ -18,6 +18,10 @@ from stem.control import Controller
|
||||
from stem import SocketError
|
||||
|
||||
from flask import Flask, Markup, Response, request, make_response, send_from_directory, render_template_string
|
||||
|
||||
class NoTor(Exception):
|
||||
pass
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
strings = {}
|
||||
@ -25,12 +29,20 @@ strings = {}
|
||||
# generate an unguessable string
|
||||
slug = os.urandom(16).encode('hex')
|
||||
|
||||
# file information
|
||||
filename = filehash = filesize = ''
|
||||
# information about the file
|
||||
filename = filesize = filehash = None
|
||||
def set_file_info(new_filename, new_filehash, new_filesize):
|
||||
global filename, filehash, filesize
|
||||
filename = new_filename
|
||||
filehash = new_filehash
|
||||
filesize = new_filesize
|
||||
|
||||
@app.route("/{0}".format(slug))
|
||||
def index():
|
||||
global filename, filesize, filehash, slug, strings
|
||||
print 'filename: {0}'.format(filename)
|
||||
print 'filehash: {0}'.format(filehash)
|
||||
print 'filesize: {0}'.format(filesize)
|
||||
return render_template_string(open('{0}/index.html'.format(os.path.dirname(__file__))).read(),
|
||||
slug=slug, filename=os.path.basename(filename), filehash=filehash, filesize=filesize, strings=strings)
|
||||
|
||||
@ -79,22 +91,10 @@ def load_strings(default="en"):
|
||||
lang = lc[:2]
|
||||
if lang in translated:
|
||||
strings = translated[lang]
|
||||
return strings
|
||||
|
||||
def main():
|
||||
global filename, filehash, filesize
|
||||
load_strings()
|
||||
|
||||
# validate filename
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit('Usage: {0} [filename]'.format(sys.argv[0]));
|
||||
filename = sys.argv[1]
|
||||
if not os.path.isfile(filename):
|
||||
sys.exit(strings["not_a_file"].format(filename))
|
||||
else:
|
||||
filename = os.path.abspath(filename)
|
||||
|
||||
def file_crunching(filename):
|
||||
# calculate filehash, file size
|
||||
print strings["calculating_sha1"]
|
||||
BLOCKSIZE = 65536
|
||||
hasher = hashlib.sha1()
|
||||
with open(filename, 'rb') as f:
|
||||
@ -104,15 +104,18 @@ def main():
|
||||
buf = f.read(BLOCKSIZE)
|
||||
filehash = hasher.hexdigest()
|
||||
filesize = os.path.getsize(filename)
|
||||
return filehash, filesize
|
||||
|
||||
def choose_port():
|
||||
# let the OS choose a port
|
||||
tmpsock = socket.socket()
|
||||
tmpsock.bind(("127.0.0.1", 0))
|
||||
port = tmpsock.getsockname()[1]
|
||||
tmpsock.close()
|
||||
return port
|
||||
|
||||
def start_hidden_service(port):
|
||||
# connect to the tor controlport
|
||||
print strings["connecting_ctrlport"].format(port)
|
||||
controlports = [9051, 9151]
|
||||
controller = False
|
||||
for controlport in controlports:
|
||||
@ -121,7 +124,7 @@ def main():
|
||||
except SocketError:
|
||||
pass
|
||||
if not controller:
|
||||
sys.exit(strings["cant_connect_ctrlport"].format(controlports))
|
||||
raise NoTor(strings["cant_connect_ctrlport"].format(controlports))
|
||||
controller.authenticate()
|
||||
|
||||
# set up hidden service
|
||||
@ -130,11 +133,33 @@ def main():
|
||||
('HiddenServicePort', '80 127.0.0.1:{0}'.format(port))
|
||||
])
|
||||
onion_host = get_hidden_service_hostname(port)
|
||||
return onion_host
|
||||
|
||||
# punch a hole in the firewall
|
||||
def main():
|
||||
load_strings()
|
||||
|
||||
# try starting hidden service
|
||||
port = choose_port()
|
||||
print strings["connecting_ctrlport"].format(port)
|
||||
try:
|
||||
onion_host = start_hidden_service(port)
|
||||
except NoTor as e:
|
||||
sys.exit(e.args[0])
|
||||
|
||||
# select file to share
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit('Usage: {0} [filename]'.format(sys.argv[0]));
|
||||
filename = sys.argv[1]
|
||||
if not os.path.isfile(filename):
|
||||
sys.exit(strings["not_a_file"].format(filename))
|
||||
else:
|
||||
filename = os.path.abspath(filename)
|
||||
|
||||
# startup
|
||||
print strings["calculating_sha1"]
|
||||
filehash, filesize = file_crunching(filename)
|
||||
set_file_info(filename, filehash, filesize)
|
||||
tails_open_port(port)
|
||||
|
||||
# instructions
|
||||
print '\n' + strings["give_this_url"]
|
||||
print 'http://{0}/{1}'.format(onion_host, slug)
|
||||
print ''
|
||||
|
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(100, 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()
|
Loading…
Reference in New Issue
Block a user