Merge branch 'share-code' of https://github.com/hiromipaw/onionshare into hiromipaw-share-code

This commit is contained in:
Micah Lee 2019-09-01 17:49:22 -04:00
commit 328b1500ab
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
7 changed files with 251 additions and 218 deletions

218
onionshare/web/base_mode.py Normal file
View File

@ -0,0 +1,218 @@
import os
import sys
import tempfile
import mimetypes
from flask import Response, request, render_template, make_response, send_from_directory
from .. import strings
class BaseModeWeb(object):
"""
All of the web logic shared between share and website mode
"""
def __init__(self, common, web):
super(BaseModeWeb, self).__init__()
self.common = common
self.web = web
# Information about the file to be shared
self.file_info = []
self.is_zipped = False
self.download_filename = None
self.download_filesize = None
self.gzip_filename = None
self.gzip_filesize = None
self.zip_writer = None
# Dictionary mapping file paths to filenames on disk
self.files = {}
# This is only the root files and dirs, as opposed to all of them
self.root_files = {}
self.cleanup_filenames = []
self.file_info = {'files': [], 'dirs': []}
self.visit_count = 0
self.download_count = 0
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
self.download_in_progress = False
self.define_routes()
def init(self):
"""
Add custom initialization here.
"""
pass
def directory_listing(self, filenames, path='', filesystem_path=None):
# If filesystem_path is None, this is the root directory listing
files = []
dirs = []
r = ''
files, dirs = self.build_directory_listing(filenames, filesystem_path)
if self.web.mode == 'website':
r = make_response(render_template('listing.html',
path=path,
files=files,
dirs=dirs,
static_url_path=self.web.static_url_path))
elif self.web.mode == 'share':
r = make_response(render_template(
'send.html',
file_info=self.file_info,
files=files,
dirs=dirs,
filename=os.path.basename(self.download_filename),
filesize=self.filesize,
filesize_human=self.common.human_readable_filesize(self.download_filesize),
is_zipped=self.is_zipped,
static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r)
def build_directory_listing(self, filenames, filesystem_path):
files = []
dirs = []
for filename in filenames:
if filesystem_path:
this_filesystem_path = os.path.join(filesystem_path, filename)
else:
this_filesystem_path = self.files[filename]
is_dir = os.path.isdir(this_filesystem_path)
if is_dir:
dirs.append({
'basename': filename
})
else:
size = os.path.getsize(this_filesystem_path)
size_human = self.common.human_readable_filesize(size)
files.append({
'basename': filename,
'size_human': size_human
})
return files, dirs
def set_file_info(self, filenames, processed_size_callback=None):
"""
Build a data structure that describes the list of files
"""
# If there's just one folder, replace filenames with a list of files inside that folder
if len(filenames) == 1 and os.path.isdir(filenames[0]):
filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
self.build_file_list(filenames)
if self.web.mode == 'share':
self.common.log("ShareModeWeb", "set_file_info")
self.web.cancel_compression = False
self.build_zipfile_list(filenames, processed_size_callback)
elif self.web.mode == 'website':
self.common.log("WebsiteModeWeb", "set_file_info")
self.web.cancel_compression = True
return True
def build_file_list(self, filenames):
"""
Build a data structure that describes the list of files that make up
the static website.
"""
self.common.log("BaseModeWeb", "build_file_list")
# Loop through the files
for filename in filenames:
basename = os.path.basename(filename.rstrip('/'))
# If it's a filename, add it
if os.path.isfile(filename):
self.files[basename] = filename
self.root_files[basename] = filename
# If it's a directory, add it recursively
elif os.path.isdir(filename):
self.root_files[basename + '/'] = filename
for root, _, nested_filenames in os.walk(filename):
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
# The normalized_root should be "some_folder/foobar"
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
# Add the dir itself
self.files[normalized_root + '/'] = root
# Add the files in this dir
for nested_filename in nested_filenames:
self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
return True
def render_logic(self, path=''):
if path in self.files:
filesystem_path = self.files[path]
# If it's a directory
if os.path.isdir(filesystem_path):
# Is there an index.html?
index_path = os.path.join(path, 'index.html')
if self.web.mode == 'website' and index_path in self.files:
# Render it
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
else:
# Otherwise, render directory listing
filenames = []
for filename in os.listdir(filesystem_path):
if os.path.isdir(os.path.join(filesystem_path, filename)):
filenames.append(filename + '/')
else:
filenames.append(filename)
filenames.sort()
return self.directory_listing(filenames, path, filesystem_path)
# If it's a file
elif os.path.isfile(filesystem_path):
dirname = os.path.dirname(filesystem_path)
basename = os.path.basename(filesystem_path)
return send_from_directory(dirname, basename)
# If it's not a directory or file, throw a 404
else:
return self.web.error404()
else:
# Special case loading /
if path == '':
index_path = 'index.html'
if self.web.mode == 'website' and index_path in self.files:
# Render it
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
else:
# Root directory listing
filenames = list(self.root_files)
filenames.sort()
return self.directory_listing(filenames, path)
else:
# If the path isn't found, throw a 404
return self.web.error404()

View File

@ -18,9 +18,6 @@ class ReceiveModeWeb(object):
self.web = web self.web = web
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.can_upload = True self.can_upload = True
self.upload_count = 0 self.upload_count = 0
self.uploads_in_progress = [] self.uploads_in_progress = []

View File

@ -6,46 +6,26 @@ import mimetypes
import gzip import gzip
from flask import Response, request, render_template, make_response from flask import Response, request, render_template, make_response
from .base_mode import BaseModeWeb
from .. import strings from .. import strings
class ShareModeWeb(object): class ShareModeWeb(BaseModeWeb):
""" """
All of the web logic for share mode All of the web logic for share mode
""" """
def __init__(self, common, web): def init(self):
self.common = common
self.common.log('ShareModeWeb', '__init__') self.common.log('ShareModeWeb', '__init__')
self.web = web
# Information about the file to be shared
self.file_info = []
self.is_zipped = False
self.download_filename = None
self.download_filesize = None
self.gzip_filename = None
self.gzip_filesize = None
self.zip_writer = None
self.download_count = 0
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
self.download_in_progress = False
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.define_routes() self.define_routes()
def define_routes(self): def define_routes(self):
""" """
The web app routes for sharing files The web app routes for sharing files
""" """
@self.web.app.route("/") @self.web.app.route('/', defaults={'path': ''})
def index(): @self.web.app.route('/<path:path>')
def index(path):
""" """
Render the template for the onionshare landing page. Render the template for the onionshare landing page.
""" """
@ -65,15 +45,8 @@ class ShareModeWeb(object):
else: else:
self.filesize = self.download_filesize self.filesize = self.download_filesize
r = make_response(render_template( return self.render_logic(path)
'send.html',
file_info=self.file_info,
filename=os.path.basename(self.download_filename),
filesize=self.filesize,
filesize_human=self.common.human_readable_filesize(self.download_filesize),
is_zipped=self.is_zipped,
static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r)
@self.web.app.route("/download") @self.web.app.route("/download")
def download(): def download():
@ -198,19 +171,8 @@ class ShareModeWeb(object):
r.headers.set('Content-Type', content_type) r.headers.set('Content-Type', content_type)
return r return r
def set_file_info(self, filenames, processed_size_callback=None): def build_zipfile_list(self, filenames, processed_size_callback=None):
""" self.common.log("ShareModeWeb", "build_zipfile_list")
Using the list of filenames being shared, fill in details that the web
page will need to display. This includes zipping up the file in order to
get the zip file's name and size.
"""
self.common.log("ShareModeWeb", "set_file_info")
self.web.cancel_compression = False
self.cleanup_filenames = []
# build file info list
self.file_info = {'files': [], 'dirs': []}
for filename in filenames: for filename in filenames:
info = { info = {
'filename': filename, 'filename': filename,

View File

@ -51,12 +51,16 @@ class Web(object):
self.common = common self.common = common
self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode)) self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode))
# The static URL path has a 128-bit random number in it to avoid having name
# collisions with files that might be getting shared
self.static_url_path = '/static_{}'.format(self.common.random_string(16))
# The flask app # The flask app
self.app = Flask(__name__, self.app = Flask(__name__,
static_url_path=self.static_url_path,
static_folder=self.common.get_resource_path('static'), static_folder=self.common.get_resource_path('static'),
template_folder=self.common.get_resource_path('templates')) template_folder=self.common.get_resource_path('templates'))
self.app.secret_key = self.common.random_string(8) self.app.secret_key = self.common.random_string(8)
self.generate_static_url_path()
self.auth = HTTPBasicAuth() self.auth = HTTPBasicAuth()
self.auth.error_handler(self.error401) self.auth.error_handler(self.error401)
@ -225,18 +229,6 @@ class Web(object):
self.password = self.common.build_password() self.password = self.common.build_password()
self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password)) self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password))
def generate_static_url_path(self):
# The static URL path has a 128-bit random number in it to avoid having name
# collisions with files that might be getting shared
self.static_url_path = '/static_{}'.format(self.common.random_string(16))
self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path))
# Update the flask route to handle the new static URL path
self.app.static_url_path = self.static_url_path
self.app.add_url_rule(
self.static_url_path + '/<path:filename>',
endpoint='static', view_func=self.app.send_static_file)
def verbose_mode(self): def verbose_mode(self):
""" """
Turn on verbose mode, which will log flask errors to a file. Turn on verbose mode, which will log flask errors to a file.

