pyservx 1.0.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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
pyservx/server.py ADDED
@@ -0,0 +1,640 @@
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 posixpath
7
+ import urllib.parse
8
+ import http.server
9
+ import socketserver
10
+ import shutil
11
+ import mimetypes
12
+ from io import BytesIO
13
+ import zipfile
14
+ import threading
15
+ import signal
16
+ import sys
17
+ import html
18
+ import logging
19
+ import socket
20
+ import json
21
+ import argparse
22
+
23
+ # Configure logging for debugging
24
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
25
+
26
+ PORT = 8088
27
+ CONFIG_FILE = os.path.expanduser("~/.pyservx_config.json") # Store config in user's home directory
28
+
29
+ def load_config():
30
+ """Load shared folder path from config file if it exists."""
31
+ if os.path.exists(CONFIG_FILE):
32
+ try:
33
+ with open(CONFIG_FILE, 'r') as f:
34
+ config = json.load(f)
35
+ return config.get("shared_folder")
36
+ except json.JSONDecodeError:
37
+ logging.warning("Invalid config file. Ignoring.")
38
+ return None
39
+
40
+ def save_config(folder_path):
41
+ """Save shared folder path to config file."""
42
+ try:
43
+ os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
44
+ with open(CONFIG_FILE, 'w') as f:
45
+ json.dump({"shared_folder": folder_path}, f)
46
+ except OSError as e:
47
+ logging.error(f"Failed to save config: {e}")
48
+
49
+ def get_shared_folder():
50
+ """Prompt user for shared folder path or load from config."""
51
+ saved_folder = load_config()
52
+ if saved_folder and os.path.isdir(saved_folder):
53
+ print(f"Using saved shared folder: {saved_folder}")
54
+ return os.path.abspath(saved_folder)
55
+
56
+ while True:
57
+ folder_path = input("Enter the path to the shared folder: ").strip()
58
+ if os.path.isdir(folder_path):
59
+ break
60
+ print("Invalid folder path. Please enter a valid directory.")
61
+
62
+ while True:
63
+ persist = input("Do you want this choice to be persistent? (y/n): ").strip().lower()
64
+ if persist in ('y', 'n'):
65
+ break
66
+ print("Please enter 'y' or 'n'.")
67
+
68
+ folder_path = os.path.abspath(folder_path)
69
+ if persist == 'y':
70
+ save_config(folder_path)
71
+ print(f"Shared folder saved for future use: {folder_path}")
72
+ else:
73
+ print("Shared folder will be prompted again next time.")
74
+
75
+ return folder_path
76
+
77
+ def zip_folder(folder_path):
78
+ memory_file = BytesIO()
79
+ with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
80
+ for root, _, files in os.walk(folder_path):
81
+ for file in files:
82
+ abs_path = os.path.join(root, file)
83
+ rel_path = os.path.relpath(abs_path, folder_path)
84
+ zipf.write(abs_path, rel_path)
85
+ memory_file.seek(0)
86
+ return memory_file
87
+
88
+ class FileRequestHandler(http.server.SimpleHTTPRequestHandler):
89
+ def translate_path(self, path):
90
+ # Prevent path traversal attacks
91
+ path = posixpath.normpath(urllib.parse.unquote(path))
92
+ rel_path = path.lstrip('/')
93
+ abs_path = os.path.abspath(os.path.join(self.server.base_dir, rel_path))
94
+ if not abs_path.startswith(self.server.base_dir):
95
+ logging.warning(f"Path traversal attempt detected: {path}")
96
+ return self.server.base_dir # Prevent access outside the base directory
97
+ return abs_path
98
+
99
+ def do_GET(self):
100
+ if self.path.endswith('/download_folder'):
101
+ folder_path = self.translate_path(self.path.replace('/download_folder', ''))
102
+ if os.path.isdir(folder_path):
103
+ zip_file = zip_folder(folder_path)
104
+ self.send_response(200)
105
+ self.send_header("Content-Type", "application/zip")
106
+ self.send_header("Content-Disposition", f"attachment; filename={os.path.basename(folder_path)}.zip")
107
+ self.end_headers()
108
+ shutil.copyfileobj(zip_file, self.wfile)
109
+ else:
110
+ self.send_error(404, "Folder not found")
111
+ return
112
+
113
+ if os.path.isdir(self.translate_path(self.path)):
114
+ self.list_directory(self.translate_path(self.path))
115
+ else:
116
+ super().do_GET()
117
+
118
+ def do_POST(self):
119
+ if self.path.endswith('/upload'):
120
+ content_length = int(self.headers.get('Content-Length', 0))
121
+ # Limit file size to prevent abuse (e.g., 100MB)
122
+ max_file_size = 100 * 1024 * 1024
123
+ if content_length > max_file_size:
124
+ self.send_error(413, "File too large")
125
+ return
126
+
127
+ # Parse multipart form data
128
+ content_type = self.headers.get('Content-Type', '')
129
+ if not content_type.startswith('multipart/form-data'):
130
+ self.send_error(400, "Invalid content type")
131
+ return
132
+
133
+ boundary = content_type.split('boundary=')[1].encode()
134
+ body = self.rfile.read(content_length)
135
+
136
+ # Simple parsing of multipart form data
137
+ parts = body.split(b'--' + boundary)
138
+ for part in parts:
139
+ if b'filename="' in part:
140
+ # Extract filename
141
+ start = part.find(b'filename="') + 10
142
+ end = part.find(b'"', start)
143
+ filename = part[start:end].decode('utf-8')
144
+ # Sanitize filename
145
+ filename = os.path.basename(filename)
146
+ if not filename:
147
+ continue
148
+
149
+ # Extract file content
150
+ content_start = part.find(b'\r\n\r\n') + 4
151
+ content_end = part.rfind(b'\r\n--' + boundary)
152
+ if content_end == -1:
153
+ content_end = len(part) - 2
154
+ file_content = part[content_start:content_end]
155
+
156
+ # Save file to the target directory
157
+ target_dir = self.translate_path(self.path.replace('/upload', ''))
158
+ if not os.path.isdir(target_dir):
159
+ self.send_error(404, "Target directory not found")
160
+ return
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 IPv4 addresses of the system."""
571
+ ip_addresses = []
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 loopback (127.x.x.x) and link-local (169.254.x.x)
577
+ if not ip.startswith("127.") and not ip.startswith("169.254."):
578
+ ip_addresses.append(ip)
579
+ return ip_addresses if ip_addresses else ["No IPv4 addresses found"]
580
+ except socket.gaierror:
581
+ return ["Unable to resolve hostname"]
582
+
583
+ def run(base_dir):
584
+ """Run the HTTP server with the specified base directory."""
585
+ # Set up the server with the base directory
586
+ class Handler(FileRequestHandler):
587
+ def __init__(self, *args, **kwargs):
588
+ self.server.base_dir = base_dir
589
+ super().__init__(*args, **kwargs)
590
+
591
+ # Print IP addresses before starting the server
592
+ print("System IPv4 addresses:")
593
+ for ip in get_ip_addresses():
594
+ print(f" http://{ip}:{PORT}")
595
+
596
+ server = None
597
+
598
+ try:
599
+ server = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), Handler)
600
+ print(f"Serving at http://0.0.0.0:{PORT} (accessible from network)")
601
+
602
+ def shutdown_handler(signum, frame):
603
+ print("\nShutting down server...")
604
+ if server:
605
+ # Run shutdown in a separate thread to avoid blocking
606
+ threading.Thread(target=server.shutdown, daemon=True).start()
607
+ server.server_close()
608
+ sys.exit(0)
609
+
610
+ # Register signal handler for SIGINT (Ctrl+C)
611
+ signal.signal(signal.SIGINT, shutdown_handler)
612
+
613
+ # Start the server
614
+ server.serve_forever()
615
+
616
+ except KeyboardInterrupt:
617
+ # Handle Ctrl+C explicitly to ensure clean shutdown
618
+ if server:
619
+ print("\nShutting down server...")
620
+ server.shutdown()
621
+ server.server_close()
622
+ sys.exit(0)
623
+ except Exception as e:
624
+ print(f"Server error: {e}")
625
+ if server:
626
+ server.server_close()
627
+ sys.exit(1)
628
+
629
+ def main():
630
+ """Main entry point for the command-line tool."""
631
+ parser = argparse.ArgumentParser(description="PyServeX: A simple HTTP server for file sharing.")
632
+ parser.add_argument('--version', action='version', version='PyServeX 1.0.0')
633
+ args = parser.parse_args()
634
+
635
+ # Get the shared folder
636
+ base_dir = get_shared_folder()
637
+ run(base_dir)
638
+
639
+ if __name__ == "__main__":
640
+ main()
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyservx
3
+ Version: 1.0.0
4
+ Summary: A simple HTTP server for file sharing with folder zipping and file upload capabilities
5
+ Author-email: SubZ3r0x01 <your.email@example.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 SubZ3r0x01
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ License-File: LICENSE
16
+ Requires-Python: >=3.6
@@ -0,0 +1,7 @@
1
+ pyservx/__init__.py,sha256=Aj77VL1d5Mdku7sgCgKQmPuYavPpAHuZuJcy6bygQZE,21
2
+ pyservx/server.py,sha256=4SnNXHazlkRfXybqbwT7RZzXhBkd8d7ZY_FOlmJlPRw,21910
3
+ pyservx-1.0.0.dist-info/METADATA,sha256=Kk2bXyOOtftvrYVrA6Yoadr0Xgeon9wJG5ez0-O5h-8,1382
4
+ pyservx-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ pyservx-1.0.0.dist-info/entry_points.txt,sha256=tRdMhIAJUa2bpIJ5h9ZkKPdSSp8fnYlPDGcJhdh01TY,48
6
+ pyservx-1.0.0.dist-info/licenses/LICENSE,sha256=RQfbbgFORXVcSaR2_p6XSOR7k0yw1iGfpnW7mg1NW9c,1074
7
+ pyservx-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyservx = pyservx.server:main
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SubZ3r0x01
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.