pyservx 1.0.3__py3-none-any.whl → 1.1.0__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.
Potentially problematic release.
This version of pyservx might be problematic. Click here for more details.
- pyservx/file_operations.py +52 -0
- pyservx/html_generator.py +443 -0
- pyservx/request_handler.py +146 -0
- pyservx/server.py +160 -639
- pyservx-1.1.0.dist-info/METADATA +74 -0
- pyservx-1.1.0.dist-info/RECORD +11 -0
- pyservx-1.1.0.dist-info/entry_points.txt +2 -0
- {pyservx-1.0.3.dist-info → pyservx-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {pyservx-1.0.3.dist-info → pyservx-1.1.0.dist-info}/top_level.txt +0 -0
- pyservx-1.0.3.dist-info/METADATA +0 -72
- pyservx-1.0.3.dist-info/RECORD +0 -7
- {pyservx-1.0.3.dist-info → pyservx-1.1.0.dist-info}/WHEEL +0 -0
pyservx/server.py
CHANGED
|
@@ -1,639 +1,160 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# Improved Python HTTP Server Developed by Subz3r0x01
|
|
3
|
-
# GitHub: https://github.com/SubZ3r0-0x01
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
while True:
|
|
57
|
-
|
|
58
|
-
if
|
|
59
|
-
break
|
|
60
|
-
print("
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
file_path = os.path.join(target_dir, filename)
|
|
163
|
-
try:
|
|
164
|
-
with open(file_path, 'wb') as f:
|
|
165
|
-
f.write(file_content)
|
|
166
|
-
except OSError:
|
|
167
|
-
self.send_error(500, "Error saving file")
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
# Log the upload and redirect URL
|
|
171
|
-
redirect_url = self.path.replace('/upload', '') or '/'
|
|
172
|
-
logging.info(f"File uploaded: {filename} to {target_dir}")
|
|
173
|
-
logging.info(f"Redirecting to: {redirect_url}")
|
|
174
|
-
|
|
175
|
-
# Serve success page with redirect
|
|
176
|
-
html_content = f'''<!DOCTYPE html>
|
|
177
|
-
<html lang="en">
|
|
178
|
-
<head>
|
|
179
|
-
<meta charset="UTF-8" />
|
|
180
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
181
|
-
<title>PyServeX - Upload Success</title>
|
|
182
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
183
|
-
<style>
|
|
184
|
-
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
|
185
|
-
|
|
186
|
-
body {{
|
|
187
|
-
font-family: 'VT323', monospace;
|
|
188
|
-
background: #000000;
|
|
189
|
-
min-height: 100vh;
|
|
190
|
-
margin: 0;
|
|
191
|
-
overflow-x: hidden;
|
|
192
|
-
}}
|
|
193
|
-
|
|
194
|
-
.text-neon {{
|
|
195
|
-
color: #00ff00;
|
|
196
|
-
}}
|
|
197
|
-
|
|
198
|
-
.typewriter h1 {{
|
|
199
|
-
overflow: hidden;
|
|
200
|
-
white-space: nowrap;
|
|
201
|
-
animation: typing 3s steps(40, end), blink-caret 0.5s step-end infinite;
|
|
202
|
-
margin: 0 auto;
|
|
203
|
-
text-align: center;
|
|
204
|
-
}}
|
|
205
|
-
|
|
206
|
-
@keyframes typing {{
|
|
207
|
-
from {{ width: 0; }}
|
|
208
|
-
to {{ width: 100%; }}
|
|
209
|
-
}}
|
|
210
|
-
|
|
211
|
-
@keyframes blink-caret {{
|
|
212
|
-
from, to {{ border-right: 2px solid #00ff00; }}
|
|
213
|
-
50% {{ border-right: 2px solid transparent; }}
|
|
214
|
-
}}
|
|
215
|
-
|
|
216
|
-
.glitch {{
|
|
217
|
-
position: relative.
|
|
218
|
-
animation: glitch 2s infinite;
|
|
219
|
-
}}
|
|
220
|
-
|
|
221
|
-
@keyframes glitch {{
|
|
222
|
-
0% {{ transform: translate(0); }}
|
|
223
|
-
10% {{ transform: translate(-2px, 2px); }}
|
|
224
|
-
20% {{ transform: translate(2px, -2px); }}
|
|
225
|
-
30% {{ transform: translate(-2px, 2px); }}
|
|
226
|
-
40% {{ transform: translate(0); }}
|
|
227
|
-
100% {{ transform: translate(0); }}
|
|
228
|
-
}}
|
|
229
|
-
|
|
230
|
-
.scanline {{
|
|
231
|
-
position: absolute;
|
|
232
|
-
top: 0;
|
|
233
|
-
left: 0;
|
|
234
|
-
width: 100%;
|
|
235
|
-
height: 100%;
|
|
236
|
-
background: linear-gradient(
|
|
237
|
-
to bottom,
|
|
238
|
-
rgba(255, 255, 255, 0),
|
|
239
|
-
rgba(255, 255, 255, 0.1) 50%,
|
|
240
|
-
rgba(255, 255, 255, 0)
|
|
241
|
-
);
|
|
242
|
-
animation: scan 4s linear infinite;
|
|
243
|
-
pointer-events: none;
|
|
244
|
-
}}
|
|
245
|
-
|
|
246
|
-
@keyframes scan {{
|
|
247
|
-
0% {{ transform: translateY(-100%); }}
|
|
248
|
-
100% {{ transform: translateY(100%); }}
|
|
249
|
-
}}
|
|
250
|
-
|
|
251
|
-
.particle {{
|
|
252
|
-
position: absolute;
|
|
253
|
-
width: 3px;
|
|
254
|
-
height: 3px;
|
|
255
|
-
background: #00ff00;
|
|
256
|
-
opacity: 0.5;
|
|
257
|
-
animation: flicker 3s infinite;
|
|
258
|
-
}}
|
|
259
|
-
|
|
260
|
-
@keyframes flicker {{
|
|
261
|
-
0% {{ opacity: 0.5; }}
|
|
262
|
-
50% {{ opacity: 0.1; }}
|
|
263
|
-
100% {{ opacity: 0.5; }}
|
|
264
|
-
}}
|
|
265
|
-
|
|
266
|
-
main {{
|
|
267
|
-
margin-top: 100px;
|
|
268
|
-
padding: 2rem;
|
|
269
|
-
color: #00ff00;
|
|
270
|
-
text-align: center;
|
|
271
|
-
max-width: 900px;
|
|
272
|
-
margin-left: auto;
|
|
273
|
-
margin-right: auto;
|
|
274
|
-
}}
|
|
275
|
-
</style>
|
|
276
|
-
</head>
|
|
277
|
-
<body>
|
|
278
|
-
<div class="scanline"></div>
|
|
279
|
-
<main>
|
|
280
|
-
<h1 class="text-4xl md:text-6xl text-neon typewriter glitch">File Uploaded Successfully!</h1>
|
|
281
|
-
<p class="text-neon text-2xl mt-4">Redirecting to directory in 3 seconds...</p>
|
|
282
|
-
</main>
|
|
283
|
-
<script>
|
|
284
|
-
// Generate random particles for hacker effect
|
|
285
|
-
function createParticles() {{
|
|
286
|
-
const numParticles = 30;
|
|
287
|
-
for (let i = 0; i < numParticles; i++) {{
|
|
288
|
-
const particle = document.createElement('div');
|
|
289
|
-
particle.classList.add('particle');
|
|
290
|
-
particle.style.left = `${{Math.random() * 100}}vw`;
|
|
291
|
-
particle.style.top = `${{Math.random() * 100}}vh`;
|
|
292
|
-
particle.style.animationDelay = `${{Math.random() * 3}}s`;
|
|
293
|
-
document.body.appendChild(particle);
|
|
294
|
-
}}
|
|
295
|
-
}}
|
|
296
|
-
|
|
297
|
-
// Auto-redirect after 3 seconds
|
|
298
|
-
setTimeout(() => {{
|
|
299
|
-
window.location.href = "{html.escape(redirect_url)}";
|
|
300
|
-
}}, 3000);
|
|
301
|
-
|
|
302
|
-
window.onload = createParticles;
|
|
303
|
-
</script>
|
|
304
|
-
</body>
|
|
305
|
-
</html>
|
|
306
|
-
'''
|
|
307
|
-
encoded = html_content.encode('utf-8', 'surrogateescape')
|
|
308
|
-
self.send_response(200)
|
|
309
|
-
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
310
|
-
self.send_header("Content-Length", str(len(encoded)))
|
|
311
|
-
self.end_headers()
|
|
312
|
-
self.wfile.write(encoded)
|
|
313
|
-
return
|
|
314
|
-
self.send_error(400, "No file provided")
|
|
315
|
-
return
|
|
316
|
-
else:
|
|
317
|
-
self.send_error(405, "Method not allowed")
|
|
318
|
-
|
|
319
|
-
def list_directory(self, path):
|
|
320
|
-
try:
|
|
321
|
-
entries = os.listdir(path)
|
|
322
|
-
except OSError:
|
|
323
|
-
self.send_error(404, "Cannot list directory")
|
|
324
|
-
return None
|
|
325
|
-
|
|
326
|
-
entries.sort(key=lambda a: a.lower())
|
|
327
|
-
displaypath = html.escape(urllib.parse.unquote(self.path))
|
|
328
|
-
|
|
329
|
-
# Build list items for directories and files
|
|
330
|
-
list_items = []
|
|
331
|
-
# Parent directory link if not root
|
|
332
|
-
if self.path != '/':
|
|
333
|
-
parent = os.path.dirname(self.path.rstrip('/'))
|
|
334
|
-
if not parent.endswith('/'):
|
|
335
|
-
parent += '/'
|
|
336
|
-
list_items.append(f'<li><a href="{html.escape(parent)}" class="text-neon">.. (Parent Directory)</a></li>')
|
|
337
|
-
|
|
338
|
-
for name in entries:
|
|
339
|
-
fullpath = os.path.join(path, name)
|
|
340
|
-
displayname = name + '/' if os.path.isdir(fullpath) else name
|
|
341
|
-
href = urllib.parse.quote(name)
|
|
342
|
-
if os.path.isdir(fullpath):
|
|
343
|
-
href += '/'
|
|
344
|
-
# Add download folder zip link for directories
|
|
345
|
-
if os.path.isdir(fullpath):
|
|
346
|
-
list_items.append(
|
|
347
|
-
f'<li>'
|
|
348
|
-
f'<a href="{href}" class="text-neon">{html.escape(displayname)}</a> '
|
|
349
|
-
f' | <a href="{href}download_folder" class="text-neon">📦 Zip Download</a>'
|
|
350
|
-
f'</li>'
|
|
351
|
-
)
|
|
352
|
-
else:
|
|
353
|
-
list_items.append(f'<li><a href="{href}" class="text-neon">{html.escape(displayname)}</a></li>')
|
|
354
|
-
|
|
355
|
-
list_html = '\n'.join(list_items)
|
|
356
|
-
|
|
357
|
-
html_content = f'''<!DOCTYPE html>
|
|
358
|
-
<html lang="en">
|
|
359
|
-
<head>
|
|
360
|
-
<meta charset="UTF-8" />
|
|
361
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
362
|
-
<title>PyServeX - Index of {displaypath}</title>
|
|
363
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
364
|
-
<style>
|
|
365
|
-
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
|
366
|
-
|
|
367
|
-
body {{
|
|
368
|
-
font-family: 'VT323', monospace;
|
|
369
|
-
background: #000000;
|
|
370
|
-
min-height: 100vh;
|
|
371
|
-
margin: 0;
|
|
372
|
-
overflow-x: hidden;
|
|
373
|
-
}}
|
|
374
|
-
|
|
375
|
-
header {{
|
|
376
|
-
position: fixed;
|
|
377
|
-
top: 0;
|
|
378
|
-
left: 0;
|
|
379
|
-
width: 100%;
|
|
380
|
-
padding: 1rem 0;
|
|
381
|
-
background: rgba(0, 0,0, 0.9);
|
|
382
|
-
box-shadow: 0 2px 5px rgba(0, 255, 0, 0.2);
|
|
383
|
-
z-index: 1000;
|
|
384
|
-
}}
|
|
385
|
-
|
|
386
|
-
.text-neon {{
|
|
387
|
-
color: #00ff00;
|
|
388
|
-
}}
|
|
389
|
-
|
|
390
|
-
.typewriter h1 {{
|
|
391
|
-
overflow: hidden;
|
|
392
|
-
white-space: nowrap;
|
|
393
|
-
animation: typing 3s steps(40, end), blink-caret 0.5s step-end infinite;
|
|
394
|
-
margin: 0 auto;
|
|
395
|
-
text-align: center;
|
|
396
|
-
}}
|
|
397
|
-
|
|
398
|
-
@keyframes typing {{
|
|
399
|
-
from {{ width: 0; }}
|
|
400
|
-
to {{ width: 100%; }}
|
|
401
|
-
}}
|
|
402
|
-
|
|
403
|
-
@keyframes blink-caret {{
|
|
404
|
-
from, to {{ border-right: 2px solid #00ff00; }}
|
|
405
|
-
50% {{ border-right: 2px solid transparent; }}
|
|
406
|
-
}}
|
|
407
|
-
|
|
408
|
-
.glitch {{
|
|
409
|
-
position: relative;
|
|
410
|
-
animation: glitch 2s infinite;
|
|
411
|
-
}}
|
|
412
|
-
|
|
413
|
-
@keyframes glitch {{
|
|
414
|
-
0% {{ transform: translate(0); }}
|
|
415
|
-
10% {{ transform: translate(-2px, 2px); }}
|
|
416
|
-
20% {{ transform: translate(2px, -2px); }}
|
|
417
|
-
30% {{ transform: translate(-2px, 2px); }}
|
|
418
|
-
40% {{ transform: translate(0); }}
|
|
419
|
-
100% {{ transform: translate(0); }}
|
|
420
|
-
}}
|
|
421
|
-
|
|
422
|
-
.scanline {{
|
|
423
|
-
position: absolute;
|
|
424
|
-
top: 0;
|
|
425
|
-
left: 0;
|
|
426
|
-
width: 100%;
|
|
427
|
-
height: 100%;
|
|
428
|
-
background: linear-gradient(
|
|
429
|
-
to bottom,
|
|
430
|
-
rgba(255, 255, 255, 0),
|
|
431
|
-
rgba(255, 255, 255, 0.1) 50%,
|
|
432
|
-
rgba(255, 255, 255, 0)
|
|
433
|
-
);
|
|
434
|
-
animation: scan 4s linear infinite;
|
|
435
|
-
pointer-events: none;
|
|
436
|
-
}}
|
|
437
|
-
|
|
438
|
-
@keyframes scan {{
|
|
439
|
-
0% {{ transform: translateY(-100%); }}
|
|
440
|
-
100% {{ transform: translateY(100%); }}
|
|
441
|
-
}}
|
|
442
|
-
|
|
443
|
-
.particle {{
|
|
444
|
-
position: absolute;
|
|
445
|
-
width: 3px;
|
|
446
|
-
height: 3px;
|
|
447
|
-
background: #00ff00;
|
|
448
|
-
opacity: 0.5;
|
|
449
|
-
animation: flicker 3s infinite;
|
|
450
|
-
}}
|
|
451
|
-
|
|
452
|
-
@keyframes flicker {{
|
|
453
|
-
0% {{ opacity: 0.5; }}
|
|
454
|
-
50% {{ opacity: 0.1; }}
|
|
455
|
-
100% {{ opacity: 0.5; }}
|
|
456
|
-
}}
|
|
457
|
-
|
|
458
|
-
main {{
|
|
459
|
-
margin-top: 100px; /* Adjust based on header height */
|
|
460
|
-
padding: 2rem;
|
|
461
|
-
color: #00ff00;
|
|
462
|
-
text-align: left;
|
|
463
|
-
max-width: 900px;
|
|
464
|
-
margin-left: auto;
|
|
465
|
-
margin-right: auto;
|
|
466
|
-
}}
|
|
467
|
-
|
|
468
|
-
ul {{
|
|
469
|
-
list-style-type: none;
|
|
470
|
-
padding-left: 0;
|
|
471
|
-
}}
|
|
472
|
-
|
|
473
|
-
li {{
|
|
474
|
-
margin-bottom: 0.7rem;
|
|
475
|
-
font-size: 1.2rem;
|
|
476
|
-
}}
|
|
477
|
-
|
|
478
|
-
a {{
|
|
479
|
-
text-decoration: none;
|
|
480
|
-
}}
|
|
481
|
-
|
|
482
|
-
a:hover {{
|
|
483
|
-
text-decoration: underline;
|
|
484
|
-
}}
|
|
485
|
-
|
|
486
|
-
.upload-form {{
|
|
487
|
-
margin-top: 1.5rem;
|
|
488
|
-
padding: 1rem;
|
|
489
|
-
border: 1px solid #00ff00;
|
|
490
|
-
border-radius: 5px;
|
|
491
|
-
}}
|
|
492
|
-
|
|
493
|
-
.upload-form label {{
|
|
494
|
-
display: block;
|
|
495
|
-
margin-bottom: 0.5rem;
|
|
496
|
-
}}
|
|
497
|
-
|
|
498
|
-
.upload-form input[type="file"] {{
|
|
499
|
-
color: #00ff00;
|
|
500
|
-
background: #000000;
|
|
501
|
-
border: 1px solid #00ff00;
|
|
502
|
-
padding: 0.5rem;
|
|
503
|
-
}}
|
|
504
|
-
|
|
505
|
-
.upload-form button {{
|
|
506
|
-
background: #00ff00;
|
|
507
|
-
color: #000000;
|
|
508
|
-
padding: 0.5rem 1rem;
|
|
509
|
-
border: none;
|
|
510
|
-
cursor: pointer;
|
|
511
|
-
font-family: 'VT323', monospace;
|
|
512
|
-
font-size: 1.2rem;
|
|
513
|
-
}}
|
|
514
|
-
|
|
515
|
-
.upload-form button:hover {{
|
|
516
|
-
background: #00cc00;
|
|
517
|
-
}}
|
|
518
|
-
</style>
|
|
519
|
-
</head>
|
|
520
|
-
<body>
|
|
521
|
-
<div class="scanline"></div>
|
|
522
|
-
<header>
|
|
523
|
-
<div class="text-center">
|
|
524
|
-
<h1 class="text-4xl md:text-6xl text-neon typewriter glitch">PyServeX</h1>
|
|
525
|
-
</div>
|
|
526
|
-
</header>
|
|
527
|
-
<main>
|
|
528
|
-
<h2>Index of {displaypath}</h2>
|
|
529
|
-
<ul>
|
|
530
|
-
{list_html}
|
|
531
|
-
</ul>
|
|
532
|
-
<div class="upload-form">
|
|
533
|
-
<form action="{html.escape(self.path)}upload" method="POST" enctype="multipart/form-data">
|
|
534
|
-
<label for="file-upload" class="text-neon">Upload a file:</label>
|
|
535
|
-
<input type="file" id="file-upload" name="file" />
|
|
536
|
-
<button type="submit">Upload</button>
|
|
537
|
-
</form>
|
|
538
|
-
</div>
|
|
539
|
-
</main>
|
|
540
|
-
|
|
541
|
-
<script>
|
|
542
|
-
// Generate random particles for hacker effect
|
|
543
|
-
function createParticles() {{
|
|
544
|
-
const numParticles = 30;
|
|
545
|
-
for (let i = 0; i < numParticles; i++) {{
|
|
546
|
-
const particle = document.createElement('div');
|
|
547
|
-
particle.classList.add('particle');
|
|
548
|
-
particle.style.left = `${{Math.random() * 100}}vw`;
|
|
549
|
-
particle.style.top = `${{Math.random() * 100}}vh`;
|
|
550
|
-
particle.style.animationDelay = `${{Math.random() * 3}}s`;
|
|
551
|
-
document.body.appendChild(particle);
|
|
552
|
-
}}
|
|
553
|
-
}}
|
|
554
|
-
|
|
555
|
-
window.onload = createParticles;
|
|
556
|
-
</script>
|
|
557
|
-
</body>
|
|
558
|
-
</html>
|
|
559
|
-
'''
|
|
560
|
-
|
|
561
|
-
encoded = html_content.encode('utf-8', 'surrogateescape')
|
|
562
|
-
self.send_response(200)
|
|
563
|
-
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
564
|
-
self.send_header("Content-Length", str(len(encoded)))
|
|
565
|
-
self.end_headers()
|
|
566
|
-
self.wfile.write(encoded)
|
|
567
|
-
return
|
|
568
|
-
|
|
569
|
-
def get_ip_addresses():
|
|
570
|
-
"""Retrieve all non-loopback and loopback IPv4 addresses of the system."""
|
|
571
|
-
ip_addresses = ["127.0.0.1"] # Explicitly include localhost
|
|
572
|
-
try:
|
|
573
|
-
# Get all network interfaces, filter for IPv4 (AF_INET)
|
|
574
|
-
for interface in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
|
575
|
-
ip = interface[4][0]
|
|
576
|
-
# Filter out link-local (169.254.x.x) but keep 127.x.x.x
|
|
577
|
-
if not ip.startswith("169.254.") and ip not in ip_addresses:
|
|
578
|
-
ip_addresses.append(ip)
|
|
579
|
-
return ip_addresses if ip_addresses else ["127.0.0.1", "No other IPv4 addresses found"]
|
|
580
|
-
except socket.gaierror:
|
|
581
|
-
return ["127.0.0.1", "Unable to resolve hostname"]
|
|
582
|
-
|
|
583
|
-
def run(base_dir):
|
|
584
|
-
"""Run the HTTP server with the specified base directory."""
|
|
585
|
-
class Handler(FileRequestHandler):
|
|
586
|
-
def __init__(self, *args, **kwargs):
|
|
587
|
-
self.base_dir = base_dir
|
|
588
|
-
super().__init__(*args, **kwargs)
|
|
589
|
-
|
|
590
|
-
# Print IP addresses before starting the server
|
|
591
|
-
print("System IPv4 addresses (including localhost):")
|
|
592
|
-
for ip in get_ip_addresses():
|
|
593
|
-
print(f" http://{ip}:{PORT}")
|
|
594
|
-
|
|
595
|
-
server = None
|
|
596
|
-
|
|
597
|
-
try:
|
|
598
|
-
server = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), Handler)
|
|
599
|
-
print(f"Serving at http://0.0.0.0:{PORT} (accessible from network and localhost)")
|
|
600
|
-
|
|
601
|
-
def shutdown_handler(signum, frame):
|
|
602
|
-
print("\nShutting down server...")
|
|
603
|
-
if server:
|
|
604
|
-
# Run shutdown in a separate thread to avoid blocking
|
|
605
|
-
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
606
|
-
server.server_close()
|
|
607
|
-
sys.exit(0)
|
|
608
|
-
|
|
609
|
-
# Register signal handler for SIGINT (Ctrl+C)
|
|
610
|
-
signal.signal(signal.SIGINT, shutdown_handler)
|
|
611
|
-
|
|
612
|
-
# Start the server
|
|
613
|
-
server.serve_forever()
|
|
614
|
-
|
|
615
|
-
except KeyboardInterrupt:
|
|
616
|
-
# Handle Ctrl+C explicitly to ensure clean shutdown
|
|
617
|
-
if server:
|
|
618
|
-
print("\nShutting down server...")
|
|
619
|
-
server.shutdown()
|
|
620
|
-
server.server_close()
|
|
621
|
-
sys.exit(0)
|
|
622
|
-
except Exception as e:
|
|
623
|
-
print(f"Server error: {e}")
|
|
624
|
-
if server:
|
|
625
|
-
server.server_close()
|
|
626
|
-
sys.exit(1)
|
|
627
|
-
|
|
628
|
-
def main():
|
|
629
|
-
"""Main entry point for the command-line tool."""
|
|
630
|
-
parser = argparse.ArgumentParser(description="PyServeX: A simple HTTP server for file sharing.")
|
|
631
|
-
parser.add_argument('--version', action='version', version='PyServeX 1.0.1')
|
|
632
|
-
args = parser.parse_args()
|
|
633
|
-
|
|
634
|
-
# Get the shared folder
|
|
635
|
-
base_dir = get_shared_folder()
|
|
636
|
-
run(base_dir)
|
|
637
|
-
|
|
638
|
-
if __name__ == "__main__":
|
|
639
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Improved Python HTTP Server Developed by Subz3r0x01
|
|
3
|
+
# GitHub: https://github.com/SubZ3r0-0x01
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import socketserver
|
|
7
|
+
import threading
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
import logging
|
|
11
|
+
import socket
|
|
12
|
+
import json
|
|
13
|
+
import argparse
|
|
14
|
+
import qrcode
|
|
15
|
+
from . import request_handler
|
|
16
|
+
|
|
17
|
+
# Configure logging for debugging
|
|
18
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
19
|
+
|
|
20
|
+
PORT = 8088
|
|
21
|
+
CONFIG_FILE = os.path.expanduser("~/.pyservx_config.json") # Store config in user's home directory
|
|
22
|
+
|
|
23
|
+
def load_config():
|
|
24
|
+
"""Load shared folder path from config file if it exists."""
|
|
25
|
+
if os.path.exists(CONFIG_FILE):
|
|
26
|
+
try:
|
|
27
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
28
|
+
config = json.load(f)
|
|
29
|
+
return config.get("shared_folder")
|
|
30
|
+
except json.JSONDecodeError:
|
|
31
|
+
logging.warning("Invalid config file. Ignoring.")
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def save_config(folder_path):
|
|
35
|
+
"""Save shared folder path to config file."""
|
|
36
|
+
try:
|
|
37
|
+
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
|
38
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
39
|
+
json.dump({"shared_folder": folder_path}, f)
|
|
40
|
+
except OSError as e:
|
|
41
|
+
logging.error(f"Failed to save config: {e}")
|
|
42
|
+
|
|
43
|
+
def get_shared_folder():
|
|
44
|
+
"""Prompt user for shared folder path or load from config."""
|
|
45
|
+
saved_folder = load_config()
|
|
46
|
+
if saved_folder and os.path.isdir(saved_folder):
|
|
47
|
+
print(f"Using saved shared folder: {saved_folder}")
|
|
48
|
+
return os.path.abspath(saved_folder)
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
folder_path = input("Enter the path to the shared folder: ").strip()
|
|
52
|
+
if os.path.isdir(folder_path):
|
|
53
|
+
break
|
|
54
|
+
print("Invalid folder path. Please enter a valid directory.")
|
|
55
|
+
|
|
56
|
+
while True:
|
|
57
|
+
persist = input("Do you want this choice to be persistent? (y/n): ").strip().lower()
|
|
58
|
+
if persist in ('y', 'n'):
|
|
59
|
+
break
|
|
60
|
+
print("Please enter 'y' or 'n'.")
|
|
61
|
+
|
|
62
|
+
folder_path = os.path.abspath(folder_path)
|
|
63
|
+
if persist == 'y':
|
|
64
|
+
save_config(folder_path)
|
|
65
|
+
print(f"Shared folder saved for future use: {folder_path}")
|
|
66
|
+
else:
|
|
67
|
+
print("Shared folder will be prompted again next time.")
|
|
68
|
+
|
|
69
|
+
return folder_path
|
|
70
|
+
|
|
71
|
+
def get_ip_addresses():
|
|
72
|
+
"""Retrieve all non-loopback and loopback IPv4 addresses of the system."""
|
|
73
|
+
ip_addresses = ["127.0.0.1"] # Explicitly include localhost
|
|
74
|
+
try:
|
|
75
|
+
# Get all network interfaces, filter for IPv4 (AF_INET)
|
|
76
|
+
for interface in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
|
77
|
+
ip = interface[4][0]
|
|
78
|
+
# Filter out link-local (169.254.x.x) but keep 127.x.x.x
|
|
79
|
+
if not ip.startswith("169.254.") and ip not in ip_addresses:
|
|
80
|
+
ip_addresses.append(ip)
|
|
81
|
+
return ip_addresses if ip_addresses else ["127.0.0.1", "No other IPv4 addresses found"]
|
|
82
|
+
except socket.gaierror:
|
|
83
|
+
return ["127.0.0.1", "Unable to resolve hostname"]
|
|
84
|
+
|
|
85
|
+
def run(base_dir, no_qr=False):
|
|
86
|
+
"""Run the HTTP server with the specified base directory."""
|
|
87
|
+
class Handler(request_handler.FileRequestHandler):
|
|
88
|
+
def __init__(self, *args, **kwargs):
|
|
89
|
+
self.base_dir = base_dir
|
|
90
|
+
super().__init__(*args, **kwargs)
|
|
91
|
+
|
|
92
|
+
# Create robots.txt if it doesn't exist
|
|
93
|
+
robots_txt_path = os.path.join(base_dir, "robots.txt")
|
|
94
|
+
if not os.path.exists(robots_txt_path):
|
|
95
|
+
with open(robots_txt_path, "w") as f:
|
|
96
|
+
f.write("User-agent: *\nDisallow: /\n")
|
|
97
|
+
|
|
98
|
+
if not no_qr:
|
|
99
|
+
# Print IP addresses before starting the server
|
|
100
|
+
print("System IPv4 addresses (including localhost):")
|
|
101
|
+
for ip in get_ip_addresses():
|
|
102
|
+
print(f" http://{ip}:{PORT}")
|
|
103
|
+
qr = qrcode.QRCode(
|
|
104
|
+
version=1,
|
|
105
|
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
106
|
+
box_size=10,
|
|
107
|
+
border=4,
|
|
108
|
+
)
|
|
109
|
+
qr.add_data(f"http://{ip}:{PORT}")
|
|
110
|
+
qr.make(fit=True)
|
|
111
|
+
try:
|
|
112
|
+
qr.print_tty()
|
|
113
|
+
except OSError:
|
|
114
|
+
print("Not a TTY. Cannot print QR code.")
|
|
115
|
+
|
|
116
|
+
server = None
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
server = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), Handler)
|
|
120
|
+
print(f"Serving at http://0.0.0.0:{PORT} (accessible from network and localhost)")
|
|
121
|
+
|
|
122
|
+
def shutdown_handler(signum, frame):
|
|
123
|
+
print("\nShutting down server...")
|
|
124
|
+
if server:
|
|
125
|
+
# Run shutdown in a separate thread to avoid blocking
|
|
126
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
127
|
+
server.server_close()
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
# Register signal handler for SIGINT (Ctrl+C)
|
|
131
|
+
signal.signal(signal.SIGINT, shutdown_handler)
|
|
132
|
+
|
|
133
|
+
# Start the server
|
|
134
|
+
server.serve_forever()
|
|
135
|
+
|
|
136
|
+
except KeyboardInterrupt:
|
|
137
|
+
# Handle Ctrl+C explicitly to ensure clean shutdown
|
|
138
|
+
if server:
|
|
139
|
+
print("\nShutting down server...")
|
|
140
|
+
server.shutdown()
|
|
141
|
+
server.server_close()
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"Server error: {e}")
|
|
145
|
+
if server:
|
|
146
|
+
server.server_close()
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
def main():
|
|
150
|
+
"""Main entry point for the command-line tool."""
|
|
151
|
+
parser = argparse.ArgumentParser(description="PyServeX: A simple HTTP server for file sharing.")
|
|
152
|
+
parser.add_argument('--version', action='version', version='PyServeX 1.0.1')
|
|
153
|
+
args = parser.parse_args()
|
|
154
|
+
|
|
155
|
+
# Get the shared folder
|
|
156
|
+
base_dir = get_shared_folder()
|
|
157
|
+
run(base_dir)
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
main()
|