No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

servefile 39KB

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