mirror of
https://github.com/onionshare/onionshare.git
synced 2025-01-11 23:39:31 -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 README.md
|
||||||
include onionshare/*.html
|
include onionshare/*.html
|
||||||
include onionshare/strings.json
|
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 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
|
||||||
|
|
||||||
|
class NoTor(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
strings = {}
|
strings = {}
|
||||||
@ -25,12 +29,20 @@ strings = {}
|
|||||||
# generate an unguessable string
|
# generate an unguessable string
|
||||||
slug = os.urandom(16).encode('hex')
|
slug = os.urandom(16).encode('hex')
|
||||||
|
|
||||||
# file information
|
# information about the file
|
||||||
filename = filehash = filesize = ''
|
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))
|
@app.route("/{0}".format(slug))
|
||||||
def index():
|
def index():
|
||||||
global filename, filesize, filehash, slug, strings
|
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(),
|
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)
|
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]
|
lang = lc[:2]
|
||||||
if lang in translated:
|
if lang in translated:
|
||||||
strings = translated[lang]
|
strings = translated[lang]
|
||||||
|
return strings
|
||||||
|
|
||||||
def main():
|
def file_crunching(filename):
|
||||||
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)
|
|
||||||
|
|
||||||
# calculate filehash, file size
|
# calculate filehash, file size
|
||||||
print strings["calculating_sha1"]
|
|
||||||
BLOCKSIZE = 65536
|
BLOCKSIZE = 65536
|
||||||
hasher = hashlib.sha1()
|
hasher = hashlib.sha1()
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, 'rb') as f:
|
||||||
@ -104,15 +104,18 @@ def main():
|
|||||||
buf = f.read(BLOCKSIZE)
|
buf = f.read(BLOCKSIZE)
|
||||||
filehash = hasher.hexdigest()
|
filehash = hasher.hexdigest()
|
||||||
filesize = os.path.getsize(filename)
|
filesize = os.path.getsize(filename)
|
||||||
|
return filehash, filesize
|
||||||
|
|
||||||
|
def choose_port():
|
||||||
# let the OS choose a port
|
# let the OS choose a port
|
||||||
tmpsock = socket.socket()
|
tmpsock = socket.socket()
|
||||||
tmpsock.bind(("127.0.0.1", 0))
|
tmpsock.bind(("127.0.0.1", 0))
|
||||||
port = tmpsock.getsockname()[1]
|
port = tmpsock.getsockname()[1]
|
||||||
tmpsock.close()
|
tmpsock.close()
|
||||||
|
return port
|
||||||
|
|
||||||
|
def start_hidden_service(port):
|
||||||
# connect to the tor controlport
|
# connect to the tor controlport
|
||||||
print strings["connecting_ctrlport"].format(port)
|
|
||||||
controlports = [9051, 9151]
|
controlports = [9051, 9151]
|
||||||
controller = False
|
controller = False
|
||||||
for controlport in controlports:
|
for controlport in controlports:
|
||||||
@ -121,7 +124,7 @@ def main():
|
|||||||
except SocketError:
|
except SocketError:
|
||||||
pass
|
pass
|
||||||
if not controller:
|
if not controller:
|
||||||
sys.exit(strings["cant_connect_ctrlport"].format(controlports))
|
raise NoTor(strings["cant_connect_ctrlport"].format(controlports))
|
||||||
controller.authenticate()
|
controller.authenticate()
|
||||||
|
|
||||||
# set up hidden service
|
# set up hidden service
|
||||||
@ -130,11 +133,33 @@ def main():
|
|||||||
('HiddenServicePort', '80 127.0.0.1:{0}'.format(port))
|
('HiddenServicePort', '80 127.0.0.1:{0}'.format(port))
|
||||||
])
|
])
|
||||||
onion_host = get_hidden_service_hostname(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)
|
tails_open_port(port)
|
||||||
|
|
||||||
# instructions
|
|
||||||
print '\n' + strings["give_this_url"]
|
print '\n' + strings["give_this_url"]
|
||||||
print 'http://{0}/{1}'.format(onion_host, slug)
|
print 'http://{0}/{1}'.format(onion_host, slug)
|
||||||
print ''
|
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()
|
4
setup.py
4
setup.py
@ -27,6 +27,6 @@ setup(
|
|||||||
],
|
],
|
||||||
license="GPL v3",
|
license="GPL v3",
|
||||||
keywords='onion, share, onionshare, tor, anonymous, web server',
|
keywords='onion, share, onionshare, tor, anonymous, web server',
|
||||||
packages=['onionshare'],
|
packages=['onionshare', 'onionshare_gui'],
|
||||||
scripts=['bin/onionshare']
|
scripts=['bin/onionshare', 'bin/onionshare-gui']
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user