onionshare_cli 2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- onionshare_cli/__init__.py +522 -0
- onionshare_cli/common.py +296 -0
- onionshare_cli/mode_settings.py +146 -0
- onionshare_cli/onion.py +845 -0
- onionshare_cli/onionshare.py +110 -0
- onionshare_cli/resources/static/css/style.css +393 -0
- onionshare_cli/resources/static/img/ajax.gif +0 -0
- onionshare_cli/resources/static/img/favicon.ico +0 -0
- onionshare_cli/resources/static/img/logo.png +0 -0
- onionshare_cli/resources/static/img/logo_large.png +0 -0
- onionshare_cli/resources/static/img/web_file.png +0 -0
- onionshare_cli/resources/static/img/web_folder.png +0 -0
- onionshare_cli/resources/static/js/chat.js +173 -0
- onionshare_cli/resources/static/js/jquery-3.5.1.min.js +2 -0
- onionshare_cli/resources/static/js/receive.js +130 -0
- onionshare_cli/resources/static/js/send.js +75 -0
- onionshare_cli/resources/static/js/socket.io.min.js +3 -0
- onionshare_cli/resources/templates/401.html +21 -0
- onionshare_cli/resources/templates/403.html +21 -0
- onionshare_cli/resources/templates/404.html +21 -0
- onionshare_cli/resources/templates/405.html +21 -0
- onionshare_cli/resources/templates/chat.html +46 -0
- onionshare_cli/resources/templates/denied.html +14 -0
- onionshare_cli/resources/templates/listing.html +59 -0
- onionshare_cli/resources/templates/receive.html +46 -0
- onionshare_cli/resources/templates/send.html +69 -0
- onionshare_cli/resources/templates/thankyou.html +27 -0
- onionshare_cli/resources/torrc_template +8 -0
- onionshare_cli/resources/torrc_template-meek_lite_amazon +2 -0
- onionshare_cli/resources/torrc_template-meek_lite_azure +2 -0
- onionshare_cli/resources/torrc_template-obfs4 +27 -0
- onionshare_cli/resources/version.txt +1 -0
- onionshare_cli/resources/wordlist.txt +7776 -0
- onionshare_cli/settings.py +201 -0
- onionshare_cli/web/__init__.py +21 -0
- onionshare_cli/web/chat_mode.py +169 -0
- onionshare_cli/web/receive_mode.py +488 -0
- onionshare_cli/web/send_base_mode.py +338 -0
- onionshare_cli/web/share_mode.py +406 -0
- onionshare_cli/web/web.py +424 -0
- onionshare_cli/web/website_mode.py +123 -0
- onionshare_cli-2.3.dist-info/METADATA +34 -0
- onionshare_cli-2.3.dist-info/RECORD +45 -0
- onionshare_cli-2.3.dist-info/WHEEL +4 -0
- onionshare_cli-2.3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
OnionShare | https://onionshare.org/
|
|
4
|
+
|
|
5
|
+
Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com>
|
|
6
|
+
|
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import mimetypes
|
|
25
|
+
import gzip
|
|
26
|
+
from flask import Response, request, render_template, make_response
|
|
27
|
+
from unidecode import unidecode
|
|
28
|
+
from werkzeug.urls import url_quote
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SendBaseModeWeb:
|
|
32
|
+
"""
|
|
33
|
+
All of the web logic shared between share and website mode (modes where the user sends files)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, common, web):
|
|
37
|
+
super(SendBaseModeWeb, self).__init__()
|
|
38
|
+
self.common = common
|
|
39
|
+
self.web = web
|
|
40
|
+
|
|
41
|
+
# Information about the file to be shared
|
|
42
|
+
self.is_zipped = False
|
|
43
|
+
self.download_filename = None
|
|
44
|
+
self.download_filesize = None
|
|
45
|
+
self.gzip_filename = None
|
|
46
|
+
self.gzip_filesize = None
|
|
47
|
+
self.zip_writer = None
|
|
48
|
+
|
|
49
|
+
# If autostop_sharing, only allow one download at a time
|
|
50
|
+
self.download_in_progress = False
|
|
51
|
+
|
|
52
|
+
# This tracks the history id
|
|
53
|
+
self.cur_history_id = 0
|
|
54
|
+
|
|
55
|
+
self.define_routes()
|
|
56
|
+
self.init()
|
|
57
|
+
|
|
58
|
+
def set_file_info(self, filenames, processed_size_callback=None):
|
|
59
|
+
"""
|
|
60
|
+
Build a data structure that describes the list of files
|
|
61
|
+
"""
|
|
62
|
+
# If there's just one folder, replace filenames with a list of files inside that folder
|
|
63
|
+
if len(filenames) == 1 and os.path.isdir(filenames[0]):
|
|
64
|
+
filenames = [
|
|
65
|
+
os.path.join(filenames[0], x) for x in os.listdir(filenames[0])
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Re-initialize
|
|
69
|
+
self.files = {} # Dictionary mapping file paths to filenames on disk
|
|
70
|
+
self.root_files = (
|
|
71
|
+
{}
|
|
72
|
+
) # This is only the root files and dirs, as opposed to all of them
|
|
73
|
+
self.cleanup_filenames = []
|
|
74
|
+
self.cur_history_id = 0
|
|
75
|
+
self.file_info = {"files": [], "dirs": []}
|
|
76
|
+
self.gzip_individual_files = {}
|
|
77
|
+
self.init()
|
|
78
|
+
|
|
79
|
+
# Build the file list
|
|
80
|
+
for filename in filenames:
|
|
81
|
+
basename = os.path.basename(filename.rstrip("/"))
|
|
82
|
+
|
|
83
|
+
# If it's a filename, add it
|
|
84
|
+
if os.path.isfile(filename):
|
|
85
|
+
self.files[basename] = filename
|
|
86
|
+
self.root_files[basename] = filename
|
|
87
|
+
|
|
88
|
+
# If it's a directory, add it recursively
|
|
89
|
+
elif os.path.isdir(filename):
|
|
90
|
+
self.root_files[basename] = filename
|
|
91
|
+
|
|
92
|
+
for root, _, nested_filenames in os.walk(filename):
|
|
93
|
+
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
|
|
94
|
+
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
|
|
95
|
+
# The normalized_root should be "some_folder/foobar"
|
|
96
|
+
normalized_root = os.path.join(
|
|
97
|
+
basename, root[len(filename) :].lstrip("/")
|
|
98
|
+
).rstrip("/")
|
|
99
|
+
|
|
100
|
+
# Add the dir itself
|
|
101
|
+
self.files[normalized_root] = root
|
|
102
|
+
|
|
103
|
+
# Add the files in this dir
|
|
104
|
+
for nested_filename in nested_filenames:
|
|
105
|
+
self.files[
|
|
106
|
+
os.path.join(normalized_root, nested_filename)
|
|
107
|
+
] = os.path.join(root, nested_filename)
|
|
108
|
+
|
|
109
|
+
self.set_file_info_custom(filenames, processed_size_callback)
|
|
110
|
+
|
|
111
|
+
def directory_listing(self, filenames, path="", filesystem_path=None):
|
|
112
|
+
# Tell the GUI about the directory listing
|
|
113
|
+
history_id = self.cur_history_id
|
|
114
|
+
self.cur_history_id += 1
|
|
115
|
+
self.web.add_request(
|
|
116
|
+
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
|
|
117
|
+
f"/{path}",
|
|
118
|
+
{"id": history_id, "method": request.method, "status_code": 200},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
breadcrumbs = [("☗", "/")]
|
|
122
|
+
parts = path.split("/")
|
|
123
|
+
if parts[-1] == "":
|
|
124
|
+
parts = parts[:-1]
|
|
125
|
+
for i in range(len(parts)):
|
|
126
|
+
breadcrumbs.append((parts[i], f"/{'/'.join(parts[0 : i + 1])}"))
|
|
127
|
+
breadcrumbs_leaf = breadcrumbs.pop()[0]
|
|
128
|
+
|
|
129
|
+
# If filesystem_path is None, this is the root directory listing
|
|
130
|
+
files, dirs = self.build_directory_listing(path, filenames, filesystem_path)
|
|
131
|
+
r = self.directory_listing_template(
|
|
132
|
+
path, files, dirs, breadcrumbs, breadcrumbs_leaf
|
|
133
|
+
)
|
|
134
|
+
return self.web.add_security_headers(r)
|
|
135
|
+
|
|
136
|
+
def build_directory_listing(self, path, filenames, filesystem_path):
|
|
137
|
+
files = []
|
|
138
|
+
dirs = []
|
|
139
|
+
|
|
140
|
+
for filename in filenames:
|
|
141
|
+
if filesystem_path:
|
|
142
|
+
this_filesystem_path = os.path.join(filesystem_path, filename)
|
|
143
|
+
else:
|
|
144
|
+
this_filesystem_path = self.files[filename]
|
|
145
|
+
|
|
146
|
+
is_dir = os.path.isdir(this_filesystem_path)
|
|
147
|
+
|
|
148
|
+
if is_dir:
|
|
149
|
+
dirs.append(
|
|
150
|
+
{"link": os.path.join(f"/{path}", filename), "basename": filename}
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
size = os.path.getsize(this_filesystem_path)
|
|
154
|
+
size_human = self.common.human_readable_filesize(size)
|
|
155
|
+
files.append(
|
|
156
|
+
{
|
|
157
|
+
"link": os.path.join(f"/{path}", filename),
|
|
158
|
+
"basename": filename,
|
|
159
|
+
"size_human": size_human,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return files, dirs
|
|
164
|
+
|
|
165
|
+
def stream_individual_file(self, filesystem_path):
|
|
166
|
+
"""
|
|
167
|
+
Return a flask response that's streaming the download of an individual file, and gzip
|
|
168
|
+
compressing it if the browser supports it.
|
|
169
|
+
"""
|
|
170
|
+
use_gzip = self.should_use_gzip()
|
|
171
|
+
|
|
172
|
+
# gzip compress the individual file, if it hasn't already been compressed
|
|
173
|
+
if use_gzip:
|
|
174
|
+
if filesystem_path not in self.gzip_individual_files:
|
|
175
|
+
gzip_filename = tempfile.mkstemp("wb+")[1]
|
|
176
|
+
self._gzip_compress(filesystem_path, gzip_filename, 6, None)
|
|
177
|
+
self.gzip_individual_files[filesystem_path] = gzip_filename
|
|
178
|
+
|
|
179
|
+
# Make sure the gzip file gets cleaned up when onionshare stops
|
|
180
|
+
self.cleanup_filenames.append(gzip_filename)
|
|
181
|
+
|
|
182
|
+
file_to_download = self.gzip_individual_files[filesystem_path]
|
|
183
|
+
filesize = os.path.getsize(self.gzip_individual_files[filesystem_path])
|
|
184
|
+
else:
|
|
185
|
+
file_to_download = filesystem_path
|
|
186
|
+
filesize = os.path.getsize(filesystem_path)
|
|
187
|
+
|
|
188
|
+
path = request.path
|
|
189
|
+
|
|
190
|
+
# Tell GUI the individual file started
|
|
191
|
+
history_id = self.cur_history_id
|
|
192
|
+
self.cur_history_id += 1
|
|
193
|
+
|
|
194
|
+
# Only GET requests are allowed, any other method should fail
|
|
195
|
+
if request.method != "GET":
|
|
196
|
+
return self.web.error405(history_id)
|
|
197
|
+
|
|
198
|
+
self.web.add_request(
|
|
199
|
+
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
|
|
200
|
+
path,
|
|
201
|
+
{"id": history_id, "filesize": filesize},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def generate():
|
|
205
|
+
chunk_size = 102400 # 100kb
|
|
206
|
+
|
|
207
|
+
fp = open(file_to_download, "rb")
|
|
208
|
+
done = False
|
|
209
|
+
while not done:
|
|
210
|
+
chunk = fp.read(chunk_size)
|
|
211
|
+
if chunk == b"":
|
|
212
|
+
done = True
|
|
213
|
+
else:
|
|
214
|
+
try:
|
|
215
|
+
yield chunk
|
|
216
|
+
|
|
217
|
+
# Tell GUI the progress
|
|
218
|
+
downloaded_bytes = fp.tell()
|
|
219
|
+
percent = (1.0 * downloaded_bytes / filesize) * 100
|
|
220
|
+
if (
|
|
221
|
+
not self.web.is_gui
|
|
222
|
+
or self.common.platform == "Linux"
|
|
223
|
+
or self.common.platform == "BSD"
|
|
224
|
+
):
|
|
225
|
+
sys.stdout.write(
|
|
226
|
+
"\r{0:s}, {1:.2f}% ".format(
|
|
227
|
+
self.common.human_readable_filesize(
|
|
228
|
+
downloaded_bytes
|
|
229
|
+
),
|
|
230
|
+
percent,
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
sys.stdout.flush()
|
|
234
|
+
|
|
235
|
+
self.web.add_request(
|
|
236
|
+
self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS,
|
|
237
|
+
path,
|
|
238
|
+
{
|
|
239
|
+
"id": history_id,
|
|
240
|
+
"bytes": downloaded_bytes,
|
|
241
|
+
"filesize": filesize,
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
done = False
|
|
245
|
+
except:
|
|
246
|
+
# Looks like the download was canceled
|
|
247
|
+
done = True
|
|
248
|
+
|
|
249
|
+
# Tell the GUI the individual file was canceled
|
|
250
|
+
self.web.add_request(
|
|
251
|
+
self.web.REQUEST_INDIVIDUAL_FILE_CANCELED,
|
|
252
|
+
path,
|
|
253
|
+
{"id": history_id},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
fp.close()
|
|
257
|
+
|
|
258
|
+
if self.common.platform != "Darwin":
|
|
259
|
+
sys.stdout.write("\n")
|
|
260
|
+
|
|
261
|
+
basename = os.path.basename(filesystem_path)
|
|
262
|
+
|
|
263
|
+
r = Response(generate())
|
|
264
|
+
if use_gzip:
|
|
265
|
+
r.headers.set("Content-Encoding", "gzip")
|
|
266
|
+
r.headers.set("Content-Length", filesize)
|
|
267
|
+
filename_dict = {
|
|
268
|
+
'filename': unidecode(basename),
|
|
269
|
+
'filename*': "UTF-8''%s" % url_quote(basename)
|
|
270
|
+
}
|
|
271
|
+
r.headers.set("Content-Disposition", "inline", **filename_dict)
|
|
272
|
+
r = self.web.add_security_headers(r)
|
|
273
|
+
(content_type, _) = mimetypes.guess_type(basename, strict=False)
|
|
274
|
+
if content_type is not None:
|
|
275
|
+
r.headers.set("Content-Type", content_type)
|
|
276
|
+
return r
|
|
277
|
+
|
|
278
|
+
def should_use_gzip(self):
|
|
279
|
+
"""
|
|
280
|
+
Should we use gzip for this browser?
|
|
281
|
+
"""
|
|
282
|
+
return (not self.is_zipped) and (
|
|
283
|
+
"gzip" in request.headers.get("Accept-Encoding", "").lower()
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _gzip_compress(
|
|
287
|
+
self, input_filename, output_filename, level, processed_size_callback=None
|
|
288
|
+
):
|
|
289
|
+
"""
|
|
290
|
+
Compress a file with gzip, without loading the whole thing into memory
|
|
291
|
+
Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
|
|
292
|
+
"""
|
|
293
|
+
bytes_processed = 0
|
|
294
|
+
blocksize = 1 << 16 # 64kB
|
|
295
|
+
with open(input_filename, "rb") as input_file:
|
|
296
|
+
output_file = gzip.open(output_filename, "wb", level)
|
|
297
|
+
while True:
|
|
298
|
+
if processed_size_callback is not None:
|
|
299
|
+
processed_size_callback(bytes_processed)
|
|
300
|
+
|
|
301
|
+
block = input_file.read(blocksize)
|
|
302
|
+
if len(block) == 0:
|
|
303
|
+
break
|
|
304
|
+
output_file.write(block)
|
|
305
|
+
bytes_processed += blocksize
|
|
306
|
+
|
|
307
|
+
output_file.close()
|
|
308
|
+
|
|
309
|
+
def init(self):
|
|
310
|
+
"""
|
|
311
|
+
Inherited class will implement this
|
|
312
|
+
"""
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
def define_routes(self):
|
|
316
|
+
"""
|
|
317
|
+
Inherited class will implement this
|
|
318
|
+
"""
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
def directory_listing_template(self):
|
|
322
|
+
"""
|
|
323
|
+
Inherited class will implement this. It should call render_template and return
|
|
324
|
+
the response.
|
|
325
|
+
"""
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
def set_file_info_custom(self, filenames, processed_size_callback):
|
|
329
|
+
"""
|
|
330
|
+
Inherited class will implement this.
|
|
331
|
+
"""
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
def render_logic(self, path=""):
|
|
335
|
+
"""
|
|
336
|
+
Inherited class will implement this.
|
|
337
|
+
"""
|
|
338
|
+
pass
|