Scripte & Zeug: Python Servefile Script
Das ist ein kleines Script, welches ich gebastelt habe um schnell ein paar Dateien im LAN zu verschicken. Startet man das Script, so öffnet es einen threaded Webserver auf Port 8080 und erlaubt es anderen diese Datei über HTTP herunterzuladen (der angegebene Pfad wird dabei ignoriert).
Download:
| Git (development version) | git clone git://git.someserver.de/git/servefile/ |
| Source-Tarball | servefile-0.4.2.tar.gz |
| Debian Repository | servefile |
| Gentoo | www-servers/servefile |
| Arch | AUR/servefile |
| Old tarballs |
servefile-0.4.1.tar.gz servefile-0.4.0.tar.gz servefile-0.3.2.tar.gz |
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Licensed under GNU General Public License v3 or later
5 # Written by Sebastian Lohff (seba@seba-geek.de)
6 # http://seba-geek.de/stuff/servefile/
7
8 from __future__ import print_function
9
10 __version__ = '0.4.2'
11
12 import argparse
13 import base64
14 import cgi
15 import datetime
16 import mimetypes
17 import urllib
18 import os
19 import posixpath
20 import re
21 import select
22 import socket
23 from subprocess import Popen, PIPE
24 import sys
25 import time
26
27 # fix imports for python2/python3
28 try:
29 import BaseHTTPServer
30 import SocketServer
31 except ImportError:
32 # both have different names in python3
33 import http.server as BaseHTTPServer
34 import socketserver as SocketServer
35
36 # only activate SSL if available
37 HAVE_SSL = False
38 try:
39 from OpenSSL import SSL, crypto
40 HAVE_SSL = True
41 except ImportError:
42 pass
43
44 def getDateStrNow():
45 """ Get the current time formatted for HTTP header """
46 now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
47 return now.strftime("%a, %d %b %Y %H:%M:%S GMT")
48
49 class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
50 fileName = None
51 blockSize = 1024 * 1024
52 server_version = "servefile/" + __version__
53
54 def checkAndDoRedirect(self, fileName=None):
55 """ If request didn't request self.fileName redirect to self.fileName.
56
57 Returns True if a redirect was issued. """
58 if not fileName:
59 fileName = self.fileName
60 if urllib.unquote(self.path) != "/" + fileName:
61 self.send_response(302)
62 self.send_header('Location', '/' + fileName)
63 self.end_headers()
64 return True
65 return False
66
67 def sendContentHeaders(self, fileName, fileLength, lastModified=None):
68 """ Send default Content headers for given fileName and fileLength.
69
70 If no lastModified is given the current date is taken. If
71 fileLength is lesser than 0 no Content-Length will be sent."""
72 if not lastModified:
73 lastModified = getDateStrNow()
74
75 if fileLength >= 0:
76 self.send_header('Content-Length', str(fileLength))
77 self.send_header('Last-Modified', lastModified)
78 self.send_header('Content-Type', 'application/octet-stream')
79 self.send_header('Content-Disposition', 'attachment; filename="%s"' % fileName)
80 self.send_header('Content-Transfer-Encoding', 'binary')
81
82 def isRangeRequest(self):
83 """ Return True if partial content is requestet """
84 return "Range" in self.headers
85
86 def handleRangeRequest(self, fileLength):
87 """ Find out and handle continuing downloads.
88
89 Returns a tuple of a boolean, if this is a valid range request,
90 and a range. When the requested range is out of range, range is
91 set to None.
92 """
93 fromto = None
94 if self.isRangeRequest():
95 cont = self.headers.get("Range").split("=")
96 if len(cont) > 1 and cont[0] == 'bytes':
97 fromto = cont[1].split('-')
98 if len(fromto) > 1:
99 if fromto[1] == '':
100 fromto[1] = fileLength - 1
101 try:
102 fromto[0] = int(fromto[0])
103 fromto[1] = int(fromto[1])
104 except:
105 return (False, None)
106
107 if fromto[0] >= fileLength or fromto[0] < 0 or fromto[1] >= fileLength or fromto[1]-fromto[0] < 0:
108 # oops, already done! (requested range out of range)
109 self.send_response(416)
110 self.send_header('Content-Range', 'bytes */%d' % fileLength)
111 self.end_headers()
112 return (True, None)
113 return (True, fromto)
114 # broken request or no range header
115 return (False, None)
116
117 def sendFile(self, filePath, fileLength=None, lastModified=None):
118 """ Send file with continuation support.
119
120 filePath: path to file to be sent
121 fileLength: length of file (if None is given this will be found out)
122 lastModified: time the file was last modified, None for "now"
123 """
124 if not fileLength:
125 fileLength = os.stat(filePath).st_size
126
127 (responseCode, myfile) = self.getFileHandle(filePath)
128 if not myfile:
129 self.send_response(responseCode)
130 self.end_headers()
131 return
132
133 (continueDownload, fromto) = self.handleRangeRequest(fileLength)
134 if continueDownload:
135 if not fromto:
136 # we are done
137 return True
138
139 # now we can wind the file *brrrrrr*
140 myfile.seek(fromto[0])
141
142 if fromto != None:
143 self.send_response(216)
144 self.send_header('Content-Range', 'bytes %d-%d/%d' % (fromto[0], fromto[1], fileLength))
145 fileLength = fromto[1] - fromto[0] + 1
146 else:
147 self.send_response(200)
148
149 fileName = self.fileName
150 if not fileName:
151 fileName = os.path.basename(filePath)
152 self.sendContentHeaders(fileName, fileLength, lastModified)
153 self.end_headers()
154 block = self.getChunk(myfile, fromto)
155 while block:
156 try:
157 self.wfile.write(block)
158 except socket.error as e:
159 print("%s ABORTED transmission (Reason %s: %s)" % (self.client_address[0], e[0], e[1]))
160 return False
161 block = self.getChunk(myfile, fromto)
162 myfile.close()
163 print("%s finished downloading %s" % (self.client_address[0], filePath))
164 return True
165
166 def getChunk(self, myfile, fromto):
167 if fromto and myfile.tell()+self.blockSize >= fromto[1]:
168 readsize = fromto[1]-myfile.tell()+1
169 else:
170 readsize = self.blockSize
171 return myfile.read(readsize)
172
173 def getFileHandle(self, path):
174 """ Get handle to a file.
175
176 Return a tuple of HTTP response code and file handle.
177 If the handle couldn't be acquired it is set to None
178 and an appropriate HTTP error code is returned.
179 """
180 myfile = None
181 responseCode = 200
182 try:
183 myfile = open(path, 'rb')
184 except IOError as e:
185 responseCode = self.getResponseForErrno(e.errno)
186 return (responseCode, myfile)
187
188 def getFileLength(self, path):
189 """ Get length of a file.
190
191 Return a tuple of HTTP response code and file length.
192 If filelength couldn't be determined, it is set to -1
193 and an appropriate HTTP error code is returned.
194 """
195 fileSize = -1
196 responseCode = 200
197 try:
198 fileSize = os.stat(path).st_size
199 except IOError as e:
200 responseCode = self.getResponseForErrno(e.errno)
201 return (responseCode, fileSize)
202
203 def getResponseForErrno(self, errno):
204 """ Return HTTP response code for an IOError errno """
205 if errno == errno.ENOENT:
206 return 404
207 elif errno == errno.EACCESS:
208 return 403
209 else:
210 return 500
211
212
213 class FileHandler(FileBaseHandler):
214 filePath = "/dev/null"
215 fileLength = 0
216 startTime = getDateStrNow()
217
218 def do_HEAD(self):
219 if self.checkAndDoRedirect():
220 return
221 self.send_response(200)
222 self.sendContentHeaders(self.fileName, self.fileLength, self.startTime)
223 self.end_headers()
224
225 def do_GET(self):
226 if self.checkAndDoRedirect():
227 return
228 self.sendFile(self.filePath, self.fileLength, self.startTime)
229
230
231 class TarFileHandler(FileBaseHandler):
232 target = None
233 compression = "none"
234 compressionMethods = ("none", "gzip", "bzip2")
235
236 def do_HEAD(self):
237 if self.checkAndDoRedirect():
238 return
239 self.send_response(200)
240 self.sendContentHeaders(self.fileName, -1)
241 self.end_headers()
242
243 def do_GET(self):
244 if self.checkAndDoRedirect():
245 return
246
247 tarCmd = Popen(self.getCompressionCmd(), stdout=PIPE)
248 # give the process a short time to find out if it can
249 # pack/compress the file
250 time.sleep(0.05)
251 if tarCmd.poll() != None and tarCmd.poll() != 0:
252 # something went wrong
253 print("Error while compressing '%s'. Aborting request." % self.target)
254 self.send_response(500)
255 self.end_headers()
256 return
257
258 self.send_response(200)
259 self.sendContentHeaders(self.fileName, -1)
260 self.end_headers()
261
262 block = True
263 while block and block != '':
264 block = tarCmd.stdout.read(self.blockSize)
265 if block and block != '':
266 self.wfile.write(block)
267 print("%s finished downloading" % (self.client_address[0]))
268
269 def getCompressionCmd(self):
270 if self.compression == "none":
271 cmd = ["tar", "-c"]
272 elif self.compression == "gzip":
273 cmd = ["tar", "-cz"]
274 elif self.compression == "bzip2":
275 cmd = ["tar", "-cj"]
276 else:
277 raise ValueError("Unknown compression mode '%s'." % self.compression)
278
279 dirname = os.path.basename(self.target.rstrip("/"))
280 chdirTo = os.path.dirname(self.target.rstrip("/"))
281 if chdirTo != '':
282 cmd.extend(["-C", chdirTo])
283 cmd.append(dirname)
284 return cmd
285
286 @staticmethod
287 def getCompressionExt():
288 if TarFileHandler.compression == "none":
289 return ".tar"
290 elif TarFileHandler.compression == "gzip":
291 return ".tar.gz"
292 elif TarFileHandler.compression == "bzip2":
293 return ".tar.bz2"
294 raise ValueError("Unknown compression mode '%s'." % TarFileHandler.compression)
295
296
297 class DirListingHandler(FileBaseHandler):
298 """ DOCUMENTATION MISSING """
299
300 targetDir = None
301
302 def do_HEAD(self):
303 self.getFileOrDirectory(head=True)
304
305 def do_GET(self):
306 self.getFileOrDirectory(head=False)
307
308 def getFileOrDirectory(self, head=False):
309 """ Send file or directory index, depending on requested path """
310 path = self.getCleanPath()
311
312 if os.path.isdir(path):
313 if not self.path.endswith('/'):
314 self.send_response(301)
315 self.send_header("Location", self.path + '/')
316 self.end_headers()
317 else:
318 self.sendDirectoryListing(path, head)
319 elif os.path.isfile(path):
320 if head:
321 (response, length) = self.getFileLength(path)
322 if length < 0:
323 self.send_response(response)
324 self.end_headers()
325 else:
326 self.send_response(200)
327 self.sendContentHeaders(self, path, length)
328 self.end_headers()
329 else:
330 self.sendFile(path, head)
331 else:
332 self.send_response(404)
333 errorMsg = """<!DOCTYPE html><html>
334 <head><title>404 Not Found</title></head>
335 <body>
336 <h1>Not Found</h1>
337 <p>The requestet URL %s was not found on this server</p>
338 <p><a href="/">Back to /</a>
339 </body>
340 </html>""" % self.escapeHTML(urllib.unquote(self.path))
341 self.send_header("Content-Length", str(len(errorMsg)))
342 self.end_headers()
343 if not head:
344 self.wfile.write(errorMsg)
345
346 def escapeHTML(self, htmlstr):
347 entities = [("<", "<"), (">", ">")]
348 for src, dst in entities:
349 htmlstr = htmlstr.replace(src, dst)
350 return htmlstr
351
352 def sendDirectoryListing(self, path, head):
353 """ Generate a directorylisting for path and send it """
354 header = """<!DOCTYPE html>
355 <html>
356 <head>
357 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
358 <title>Index of %(path)s</title>
359 <style type="text/css">
360 a { text-decoration: none; color: #0000BB;}
361 a:visited { color: #000066;}
362 a:hover, a:focus, a:active { text-decoration: underline; color: #cc0000; text-indent: 5px; }
363 body { background-color: #eaeaea; padding: 20px 0; margin: 0; font: 400 13px/1.2em Arial, sans-serif; }
364 h1 { margin: 0 10px 12px 10px; font-family: Arial, sans-serif; }
365 div.content { background-color: white; border-color: #ccc; border-width: 1px 0; border-style: solid; padding: 10px 10px 15px 10px; }
366 td { padding-right: 15px; text-align: left; font-family: monospace; }
367 th { font-weight: bold; font-size: 115%%; padding: 0 15px 5px 0; text-align: left; }
368 .size { text-align: right; }
369 .footer { font: 12px monospace; color: #333; margin: 5px 10px 0; }
370 .footer, h1 { text-shadow: 0 1px 0 white; }
371 </style>
372 </head>
373 <body>
374 <h1>Index of %(path)s</h1>
375 <div class="content">
376 <table summary="Directory Listing">
377 <thead>
378 <tr>
379 <th class="name">Name</th>
380 <th class="lastModified">Last Modified</th>
381 <th class="size">Size</th>
382 <th class="type">Type</th>
383 </tr>
384 </thead>
385 <tbody>
386 """ % {'path': posixpath.normpath(urllib.unquote(self.path))}
387 footer = """</tbody></table></div>
388 <div class="footer"><a href="http://seba-geek.de/stuff/servefile/">servefile %(version)s</a></div>
389 </body>
390 </html>""" % {'version': __version__}
391 content = []
392 for item in [".."] + sorted(os.listdir(path)):
393 # create path to item
394 itemPath = os.path.join(path, item)
395
396 # try to stat file for size, last modified... continue on error
397 stat = None
398 try:
399 stat = os.stat(itemPath)
400 except IOError:
401 continue
402
403 # Strings to display on directory listing
404 lastModifiedDate = datetime.datetime.fromtimestamp(stat.st_mtime)
405 lastModified = lastModifiedDate.strftime("%Y-%m-%d %H:%M")
406 fileSize = "%.1f%s" % self.convertSize(stat.st_size)
407 (fileType, _) = mimetypes.guess_type(itemPath)
408 if not fileType:
409 fileType = "-"
410
411 if os.path.isdir(itemPath):
412 item += "/"
413 fileType = "Directory"
414 content.append("""
415 <tr>
416 <td class="name"><a href="%s">%s</a></td>
417 <td class="last-modified">%s</td>
418 <td class="size">%s</td>
419 <td class="type">%s</td>
420 </tr>""" % (urllib.quote(item), item, lastModified, fileSize, fileType))
421
422 listing = header + "\n".join(content) + footer
423
424 # write listing
425 self.send_response(200)
426 self.send_header("Content-Type", "text/html")
427 if head:
428 self.end_headers()
429 return
430 self.send_header("Content-Length", str(len(listing)))
431 self.end_headers()
432 self.wfile.write(listing)
433
434 def convertSize(self, size):
435 for ext in "KMGT":
436 size /= 1024.0
437 if size < 1024.0:
438 break
439 if ext == "K" and size < 0.1:
440 size = 0.1
441 return (size, ext.strip())
442
443 def getCleanPath(self):
444 urlPath = posixpath.normpath(urllib.unquote(self.path)).strip("/")
445 path = os.path.join(self.targetDir, urlPath)
446 return path
447
448
449 class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
450 """ Simple HTTP Server which allows uploading to a specified directory
451 either via multipart/form-data or POST/PUT requests containing the file.
452 """
453
454 targetDir = None
455 maxUploadSize = 0
456 blockSize = 1024 * 1024
457 uploadPage = """
458 <!docype html>
459 <html>
460 <form action="/" method="post" enctype="multipart/form-data">
461 <label for="file">Filename:</label>
462 <input type="file" name="file" id="file" />
463 <br />
464 <input type="submit" name="submit" value="Upload" />
465 </form>
466 </html>
467 """
468
469 def do_GET(self):
470 """ Answer every GET request with the upload form """
471 self.sendResponse(200, self.uploadPage)
472
473 def do_POST(self):
474 """ Upload a file via POST
475
476 If the content-type is multipart/form-data it checks for the file
477 field and saves the data to disk. For other content-types it just
478 calls do_PUT and is handled as such except for the http response code.
479
480 Files can be uploaded with wget --post-file=path/to/file <url> or
481 curl -X POST -d @file <url> .
482 """
483 length = self.getContentLength()
484 if length < 0:
485 return
486 ctype = self.headers.getheader('Content-Type')
487
488 # check for multipart/form-data.
489 if not (ctype and ctype.lower().startswith("multipart/form-data")):
490 # not a normal multipart request ==> handle as PUT request
491 return self.do_PUT(fromPost=True)
492
493 # create FieldStorage object for multipart parsing
494 env = os.environ
495 env['REQUEST_METHOD'] = "POST"
496 fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
497 if not "file" in fstorage:
498 self.sendResponse(400, "No file found in request.")
499 return
500
501 destFileName = self.getTargetName(fstorage["file"].filename)
502 if destFileName == "":
503 self.sendResponse(400, "Filename was empty or invalid")
504 return
505
506 # write file down to disk, send a 200 afterwards
507 target = open(destFileName, "w")
508 bytesLeft = length
509 while bytesLeft > 0:
510 bytesToRead = min(self.blockSize, bytesLeft)
511 target.write(fstorage["file"].file.read(bytesToRead))
512 bytesLeft -= bytesToRead
513 target.close()
514 self.sendResponse(200, "OK! Thanks for uploading")
515 print("Received file '%s' from %s." % (destFileName, self.client_address[0]))
516
517 def do_PUT(self, fromPost=False):
518 """ Upload a file via PUT
519
520 The request path is used as filename, so uploading a file to the url
521 http://host:8080/testfile will cause the file to be named testfile. If
522 no filename is given, a random name will be generated.
523
524 Files can be uploaded with e.g. curl -X POST -d @file <url> .
525 """
526 length = self.getContentLength()
527 if length < 0:
528 return
529
530 fileName = urllib.unquote(self.path)
531 if fileName == "/":
532 # if no filename was given we have to generate one
533 fileName = str(time.time())
534
535 cleanFileName = self.getTargetName(fileName)
536 if cleanFileName == "":
537 self.sendResponse(400, "Filename was invalid")
538 return
539
540 # Sometimes clients want to be told to continue with their transfer
541 if self.headers.getheader("Expect") == "100-continue":
542 self.send_response(100)
543 self.end_headers()
544
545 target = open(cleanFileName, "w")
546 bytesLeft = int(self.headers['Content-Length'])
547 while bytesLeft > 0:
548 bytesToRead = min(self.blockSize, bytesLeft)
549 target.write(self.rfile.read(bytesToRead))
550 bytesLeft -= bytesToRead
551 target.close()
552 self.sendResponse(200 if fromPost else 201, "OK!")
553
554 def getContentLength(self):
555 length = 0
556 try:
557 length = int(self.headers['Content-Length'])
558 except (ValueError, KeyError):
559 pass
560 if length <= 0:
561 self.sendResponse(411, "Content-Length was invalid or not set.")
562 return -1
563 if self.maxUploadSize > 0 and length > self.maxUploadSize:
564 self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" % self.maxUploadSize)
565 return -1
566 return length
567
568 def sendResponse(self, code, msg):
569 """ Send a HTTP response with HTTP statuscode code and message msg,
570 providing the correct content-length.
571 """
572 self.send_response(code)
573 self.send_header('Content-Type', 'text/html')
574 self.send_header('Content-Length', str(len(msg)))
575 self.end_headers()
576 self.wfile.write(msg)
577
578 def getTargetName(self, fname):
579 """ Generate a clean and secure filename.
580
581 This function takes a filename and strips all the slashes out of it.
582 If the file already exists in the target directory, a (NUM) will be
583 appended, so no file will be overwritten.
584 """
585 cleanFileName = fname.replace("/", "")
586 if cleanFileName == "":
587 return ""
588 destFileName = os.path.join(self.targetDir, cleanFileName)
589 if not os.path.exists(destFileName):
590 return destFileName
591 else:
592 i = 1
593 extraDestFileName = destFileName + "(%s)" % i
594 while os.path.exists(extraDestFileName):
595 i += 1
596 extraDestFileName = destFileName + "(%s)" % i
597 return extraDestFileName
598 # never reached
599
600 class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
601 pass
602
603 def catchSSLErrors(BaseSSLClass):
604 """ Class decorator which catches SSL errors and prints them. """
605 class X(BaseSSLClass):
606 def handle_one_request(self, *args, **kwargs):
607 try:
608 BaseSSLClass.handle_one_request(self, *args, **kwargs)
609 except SSL.Error as e:
610 if str(e) == "":
611 print("%s SSL error (empty error message)" % (self.client_address[0],))
612 else:
613 print("%s SSL error: %s" % (self.client_address[0], e))
614 return X
615
616
617 class SecureThreadedHTTPServer(ThreadedHTTPServer):
618 def __init__(self, pubKey, privKey, server_address, RequestHandlerClass, bind_and_activate=True):
619 ThreadedHTTPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate)
620 ctx = SSL.Context(SSL.SSLv23_METHOD)
621 if type(pubKey) is crypto.X509 and type(privKey) is crypto.PKey:
622 ctx.use_certificate(pubKey)
623 ctx.use_privatekey(privKey)
624 else:
625 ctx.use_certificate_file(pubKey)
626 ctx.use_privatekey_file(privKey)
627
628 self.bsocket = socket.socket(self.address_family, self.socket_type)
629 self.socket = SSL.Connection(ctx, self.bsocket)
630
631 if bind_and_activate:
632 self.server_bind()
633 self.server_activate()
634
635 def shutdown_request(self, request):
636 request.shutdown()
637
638
639 class SecureHandler():
640 def setup(self):
641 self.connection = self.request
642 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
643 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
644
645 class ServeFileException(Exception):
646 pass
647
648
649 class ServeFile():
650 """ Main class to manage everything. """
651
652 _NUM_MODES = 4
653 (MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD, MODE_LISTDIR) = range(_NUM_MODES)
654
655 def __init__(self, target, port=8080, serveMode=0, useSSL=False):
656 self.target = target
657 self.port = port
658 self.serveMode = serveMode
659 self.dirCreated = False
660 self.useSSL = useSSL
661 self.cert = self.key = None
662 self.auth = None
663 self.maxUploadSize = 0
664 self.listenIPv4 = True
665 self.listenIPv6 = True
666
667 if self.serveMode not in range(self._NUM_MODES):
668 self.serveMode = None
669 raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD or MODE_DIRLIST.")
670
671 def setIPv4(self, ipv4):
672 """ En- or disable ipv4 """
673 self.listenIPv4 = ipv4
674
675 def setIPv6(self, ipv6):
676 """ En- or disable ipv6 """
677 self.listenIPv6 = ipv6
678
679 def getIPs(self):
680 """ Get IPs from all interfaces via ip or ifconfig. """
681 # ip and ifconfig sometimes are located in /sbin/
682 os.environ['PATH'] += ':/sbin:/usr/sbin'
683 proc = Popen(r"ip addr|" + \
684 "sed -n -e 's/.*inet6\{0,1\} \([0-9.a-fA-F:]\+\).*/\\1/ p'|" + \
685 "grep -v '^fe80\|^127.0.0.1\|^::1'", \
686 shell=True, stdout=PIPE, stderr=PIPE)
687 if proc.wait() != 0:
688 # ip failed somehow, falling back to ifconfig
689 oldLang = os.environ.get("LC_ALL", None)
690 os.environ['LC_ALL'] = "C"
691 proc = Popen(r"ifconfig|" + \
692 "sed -n 's/.*inet6\{0,1\}\( addr:\)\{0,1\} \{0,1\}\([0-9a-fA-F.:]*\).*/" + \
693 "\\2/p'|" + \
694 "grep -v '^fe80\|^127.0.0.1\|^::1'", \
695 shell=True, stdout=PIPE, stderr=PIPE)
696 if oldLang:
697 os.environ['LC_ALL'] = oldLang
698 else:
699 del(os.environ['LC_ALL'])
700 if proc.wait() != 0:
701 # we couldn't find any ip address
702 proc = None
703 if proc:
704 ips = proc.stdout.read().strip().split("\n")
705
706 # filter out ips we are not listening on
707 if not self.listenIPv6:
708 ips = filter(lambda ip: ":" not in ip, ips)
709 if not self.listenIPv4:
710 ips = filter(lambda ip: "." not in ip, ips)
711
712 return ips
713 return None
714
715 def setSSLKeys(self, cert, key):
716 """ Set SSL cert/key. Can be either path to file or pyssl X509/PKey object. """
717 self.cert = cert
718 self.key = key
719
720 def setMaxUploadSize(self, limit):
721 """ Set the maximum upload size in byte """
722 self.maxUploadSize = limit
723
724 def setCompression(self, compression):
725 """ Set the compression of TarFileHandler """
726 if self.serveMode != self.MODE_SINGLETAR:
727 raise ServeFileException("Compression mode can only be set in tar-mode.")
728 if compression not in TarFileHandler.compressionMethods:
729 raise ServeFileException("Compression mode not available.")
730 TarFileHandler.compression = compression
731
732 def genKeyPair(self):
733 print("Generating SSL certificate...", end="")
734 sys.stdout.flush()
735
736 pkey = crypto.PKey()
737 pkey.generate_key(crypto.TYPE_RSA, 2048)
738
739 req = crypto.X509Req()
740 subj = req.get_subject()
741 subj.CN = "127.0.0.1"
742 subj.O = "servefile laboratories"
743 subj.OU = "servefile"
744
745 # generate altnames
746 altNames = []
747 for ip in self.getIPs() + ["127.0.0.1", "::1"]:
748 altNames.append("IP:%s" % ip)
749 altNames.append("DNS:localhost")
750 ext = crypto.X509Extension("subjectAltName", False, ",".join(altNames))
751 req.add_extensions([ext])
752
753 req.set_pubkey(pkey)
754 req.sign(pkey, "sha1")
755
756 cert = crypto.X509()
757 # some browsers complain if they see a cert from the same authority
758 # with the same serial ==> we just use the seconds as serial.
759 cert.set_serial_number(int(time.time()))
760 cert.gmtime_adj_notBefore(0)
761 cert.gmtime_adj_notAfter(365*24*60*60)
762 cert.set_issuer(req.get_subject())
763 cert.set_subject(req.get_subject())
764 cert.add_extensions([ext])
765 cert.set_pubkey(req.get_pubkey())
766 cert.sign(pkey, "sha1")
767
768 self.cert = cert
769 self.key = pkey
770
771 print("done.")
772 print("SHA1 fingerprint:", cert.digest("sha1"))
773 print("MD5 fingerprint:", cert.digest("md5"))
774
775 def _getCert(self):
776 return self.cert
777
778 def _getKey(self):
779 return self.key
780
781 def setAuth(self, user, password, realm=None):
782 if not user or not password:
783 raise ServeFileException("User and password both need to be at least one character.")
784 self.auth = base64.b64encode("%s:%s" % (user, password))
785 self.authrealm = realm
786
787 def _createServer(self, handler, withv6=False):
788 ThreadedHTTPServer.address_family = socket.AF_INET
789 SecureThreadedHTTPServer.address_family = socket.AF_INET
790 listenIp = ''
791 server = None
792
793 if withv6:
794 listenIp = '::'
795 ThreadedHTTPServer.address_family = socket.AF_INET6
796 SecureThreadedHTTPServer.address_family = socket.AF_INET6
797
798 if self.useSSL:
799 if not self._getKey():
800 self.genKeyPair()
801 try:
802 server = SecureThreadedHTTPServer(self._getCert(), self._getKey(),
803 (listenIp, self.port), handler, bind_and_activate=False)
804 except SSL.Error as e:
805 raise ServeFileException("SSL error: Could not read SSL public/private key from file(s) (error was: \"%s\")" % (e[0][0][2],))
806 else:
807 server = ThreadedHTTPServer((listenIp, self.port), handler,
808 bind_and_activate=False)
809
810 if withv6:
811 server.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
812
813 server.server_bind()
814 server.server_activate()
815
816 return server
817
818 def serve(self):
819 self.handler = self._confAndFindHandler()
820 self.server = []
821
822 try:
823 currsocktype = "IPv4"
824 if self.listenIPv4:
825 self.server.append(self._createServer(self.handler))
826 currsocktype = "IPv6"
827 if self.listenIPv6:
828 self.server.append(self._createServer(self.handler, withv6=True))
829 except socket.error as e:
830 raise ServeFileException("Could not open %s socket: %s" % (currsocktype, e))
831
832 if self.serveMode != self.MODE_UPLOAD:
833 print("Serving \"%s\" at port %d." % (self.target, self.port))
834 else:
835 print("Serving \"%s\" for uploads at port %d." % (self.target, self.port))
836
837 # print urls with local network adresses
838 print("\nSome addresses %s will be available at:" % \
839 ("this file" if (self.serveMode != self.MODE_UPLOAD) else "the uploadform", ))
840 ips = self.getIPs()
841 if not ips or len(ips) == 0 or ips[0] == '':
842 print("Could not find any addresses.")
843 else:
844 for ip in ips:
845 if ":" in ip:
846 ip = "[%s]" % ip
847 print("\thttp%s://%s:%d/" % (self.useSSL and "s" or "", ip, self.port))
848 print()
849
850 try:
851 while True:
852 (servers, _, _) = select.select(self.server, [], [])
853 for server in servers:
854 server.handle_request()
855 except KeyboardInterrupt:
856 for server in self.server:
857 server.socket.close()
858
859 # cleanup potential upload directory
860 if self.dirCreated and len(os.listdir(self.target)) == 0:
861 # created upload dir was not used
862 os.rmdir(self.target)
863
864 def _confAndFindHandler(self):
865 handler = None
866 if self.serveMode == self.MODE_SINGLE:
867 try:
868 testit = open(self.target, 'r')
869 testit.close()
870 except IOError:
871 raise ServeFileException("Error: Could not open file!")
872 FileHandler.filePath = self.target
873 FileHandler.fileName = os.path.basename(self.target)
874 FileHandler.fileLength = os.stat(self.target).st_size
875 handler = FileHandler
876 elif self.serveMode == self.MODE_SINGLETAR:
877 self.realTarget = os.path.realpath(self.target)
878 if not os.path.exists(self.realTarget):
879 raise ServeFileException("Error: Could not open file or directory.")
880 TarFileHandler.target = self.realTarget
881 TarFileHandler.fileName = os.path.basename(self.realTarget.rstrip("/")) + TarFileHandler.getCompressionExt()
882
883 handler = TarFileHandler
884 elif self.serveMode == self.MODE_UPLOAD:
885 if os.path.isdir(self.target):
886 print("Warning: Uploading to an already existing directory.")
887 elif not os.path.exists(self.target):
888 self.dirCreated = True
889 try:
890 os.mkdir(self.target)
891 except (IOError, OSError):
892 raise ServeFileException("Error: Could not create directory '%s' for uploads." % (self.target,))
893 else:
894 raise ServeFileException("Error: Upload directory already exists and is a file.")
895 FilePutter.targetDir = self.target
896 FilePutter.maxUploadSize = self.maxUploadSize
897 handler = FilePutter
898 elif self.serveMode == self.MODE_LISTDIR:
899 if not os.path.exists(self.target):
900 raise ServeFileException("Error: Could not open file or directory.")
901 if not os.path.isdir(self.target):
902 raise ServeFileException("Error: '%s' is not a directory." % (self.target,))
903 handler = DirListingHandler
904 handler.targetDir = self.target
905
906 if self.auth:
907 # do authentication
908 AuthenticationHandler.authString = self.auth
909 if self.authrealm:
910 AuthenticationHandler.realm = self.authrealm
911 class AuthenticatedHandler(AuthenticationHandler, handler):
912 pass
913 handler = AuthenticatedHandler
914
915 if self.useSSL:
916 # secure handler
917 @catchSSLErrors
918 class AlreadySecuredHandler(SecureHandler, handler):
919 pass
920 handler = AlreadySecuredHandler
921 return handler
922
923
924 class AuthenticationHandler():
925 # base64 encoded user:password string for authentication
926 authString = None
927 realm = "Restricted area"
928
929 def handle_one_request(self):
930 """ Overloaded function to handle one request.
931
932 Before calling the responsible do_METHOD function, check credentials
933 """
934 self.raw_requestline = self.rfile.readline()
935 if not self.raw_requestline:
936 self.close_connection = 1
937 return
938 if not self.parse_request(): # An error code has been sent, just exit
939 return
940
941 authorized = False
942 if "Authorization" in self.headers:
943 if self.headers["Authorization"] == ("Basic " + self.authString):
944 authorized = True
945 if authorized:
946 mname = 'do_' + self.command
947 if not hasattr(self, mname):
948 self.send_error(501, "Unsupported method (%r)" % self.command)
949 return
950 method = getattr(self, mname)
951 method()
952 else:
953 self.send_response(401)
954 self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm)
955
956
957 def main():
958 parser = argparse.ArgumentParser(description='Serve a single file via HTTP.')
959 parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
960 parser.add_argument('target', metavar='file/directory', type=str)
961 parser.add_argument('-p', '--port', type=int, default=8080, \
962 help='Port to listen on')
963 parser.add_argument('-u', '--upload', action="store_true", default=False, \
964 help="Enable uploads to a given directory")
965 parser.add_argument('-s', '--max-upload-size', type=str, \
966 help="Limit upload size in kB. Size modifiers are allowed, e.g. 2G, 12MB, 1B")
967 parser.add_argument('-l', '--list-dir', action="store_true", default=False, \
968 help="Show directory indexes and allow access to all subdirectories")
969 parser.add_argument('--ssl', action="store_true", default=False, \
970 help="Enable SSL. If no key/cert is specified one will be generated")
971 parser.add_argument('--key', type=str, \
972 help="Keyfile to use for SSL. If no cert is given with --cert the keyfile will also be searched for a cert")
973 parser.add_argument('--cert', type=str, \
974 help="Certfile to use for SSL")
975 parser.add_argument('-a', '--auth', type=str, metavar='user:password', \
976 help="Set user and password for HTTP basic authentication")
977 parser.add_argument('--realm', type=str, default=None,\
978 help="Set a realm for HTTP basic authentication")
979 parser.add_argument('-t', '--tar', action="store_true", default=False, \
980 help="Enable on the fly tar creation for given file or directory. Note: Download continuation will not be available")
981 parser.add_argument('-c', '--compression', type=str, metavar='method', \
982 default="none", \
983 help="Set compression method, only in combination with --tar. Can be one of %s" % ", ".join(TarFileHandler.compressionMethods))
984 parser.add_argument('-4', '--ipv4-only', action="store_true", default=False, \
985 help="Listen on IPv4 only")
986 parser.add_argument('-6', '--ipv6-only', action="store_true", default=False, \
987 help="Listen on IPv6 only")
988
989 args = parser.parse_args()
990 maxUploadSize = 0
991
992 # check for invalid option combinations/preparse stuff
993 if args.max_upload_size and not args.upload:
994 print("Error: Maximum upload size can only be specified when in upload mode.")
995 sys.exit(1)
996
997 if args.upload and args.list_dir:
998 print("Error: Upload and dirlisting can't be enabled together.")
999 sys.exit(1)
1000
1001 if args.max_upload_size:
1002 sizeRe = re.match("^(\d+(?:[,.]\d+)?)(?:([bkmgtpe])(?:(?<!b)b?)?)?$", args.max_upload_size.lower())
1003 if not sizeRe:
1004 print("Error: Your max upload size param is broken. Try something like 3M or 2.5Gb.")
1005 sys.exit(1)
1006 uploadSize, modifier = sizeRe.groups()
1007 uploadSize = float(uploadSize.replace(",", "."))
1008 sizes = ["b", "k", "m", "g", "t", "p", "e"]
1009 maxUploadSize = int(uploadSize * pow(1024, sizes.index(modifier or "k")))
1010 if maxUploadSize < 0:
1011 print("Error: Your max upload size can't be negative")
1012 sys.exit(1)
1013
1014 if args.ssl and not HAVE_SSL:
1015 print("Error: SSL is not available, please install pyssl (python-openssl).")
1016 sys.exit(1)
1017
1018 if args.cert and not args.key:
1019 print("Error: Please specify a key along with your cert.")
1020 sys.exit(1)
1021
1022 if not args.ssl and (args.cert or args.key):
1023 print("Error: You need to enable ssl with --ssl when specifying certs/keys.")
1024 sys.exit(1)
1025
1026 if args.auth:
1027 dpos = args.auth.find(":")
1028 if dpos <= 0 or dpos == (len(args.auth)-1):
1029 print("Error: User and password for HTTP basic authentication need to be both at least one character and have to be separated by a \":\".")
1030 sys.exit(1)
1031
1032 if args.realm and not args.auth:
1033 print("You can only specify a realm when HTTP basic authentication is enabled.")
1034 sys.exit(1)
1035
1036 if args.compression != "none" and not args.tar:
1037 print("Error: Please use --tar if you want to tar everything.")
1038 sys.exit(1)
1039
1040 if args.tar and args.upload:
1041 print("Error: --tar mode will not work with uploads.")
1042 sys.exit(1)
1043
1044 if args.tar and args.list_dir:
1045 print("Error: --tar mode will not work with directory listings.")
1046 sys.exit(1)
1047
1048 compression = None
1049 if args.compression:
1050 if args.compression in TarFileHandler.compressionMethods:
1051 compression = args.compression
1052 else:
1053 print("Error: Compression mode '%s' is unknown." % args.compression)
1054 sys.exit(1)
1055
1056 if args.ipv4_only and args.ipv6_only:
1057 print("You can't listen both on IPv4 and IPv6 \"only\".")
1058 sys.exit(1)
1059
1060 if args.ipv6_only and not socket.has_ipv6:
1061 print("Your system does not support IPv6.")
1062 sys.exit(1)
1063
1064 mode = None
1065 if args.upload:
1066 mode = ServeFile.MODE_UPLOAD
1067 elif args.list_dir:
1068 mode = ServeFile.MODE_LISTDIR
1069 elif args.tar:
1070 mode = ServeFile.MODE_SINGLETAR
1071 else:
1072 mode = ServeFile.MODE_SINGLE
1073
1074 server = None
1075 try:
1076 server = ServeFile(args.target, args.port, mode, args.ssl)
1077 if maxUploadSize > 0:
1078 server.setMaxUploadSize(maxUploadSize)
1079 if args.ssl and args.key:
1080 cert = args.cert or args.key
1081 server.setSSLKeys(cert, args.key)
1082 if args.auth:
1083 user, password = args.auth.split(":", 1)
1084 server.setAuth(user, password, args.realm)
1085 if compression and compression != "none":
1086 server.setCompression(compression)
1087 if args.ipv4_only or not socket.has_ipv6:
1088 server.setIPv6(False)
1089 if args.ipv6_only:
1090 server.setIPv4(False)
1091 server.serve()
1092 except ServeFileException as e:
1093 print(e)
1094 sys.exit(1)
1095 print("Good bye.")
1096
1097
1098 if __name__ == '__main__':
1099 main()