View File

@ -2,30 +2,21 @@ import os
import sys import sys
import tempfile import tempfile
import mimetypes import mimetypes
from flask import Response, request, render_template, make_response, send_from_directory from flask import Response, request, render_template, make_response
from .base_mode import BaseModeWeb
from .. import strings from .. import strings
class WebsiteModeWeb(object): class WebsiteModeWeb(BaseModeWeb):
""" """
All of the web logic for share mode All of the web logic for website mode
""" """
def __init__(self, common, web): def init(self):
self.common = common
self.common.log('WebsiteModeWeb', '__init__') self.common.log('WebsiteModeWeb', '__init__')
self.web = web
# Dictionary mapping file paths to filenames on disk
self.files = {}
self.visit_count = 0
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.define_routes() self.define_routes()
def define_routes(self): def define_routes(self):
""" """
The web app routes for sharing a website The web app routes for sharing a website
@ -51,131 +42,4 @@ class WebsiteModeWeb(object):
'action': 'visit' 'action': 'visit'
}) })
if path in self.files: return self.render_logic(path)
filesystem_path = self.files[path]
# If it's a directory
if os.path.isdir(filesystem_path):
# Is there an index.html?
index_path = os.path.join(path, 'index.html')
if index_path in self.files:
# Render it
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
else:
# Otherwise, render directory listing
filenames = []
for filename in os.listdir(filesystem_path):
if os.path.isdir(os.path.join(filesystem_path, filename)):
filenames.append(filename + '/')
else:
filenames.append(filename)
filenames.sort()
return self.directory_listing(path, filenames, filesystem_path)
# If it's a file
elif os.path.isfile(filesystem_path):
dirname = os.path.dirname(filesystem_path)
basename = os.path.basename(filesystem_path)
return send_from_directory(dirname, basename)
# If it's not a directory or file, throw a 404
else:
return self.web.error404()
else:
# Special case loading /
if path == '':
index_path = 'index.html'
if index_path in self.files:
# Render it
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
else:
# Root directory listing
filenames = list(self.root_files)
filenames.sort()
return self.directory_listing(path, filenames)
else:
# If the path isn't found, throw a 404
return self.web.error404()
def directory_listing(self, path, filenames, filesystem_path=None):
# If filesystem_path is None, this is the root directory listing
files = []
dirs = []
for filename in filenames:
if filesystem_path:
this_filesystem_path = os.path.join(filesystem_path, filename)
else:
this_filesystem_path = self.files[filename]
is_dir = os.path.isdir(this_filesystem_path)
if is_dir:
dirs.append({
'basename': filename
})
else:
size = os.path.getsize(this_filesystem_path)
size_human = self.common.human_readable_filesize(size)
files.append({
'basename': filename,
'size_human': size_human
})
r = make_response(render_template('listing.html',
path=path,
files=files,
dirs=dirs,
static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r)
def set_file_info(self, filenames):
"""
Build a data structure that describes the list of files that make up
the static website.
"""
self.common.log("WebsiteModeWeb", "set_file_info")
# This is a dictionary that maps HTTP routes to filenames on disk
self.files = {}
# This is only the root files and dirs, as opposed to all of them
self.root_files = {}
# If there's just one folder, replace filenames with a list of files inside that folder
if len(filenames) == 1 and os.path.isdir(filenames[0]):
filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
# Loop through the files
for filename in filenames:
basename = os.path.basename(filename.rstrip('/'))
# If it's a filename, add it
if os.path.isfile(filename):
self.files[basename] = filename
self.root_files[basename] = filename
# If it's a directory, add it recursively
elif os.path.isdir(filename):
self.root_files[basename + '/'] = filename
for root, _, nested_filenames in os.walk(filename):
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
# The normalized_root should be "some_folder/foobar"
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
# Add the dir itself
self.files[normalized_root + '/'] = root
# Add the files in this dir
for nested_filename in nested_filenames:
self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
return True

View File

@ -42,9 +42,6 @@ class OnionThread(QtCore.QThread):
def run(self): def run(self):
self.mode.common.log('OnionThread', 'run') self.mode.common.log('OnionThread', 'run')
# Make a new static URL path for each new share
self.mode.web.generate_static_url_path()
# Choose port and password early, because we need them to exist in advance for scheduled shares # Choose port and password early, because we need them to exist in advance for scheduled shares
self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download')
if not self.mode.app.port: if not self.mode.app.port:

View File

@ -28,24 +28,27 @@
<th id="size-header">Size</th> <th id="size-header">Size</th>
<th></th> <th></th>
</tr> </tr>
{% for info in file_info.dirs %} {% for info in dirs %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" />
<a href="{{ info.basename }}">
{{ info.basename }} {{ info.basename }}
</a>
</td> </td>
<td>{{ info.size_human }}</td> <td>&mdash;</td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
{% for info in file_info.files %}
{% for info in files %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" />
<a href="{{ info.basename }}">
{{ info.basename }} {{ info.basename }}
</a>
</td> </td>
<td>{{ info.size_human }}</td> <td>{{ info.size_human }}</td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>