onionshare_cli 2.6.1__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 +538 -0
- onionshare_cli/censorship.py +311 -0
- onionshare_cli/common.py +624 -0
- onionshare_cli/meek.py +199 -0
- onionshare_cli/mode_settings.py +150 -0
- onionshare_cli/onion.py +964 -0
- onionshare_cli/onionshare.py +91 -0
- onionshare_cli/resources/static/css/style.css +432 -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 +188 -0
- onionshare_cli/resources/static/js/jquery-3.5.1.min.js +2 -0
- onionshare_cli/resources/static/js/receive.js +141 -0
- onionshare_cli/resources/static/js/send.js +79 -0
- onionshare_cli/resources/static/js/socket.io.js +9 -0
- onionshare_cli/resources/static/js/socket.io.min.js +7 -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/500.html +21 -0
- onionshare_cli/resources/templates/chat.html +47 -0
- onionshare_cli/resources/templates/denied.html +14 -0
- onionshare_cli/resources/templates/listing.html +60 -0
- onionshare_cli/resources/templates/receive.html +66 -0
- onionshare_cli/resources/templates/send.html +70 -0
- onionshare_cli/resources/templates/thankyou.html +27 -0
- onionshare_cli/resources/torrc_template +12 -0
- onionshare_cli/resources/torrc_template-meek_lite_azure +2 -0
- onionshare_cli/resources/torrc_template-obfs4 +12 -0
- onionshare_cli/resources/torrc_template-snowflake +3 -0
- onionshare_cli/resources/version.txt +1 -0
- onionshare_cli/resources/wordlist.txt +7776 -0
- onionshare_cli/settings.py +209 -0
- onionshare_cli/web/__init__.py +21 -0
- onionshare_cli/web/chat_mode.py +190 -0
- onionshare_cli/web/receive_mode.py +630 -0
- onionshare_cli/web/send_base_mode.py +360 -0
- onionshare_cli/web/share_mode.py +599 -0
- onionshare_cli/web/web.py +409 -0
- onionshare_cli/web/website_mode.py +122 -0
- onionshare_cli-2.6.1.dist-info/METADATA +44 -0
- onionshare_cli-2.6.1.dist-info/RECORD +48 -0
- onionshare_cli-2.6.1.dist-info/WHEEL +4 -0
- onionshare_cli-2.6.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
OnionShare | https://onionshare.org/
|
|
4
|
+
|
|
5
|
+
Copyright (C) 2014-2022 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
|
|
27
|
+
from unidecode import unidecode
|
|
28
|
+
from urllib.parse import 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.zip_writer = None
|
|
46
|
+
|
|
47
|
+
# Create a temporary dir to store gzip files in
|
|
48
|
+
self.gzip_tmp_dir = tempfile.TemporaryDirectory(dir=self.common.build_tmp_dir())
|
|
49
|
+
self.gzip_counter = 0
|
|
50
|
+
|
|
51
|
+
# If autostop_sharing, only allow one download at a time
|
|
52
|
+
self.download_in_progress = False
|
|
53
|
+
|
|
54
|
+
# This tracks the history id
|
|
55
|
+
self.cur_history_id = 0
|
|
56
|
+
|
|
57
|
+
# Whether or not we can send REQUEST_INDIVIDUAL_FILE_STARTED
|
|
58
|
+
# and maybe other events when requests come in to this mode
|
|
59
|
+
self.supports_file_requests = True
|
|
60
|
+
|
|
61
|
+
self.define_routes()
|
|
62
|
+
self.init()
|
|
63
|
+
|
|
64
|
+
def fix_windows_paths(self, path):
|
|
65
|
+
"""
|
|
66
|
+
If on Windows, replace backslashes with slashes
|
|
67
|
+
"""
|
|
68
|
+
if self.common.platform == "Windows":
|
|
69
|
+
return path.replace("\\", "/")
|
|
70
|
+
|
|
71
|
+
return path
|
|
72
|
+
|
|
73
|
+
def set_file_info(self, filenames, processed_size_callback=None):
|
|
74
|
+
"""
|
|
75
|
+
Build a data structure that describes the list of files
|
|
76
|
+
"""
|
|
77
|
+
# If there's just one folder, replace filenames with a list of files inside that folder
|
|
78
|
+
if len(filenames) == 1 and os.path.isdir(filenames[0]):
|
|
79
|
+
filenames = [
|
|
80
|
+
os.path.join(filenames[0], x) for x in os.listdir(filenames[0])
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Re-initialize
|
|
84
|
+
self.files = {} # Dictionary mapping file paths to filenames on disk
|
|
85
|
+
self.root_files = (
|
|
86
|
+
{}
|
|
87
|
+
) # This is only the root files and dirs, as opposed to all of them
|
|
88
|
+
self.cur_history_id = 0
|
|
89
|
+
self.file_info = {"files": [], "dirs": []}
|
|
90
|
+
self.gzip_individual_files = {}
|
|
91
|
+
self.init()
|
|
92
|
+
|
|
93
|
+
# Windows paths use backslashes, but website paths use forward slashes. We have to
|
|
94
|
+
# make sure we're stripping the correct type of slash
|
|
95
|
+
if self.common.platform == "Windows":
|
|
96
|
+
slash = "\\"
|
|
97
|
+
else:
|
|
98
|
+
slash = "/"
|
|
99
|
+
|
|
100
|
+
# Build the file list
|
|
101
|
+
for filename in filenames:
|
|
102
|
+
basename = os.path.basename(filename.rstrip(slash))
|
|
103
|
+
|
|
104
|
+
# If it's a filename, add it
|
|
105
|
+
if os.path.isfile(filename):
|
|
106
|
+
self.files[self.fix_windows_paths(basename)] = filename
|
|
107
|
+
self.root_files[self.fix_windows_paths(basename)] = filename
|
|
108
|
+
|
|
109
|
+
# If it's a directory, add it recursively
|
|
110
|
+
elif os.path.isdir(filename):
|
|
111
|
+
self.root_files[self.fix_windows_paths(basename)] = filename
|
|
112
|
+
|
|
113
|
+
for root, _, nested_filenames in os.walk(filename):
|
|
114
|
+
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
|
|
115
|
+
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
|
|
116
|
+
# The normalized_root should be "some_folder/foobar"
|
|
117
|
+
normalized_root = os.path.join(
|
|
118
|
+
basename, root[len(filename) :].lstrip(slash)
|
|
119
|
+
).rstrip(slash)
|
|
120
|
+
|
|
121
|
+
# Add the dir itself
|
|
122
|
+
self.files[self.fix_windows_paths(normalized_root)] = root
|
|
123
|
+
|
|
124
|
+
# Add the files in this dir
|
|
125
|
+
for nested_filename in nested_filenames:
|
|
126
|
+
self.files[
|
|
127
|
+
self.fix_windows_paths(
|
|
128
|
+
os.path.join(normalized_root, nested_filename)
|
|
129
|
+
)
|
|
130
|
+
] = os.path.join(root, nested_filename)
|
|
131
|
+
|
|
132
|
+
self.set_file_info_custom(filenames, processed_size_callback)
|
|
133
|
+
|
|
134
|
+
def directory_listing(self, filenames, path="", filesystem_path=None, add_trailing_slash=False):
|
|
135
|
+
# Tell the GUI about the directory listing
|
|
136
|
+
history_id = self.cur_history_id
|
|
137
|
+
self.cur_history_id += 1
|
|
138
|
+
self.web.add_request(
|
|
139
|
+
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
|
|
140
|
+
f"/{path}",
|
|
141
|
+
{"id": history_id, "method": request.method, "status_code": 200},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
breadcrumbs = [("☗", "/")]
|
|
145
|
+
parts = path.split("/")
|
|
146
|
+
if parts[-1] == "":
|
|
147
|
+
parts = parts[:-1]
|
|
148
|
+
for i in range(len(parts)):
|
|
149
|
+
breadcrumbs.append((parts[i], f"/{'/'.join(parts[0 : i + 1])}"))
|
|
150
|
+
breadcrumbs_leaf = breadcrumbs.pop()[0]
|
|
151
|
+
|
|
152
|
+
# If filesystem_path is None, this is the root directory listing
|
|
153
|
+
files, dirs = self.build_directory_listing(path, filenames, filesystem_path, add_trailing_slash)
|
|
154
|
+
return self.directory_listing_template(
|
|
155
|
+
path, files, dirs, breadcrumbs, breadcrumbs_leaf
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def build_directory_listing(self, path, filenames, filesystem_path, add_trailing_slash=False):
|
|
159
|
+
files = []
|
|
160
|
+
dirs = []
|
|
161
|
+
|
|
162
|
+
for filename in filenames:
|
|
163
|
+
if filesystem_path:
|
|
164
|
+
this_filesystem_path = os.path.join(filesystem_path, filename)
|
|
165
|
+
else:
|
|
166
|
+
this_filesystem_path = self.files[filename]
|
|
167
|
+
|
|
168
|
+
is_dir = os.path.isdir(this_filesystem_path)
|
|
169
|
+
|
|
170
|
+
if is_dir:
|
|
171
|
+
if add_trailing_slash:
|
|
172
|
+
dirs.append(
|
|
173
|
+
{"link": os.path.join(f"/{path}", filename, ""), "basename": filename}
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
dirs.append(
|
|
177
|
+
{"link": os.path.join(f"/{path}", filename), "basename": filename}
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
size = os.path.getsize(this_filesystem_path)
|
|
181
|
+
size_human = self.common.human_readable_filesize(size)
|
|
182
|
+
files.append(
|
|
183
|
+
{
|
|
184
|
+
"link": os.path.join(f"/{path}", filename),
|
|
185
|
+
"basename": filename,
|
|
186
|
+
"size_human": size_human,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return files, dirs
|
|
191
|
+
|
|
192
|
+
def stream_individual_file(self, filesystem_path):
|
|
193
|
+
"""
|
|
194
|
+
Return a flask response that's streaming the download of an individual file, and gzip
|
|
195
|
+
compressing it if the browser supports it.
|
|
196
|
+
"""
|
|
197
|
+
use_gzip = self.should_use_gzip()
|
|
198
|
+
|
|
199
|
+
# gzip compress the individual file, if it hasn't already been compressed
|
|
200
|
+
if use_gzip:
|
|
201
|
+
if filesystem_path not in self.gzip_individual_files:
|
|
202
|
+
gzip_filename = os.path.join(
|
|
203
|
+
self.gzip_tmp_dir.name, str(self.gzip_counter)
|
|
204
|
+
)
|
|
205
|
+
self.gzip_counter += 1
|
|
206
|
+
self._gzip_compress(filesystem_path, gzip_filename, 6, None)
|
|
207
|
+
self.gzip_individual_files[filesystem_path] = gzip_filename
|
|
208
|
+
|
|
209
|
+
file_to_download = self.gzip_individual_files[filesystem_path]
|
|
210
|
+
filesize = os.path.getsize(self.gzip_individual_files[filesystem_path])
|
|
211
|
+
else:
|
|
212
|
+
file_to_download = filesystem_path
|
|
213
|
+
filesize = os.path.getsize(filesystem_path)
|
|
214
|
+
|
|
215
|
+
path = request.path
|
|
216
|
+
|
|
217
|
+
# Tell GUI the individual file started
|
|
218
|
+
history_id = self.cur_history_id
|
|
219
|
+
self.cur_history_id += 1
|
|
220
|
+
|
|
221
|
+
self.web.add_request(
|
|
222
|
+
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
|
|
223
|
+
path,
|
|
224
|
+
{"id": history_id, "filesize": filesize},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def generate():
|
|
228
|
+
chunk_size = 102400 # 100kb
|
|
229
|
+
|
|
230
|
+
fp = open(file_to_download, "rb")
|
|
231
|
+
done = False
|
|
232
|
+
while not done:
|
|
233
|
+
chunk = fp.read(chunk_size)
|
|
234
|
+
if chunk == b"":
|
|
235
|
+
done = True
|
|
236
|
+
else:
|
|
237
|
+
try:
|
|
238
|
+
yield chunk
|
|
239
|
+
|
|
240
|
+
# Tell GUI the progress
|
|
241
|
+
downloaded_bytes = fp.tell()
|
|
242
|
+
percent = (1.0 * downloaded_bytes / filesize) * 100
|
|
243
|
+
if (
|
|
244
|
+
not self.web.is_gui
|
|
245
|
+
or self.common.platform == "Linux"
|
|
246
|
+
or self.common.platform == "BSD"
|
|
247
|
+
):
|
|
248
|
+
sys.stdout.write(
|
|
249
|
+
"\r{0:s}, {1:.2f}% ".format(
|
|
250
|
+
self.common.human_readable_filesize(
|
|
251
|
+
downloaded_bytes
|
|
252
|
+
),
|
|
253
|
+
percent,
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
sys.stdout.flush()
|
|
257
|
+
|
|
258
|
+
self.web.add_request(
|
|
259
|
+
self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS,
|
|
260
|
+
path,
|
|
261
|
+
{
|
|
262
|
+
"id": history_id,
|
|
263
|
+
"bytes": downloaded_bytes,
|
|
264
|
+
"filesize": filesize,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
done = False
|
|
268
|
+
except Exception:
|
|
269
|
+
# Looks like the download was canceled
|
|
270
|
+
done = True
|
|
271
|
+
|
|
272
|
+
# Tell the GUI the individual file was canceled
|
|
273
|
+
self.web.add_request(
|
|
274
|
+
self.web.REQUEST_INDIVIDUAL_FILE_CANCELED,
|
|
275
|
+
path,
|
|
276
|
+
{"id": history_id},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
fp.close()
|
|
280
|
+
|
|
281
|
+
if self.common.platform != "Darwin":
|
|
282
|
+
sys.stdout.write("\n")
|
|
283
|
+
|
|
284
|
+
basename = os.path.basename(filesystem_path)
|
|
285
|
+
|
|
286
|
+
r = Response(generate())
|
|
287
|
+
if use_gzip:
|
|
288
|
+
r.headers.set("Content-Encoding", "gzip")
|
|
289
|
+
r.headers.set("Content-Length", filesize)
|
|
290
|
+
filename_dict = {
|
|
291
|
+
"filename": unidecode(basename),
|
|
292
|
+
"filename*": "UTF-8''%s" % quote(basename),
|
|
293
|
+
}
|
|
294
|
+
r.headers.set("Content-Disposition", "inline", **filename_dict)
|
|
295
|
+
(content_type, _) = mimetypes.guess_type(basename, strict=False)
|
|
296
|
+
if content_type is not None:
|
|
297
|
+
r.headers.set("Content-Type", content_type)
|
|
298
|
+
return r
|
|
299
|
+
|
|
300
|
+
def should_use_gzip(self):
|
|
301
|
+
"""
|
|
302
|
+
Should we use gzip for this browser?
|
|
303
|
+
"""
|
|
304
|
+
return (not self.is_zipped) and (
|
|
305
|
+
"gzip" in request.headers.get("Accept-Encoding", "").lower()
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _gzip_compress(
|
|
309
|
+
self, input_filename, output_filename, level, processed_size_callback=None
|
|
310
|
+
):
|
|
311
|
+
"""
|
|
312
|
+
Compress a file with gzip, without loading the whole thing into memory
|
|
313
|
+
Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
|
|
314
|
+
"""
|
|
315
|
+
bytes_processed = 0
|
|
316
|
+
blocksize = 1 << 16 # 64kB
|
|
317
|
+
with open(input_filename, "rb") as input_file:
|
|
318
|
+
output_file = gzip.open(output_filename, "wb", level)
|
|
319
|
+
while True:
|
|
320
|
+
if processed_size_callback is not None:
|
|
321
|
+
processed_size_callback(bytes_processed)
|
|
322
|
+
|
|
323
|
+
block = input_file.read(blocksize)
|
|
324
|
+
if len(block) == 0:
|
|
325
|
+
break
|
|
326
|
+
output_file.write(block)
|
|
327
|
+
bytes_processed += blocksize
|
|
328
|
+
|
|
329
|
+
output_file.close()
|
|
330
|
+
|
|
331
|
+
def init(self):
|
|
332
|
+
"""
|
|
333
|
+
Inherited class will implement this
|
|
334
|
+
"""
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
def define_routes(self):
|
|
338
|
+
"""
|
|
339
|
+
Inherited class will implement this
|
|
340
|
+
"""
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
def directory_listing_template(self):
|
|
344
|
+
"""
|
|
345
|
+
Inherited class will implement this. It should call render_template and return
|
|
346
|
+
the response.
|
|
347
|
+
"""
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
def set_file_info_custom(self, filenames, processed_size_callback):
|
|
351
|
+
"""
|
|
352
|
+
Inherited class will implement this.
|
|
353
|
+
"""
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
def render_logic(self, path=""):
|
|
357
|
+
"""
|
|
358
|
+
Inherited class will implement this.
|
|
359
|
+
"""
|
|
360
|
+
pass
|