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.py 46KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270
  1. #!/usr/bin/env 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.5.1'
  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', '/' + quote(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 is not 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() is not 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 = 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))} # noqa: E501
  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. if sys.version_info.major >= 3:
  472. listing = listing.encode()
  473. self.wfile.write(listing)
  474. def convertSize(self, size):
  475. for ext in "KMGT":
  476. size /= 1024.0
  477. if size < 1024.0:
  478. break
  479. if ext == "K" and size < 0.1:
  480. size = 0.1
  481. return (size, ext.strip())
  482. def getCleanPath(self):
  483. urlPath = os.path.normpath(unquote(self.path)).strip("/")
  484. path = os.path.join(self.targetDir, urlPath)
  485. return path
  486. class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
  487. """ Simple HTTP Server which allows uploading to a specified directory
  488. either via multipart/form-data or POST/PUT requests containing the file.
  489. """
  490. targetDir = None
  491. maxUploadSize = 0
  492. blockSize = 1024 * 1024
  493. uploadPage = """
  494. <!docype html>
  495. <html>
  496. <form action="/" method="post" enctype="multipart/form-data">
  497. <label for="file">Filename:</label>
  498. <input type="file" name="file" id="file" />
  499. <br />
  500. <input type="submit" name="submit" value="Upload" />
  501. </form>
  502. </html>
  503. """
  504. def do_GET(self):
  505. """ Answer every GET request with the upload form """
  506. self.sendResponse(200, self.uploadPage)
  507. def do_POST(self):
  508. """ Upload a file via POST
  509. If the content-type is multipart/form-data it checks for the file
  510. field and saves the data to disk. For other content-types it just
  511. calls do_PUT and is handled as such except for the http response code.
  512. Files can be uploaded with wget --post-file=path/to/file <url> or
  513. curl -X POST -d @file <url> .
  514. """
  515. length = self.getContentLength()
  516. if length < 0:
  517. return
  518. print(self.headers)
  519. ctype = self.headers.get('Content-Type')
  520. # check for multipart/form-data.
  521. if not (ctype and ctype.lower().startswith("multipart/form-data")):
  522. # not a normal multipart request ==> handle as PUT request
  523. return self.do_PUT(fromPost=True)
  524. # create FieldStorage object for multipart parsing
  525. env = os.environ
  526. env['REQUEST_METHOD'] = "POST"
  527. fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
  528. if "file" not in fstorage:
  529. self.sendResponse(400, "No file found in request.")
  530. return
  531. destFileName = self.getTargetName(fstorage["file"].filename)
  532. if destFileName == "":
  533. self.sendResponse(400, "Filename was empty or invalid")
  534. return
  535. # write file down to disk, send a 200 afterwards
  536. target = open(destFileName, "wb")
  537. bytesLeft = length
  538. while bytesLeft > 0:
  539. bytesToRead = min(self.blockSize, bytesLeft)
  540. target.write(fstorage["file"].file.read(bytesToRead))
  541. bytesLeft -= bytesToRead
  542. target.close()
  543. self.sendResponse(200, "OK! Thanks for uploading")
  544. print("Received file '%s' from %s." % (destFileName, self.client_address[0]))
  545. def do_PUT(self, fromPost=False):
  546. """ Upload a file via PUT
  547. The request path is used as filename, so uploading a file to the url
  548. http://host:8080/testfile will cause the file to be named testfile. If
  549. no filename is given, a random name will be generated.
  550. Files can be uploaded with e.g. curl -T file <url> .
  551. """
  552. length = self.getContentLength()
  553. if length < 0:
  554. return
  555. fileName = unquote(self.path)
  556. if fileName == "/":
  557. # if no filename was given we have to generate one
  558. fileName = str(time.time())
  559. cleanFileName = self.getTargetName(fileName)
  560. if cleanFileName == "":
  561. self.sendResponse(400, "Filename was invalid")
  562. return
  563. # Sometimes clients want to be told to continue with their transfer
  564. if self.headers.get("Expect") == "100-continue":
  565. self.send_response(100)
  566. self.end_headers()
  567. target = open(cleanFileName, "wb")
  568. bytesLeft = int(self.headers['Content-Length'])
  569. while bytesLeft > 0:
  570. bytesToRead = min(self.blockSize, bytesLeft)
  571. target.write(self.rfile.read(bytesToRead))
  572. bytesLeft -= bytesToRead
  573. target.close()
  574. self.sendResponse(200 if fromPost else 201, "OK!")
  575. def getContentLength(self):
  576. length = 0
  577. try:
  578. length = int(self.headers['Content-Length'])
  579. except (ValueError, KeyError):
  580. pass
  581. if length <= 0:
  582. self.sendResponse(411, "Content-Length was invalid or not set.")
  583. return -1
  584. if self.maxUploadSize > 0 and length > self.maxUploadSize:
  585. self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" %
  586. self.maxUploadSize)
  587. return -1
  588. return length
  589. def sendResponse(self, code, msg):
  590. """ Send a HTTP response with HTTP statuscode code and message msg,
  591. providing the correct content-length.
  592. """
  593. self.send_response(code)
  594. self.send_header('Content-Type', 'text/html')
  595. self.send_header('Content-Length', str(len(msg)))
  596. self.send_header('Connection', 'close')
  597. self.end_headers()
  598. self.wfile.write(msg.encode())
  599. def getTargetName(self, fname):
  600. """ Generate a clean and secure filename.
  601. This function takes a filename and strips all the slashes out of it.
  602. If the file already exists in the target directory, a (NUM) will be
  603. appended, so no file will be overwritten.
  604. """
  605. cleanFileName = fname.replace("/", "")
  606. if cleanFileName == "":
  607. return ""
  608. destFileName = os.path.join(self.targetDir, cleanFileName)
  609. if not os.path.exists(destFileName):
  610. return destFileName
  611. else:
  612. i = 1
  613. extraDestFileName = destFileName + "(%s)" % i
  614. while os.path.exists(extraDestFileName):
  615. i += 1
  616. extraDestFileName = destFileName + "(%s)" % i
  617. return extraDestFileName
  618. # never reached
  619. class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
  620. def handle_error(self, request, client_address):
  621. _, exc_value, _ = sys.exc_info()
  622. print("%s ABORTED transmission (Reason: %s)" % (client_address[0], exc_value))
  623. def catchSSLErrors(BaseSSLClass):
  624. """ Class decorator which catches SSL errors and prints them. """
  625. class X(BaseSSLClass):
  626. def handle_one_request(self, *args, **kwargs):
  627. try:
  628. BaseSSLClass.handle_one_request(self, *args, **kwargs)
  629. except SSL.Error as e:
  630. if str(e) == "":
  631. print("%s SSL error (empty error message)" % (self.client_address[0],))
  632. else:
  633. print("%s SSL error: %s" % (self.client_address[0], e))
  634. return X
  635. class SecureThreadedHTTPServer(ThreadedHTTPServer):
  636. def __init__(self, pubKey, privKey, server_address, RequestHandlerClass, bind_and_activate=True):
  637. ThreadedHTTPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate)
  638. # choose TLS1.2 or TLS1, if available
  639. sslMethod = None
  640. if hasattr(SSL, "TLSv1_2_METHOD"):
  641. sslMethod = SSL.TLSv1_2_METHOD
  642. elif hasattr(SSL, "TLSv1_METHOD"):
  643. sslMethod = SSL.TLSv1_METHOD
  644. else:
  645. # only SSLv23 available
  646. print("Warning: Only SSLv2/SSLv3 is available, connection might be insecure.")
  647. sslMethod = SSL.SSLv23_METHOD
  648. ctx = SSL.Context(sslMethod)
  649. if type(pubKey) is crypto.X509 and type(privKey) is crypto.PKey:
  650. ctx.use_certificate(pubKey)
  651. ctx.use_privatekey(privKey)
  652. else:
  653. ctx.use_certificate_file(pubKey)
  654. ctx.use_privatekey_file(privKey)
  655. self.bsocket = socket.socket(self.address_family, self.socket_type)
  656. self.socket = SSL.Connection(ctx, self.bsocket)
  657. if bind_and_activate:
  658. self.server_bind()
  659. self.server_activate()
  660. def shutdown_request(self, request):
  661. try:
  662. request.shutdown()
  663. except SSL.Error:
  664. # ignore SSL errors on connection shutdown
  665. pass
  666. class SecureHandler():
  667. def setup(self):
  668. self.connection = self.request
  669. if sys.version_info[0] > 2:
  670. # python3 SocketIO (replacement for socket._fileobject)
  671. raw_read_sock = socket.SocketIO(self.request, 'rb')
  672. raw_write_sock = socket.SocketIO(self.request, 'wb')
  673. rbufsize = self.rbufsize > 0 and self.rbufsize or io.DEFAULT_BUFFER_SIZE
  674. wbufsize = self.wbufsize > 0 and self.wbufsize or io.DEFAULT_BUFFER_SIZE
  675. self.rfile = io.BufferedReader(raw_read_sock, rbufsize)
  676. self.wfile = io.BufferedWriter(raw_write_sock, wbufsize)
  677. else:
  678. # python2 does not have SocketIO
  679. self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
  680. self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
  681. class ServeFileException(Exception):
  682. pass
  683. class ServeFile():
  684. """ Main class to manage everything. """
  685. _NUM_MODES = 4
  686. (MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD, MODE_LISTDIR) = range(_NUM_MODES)
  687. def __init__(self, target, port=8080, serveMode=0, useSSL=False):
  688. self.target = target
  689. self.port = port
  690. self.serveMode = serveMode
  691. self.dirCreated = False
  692. self.useSSL = useSSL
  693. self.cert = self.key = None
  694. self.auth = None
  695. self.maxUploadSize = 0
  696. self.listenIPv4 = True
  697. self.listenIPv6 = True
  698. if self.serveMode not in range(self._NUM_MODES):
  699. self.serveMode = None
  700. raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, "
  701. "MODE_SINGLETAR, MODE_UPLOAD or MODE_DIRLIST.")
  702. def setIPv4(self, ipv4):
  703. """ En- or disable ipv4 """
  704. self.listenIPv4 = ipv4
  705. def setIPv6(self, ipv6):
  706. """ En- or disable ipv6 """
  707. self.listenIPv6 = ipv6
  708. def getIPs(self):
  709. """ Get IPs from all interfaces via ip or ifconfig. """
  710. # ip and ifconfig sometimes are located in /sbin/
  711. os.environ['PATH'] += ':/sbin:/usr/sbin'
  712. proc = Popen(r"ip addr|"
  713. r"sed -n -e 's/.*inet6\{0,1\} \([0-9.a-fA-F:]\+\).*/\1/ p'|"
  714. r"grep -v '^fe80\|^127.0.0.1\|^::1'",
  715. shell=True, stdout=PIPE, stderr=PIPE)
  716. if proc.wait() != 0:
  717. # ip failed somehow, falling back to ifconfig
  718. oldLang = os.environ.get("LC_ALL", None)
  719. os.environ['LC_ALL'] = "C"
  720. proc = Popen(r"ifconfig|"
  721. r"sed -n 's/.*inet6\{0,1\}\( addr:\)\{0,1\} \{0,1\}\([0-9a-fA-F.:]*\).*/"
  722. r"\2/p'|"
  723. r"grep -v '^fe80\|^127.0.0.1\|^::1'",
  724. shell=True, stdout=PIPE, stderr=PIPE)
  725. if oldLang:
  726. os.environ['LC_ALL'] = oldLang
  727. else:
  728. del(os.environ['LC_ALL'])
  729. if proc.wait() != 0:
  730. # we couldn't find any ip address
  731. proc = None
  732. if proc:
  733. ips = proc.stdout.read().decode().strip().split("\n")
  734. # filter out ips we are not listening on
  735. if not self.listenIPv6:
  736. ips = [ip for ip in ips if '.' in ip]
  737. if not self.listenIPv4:
  738. ips = [ip for ip in ips if ':' in ip]
  739. return ips
  740. return None
  741. def setSSLKeys(self, cert, key):
  742. """ Set SSL cert/key. Can be either path to file or pyopenssl X509/PKey object. """
  743. self.cert = cert
  744. self.key = key
  745. def setMaxUploadSize(self, limit):
  746. """ Set the maximum upload size in byte """
  747. self.maxUploadSize = limit
  748. def setCompression(self, compression):
  749. """ Set the compression of TarFileHandler """
  750. if self.serveMode != self.MODE_SINGLETAR:
  751. raise ServeFileException("Compression mode can only be set in tar-mode.")
  752. if compression not in TarFileHandler.compressionMethods:
  753. raise ServeFileException("Compression mode not available.")
  754. TarFileHandler.compression = compression
  755. def genKeyPair(self):
  756. print("Generating SSL certificate...", end="")
  757. sys.stdout.flush()
  758. pkey = crypto.PKey()
  759. pkey.generate_key(crypto.TYPE_RSA, 2048)
  760. req = crypto.X509Req()
  761. subj = req.get_subject()
  762. subj.CN = "127.0.0.1"
  763. subj.O = "servefile laboratories" # noqa: E741
  764. subj.OU = "servefile"
  765. # generate altnames
  766. altNames = []
  767. for ip in self.getIPs() + ["127.0.0.1", "::1"]:
  768. altNames.append("IP:%s" % ip)
  769. altNames.append("DNS:localhost")
  770. ext = crypto.X509Extension(b"subjectAltName", False, (",".join(altNames)).encode())
  771. req.add_extensions([ext])
  772. req.set_pubkey(pkey)
  773. req.sign(pkey, "sha1")
  774. cert = crypto.X509()
  775. # Mozilla only accepts v3 certificates with v3 extensions, not v1
  776. cert.set_version(0x2)
  777. # some browsers complain if they see a cert from the same authority
  778. # with the same serial ==> we just use the seconds as serial.
  779. cert.set_serial_number(int(time.time()))
  780. cert.gmtime_adj_notBefore(0)
  781. cert.gmtime_adj_notAfter(365*24*60*60)
  782. cert.set_issuer(req.get_subject())
  783. cert.set_subject(req.get_subject())
  784. cert.add_extensions([ext])
  785. cert.set_pubkey(req.get_pubkey())
  786. cert.sign(pkey, "sha1")
  787. self.cert = cert
  788. self.key = pkey
  789. print("done.")
  790. print("SHA1 fingerprint:", cert.digest("sha1").decode())
  791. print("MD5 fingerprint:", cert.digest("md5").decode())
  792. def _getCert(self):
  793. return self.cert
  794. def _getKey(self):
  795. return self.key
  796. def setAuth(self, user, password, realm=None):
  797. if not user or not password:
  798. raise ServeFileException("User and password both need to be at least one character.")
  799. self.auth = base64.b64encode(("%s:%s" % (user, password)).encode()).decode()
  800. self.authrealm = realm
  801. def _createServer(self, handler, withv6=False):
  802. ThreadedHTTPServer.address_family = socket.AF_INET
  803. SecureThreadedHTTPServer.address_family = socket.AF_INET
  804. listenIp = ''
  805. server = None
  806. if withv6:
  807. listenIp = '::'
  808. ThreadedHTTPServer.address_family = socket.AF_INET6
  809. SecureThreadedHTTPServer.address_family = socket.AF_INET6
  810. if self.useSSL:
  811. if not self._getKey():
  812. self.genKeyPair()
  813. try:
  814. server = SecureThreadedHTTPServer(self._getCert(), self._getKey(),
  815. (listenIp, self.port), handler, bind_and_activate=False)
  816. except SSL.Error as e:
  817. raise ServeFileException("SSL error: Could not read SSL public/private key "
  818. "from file(s) (error was: \"%s\")" % (e[0][0][2],))
  819. else:
  820. server = ThreadedHTTPServer((listenIp, self.port), handler,
  821. bind_and_activate=False)
  822. if withv6:
  823. server.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  824. server.server_bind()
  825. server.server_activate()
  826. return server
  827. def serve(self):
  828. self.handler = self._confAndFindHandler()
  829. self.server = []
  830. try:
  831. currsocktype = "IPv4"
  832. if self.listenIPv4:
  833. self.server.append(self._createServer(self.handler))
  834. currsocktype = "IPv6"
  835. if self.listenIPv6:
  836. self.server.append(self._createServer(self.handler, withv6=True))
  837. except socket.error as e:
  838. raise ServeFileException("Could not open %s socket: %s" % (currsocktype, e))
  839. if self.serveMode != self.MODE_UPLOAD:
  840. print("Serving \"%s\" at port %d." % (self.target, self.port))
  841. else:
  842. print("Serving \"%s\" for uploads at port %d." % (self.target, self.port))
  843. # print urls with local network adresses
  844. print("\nSome addresses %s will be available at:" %
  845. ("this file" if (self.serveMode != self.MODE_UPLOAD) else "the uploadform", ))
  846. ips = self.getIPs()
  847. if not ips or len(ips) == 0 or ips[0] == '':
  848. print("Could not find any addresses.")
  849. else:
  850. pwPart = ""
  851. if self.auth:
  852. pwPart = base64.b64decode(self.auth).decode() + "@"
  853. for ip in ips:
  854. if ":" in ip:
  855. ip = "[%s]" % ip
  856. print("\thttp%s://%s%s:%d/" % (self.useSSL and "s" or "", pwPart, ip, self.port))
  857. print()
  858. try:
  859. while True:
  860. (servers, _, _) = select.select(self.server, [], [])
  861. for server in servers:
  862. server.handle_request()
  863. except KeyboardInterrupt:
  864. for server in self.server:
  865. server.socket.close()
  866. # cleanup potential upload directory
  867. if self.dirCreated and len(os.listdir(self.target)) == 0:
  868. # created upload dir was not used
  869. os.rmdir(self.target)
  870. def _confAndFindHandler(self):
  871. handler = None
  872. if self.serveMode == self.MODE_SINGLE:
  873. try:
  874. testit = open(self.target, 'r')
  875. testit.close()
  876. except IOError as e:
  877. raise ServeFileException("Error: Could not open file, %r" % (str(e),))
  878. FileHandler.filePath = self.target
  879. FileHandler.fileName = os.path.basename(self.target)
  880. FileHandler.fileLength = os.stat(self.target).st_size
  881. handler = FileHandler
  882. elif self.serveMode == self.MODE_SINGLETAR:
  883. self.realTarget = os.path.realpath(self.target)
  884. if not os.path.exists(self.realTarget):
  885. raise ServeFileException("Error: Could not open file or directory.")
  886. TarFileHandler.target = self.realTarget
  887. TarFileHandler.fileName = os.path.basename(self.realTarget.rstrip("/")) + TarFileHandler.getCompressionExt()
  888. handler = TarFileHandler
  889. elif self.serveMode == self.MODE_UPLOAD:
  890. if os.path.isdir(self.target):
  891. print("Warning: Uploading to an already existing directory.")
  892. elif not os.path.exists(self.target):
  893. self.dirCreated = True
  894. try:
  895. os.mkdir(self.target)
  896. except (IOError, OSError) as e:
  897. raise ServeFileException("Error: Could not create directory '%s' for uploads, %r" %
  898. (self.target, str(e)))
  899. else:
  900. raise ServeFileException("Error: Upload directory already exists and is a file.")
  901. FilePutter.targetDir = os.path.abspath(self.target)
  902. FilePutter.maxUploadSize = self.maxUploadSize
  903. handler = FilePutter
  904. elif self.serveMode == self.MODE_LISTDIR:
  905. if not os.path.exists(self.target):
  906. raise ServeFileException("Error: Could not open file or directory.")
  907. if not os.path.isdir(self.target):
  908. raise ServeFileException("Error: '%s' is not a directory." % (self.target,))
  909. handler = DirListingHandler
  910. handler.targetDir = os.path.abspath(self.target)
  911. if self.auth:
  912. # do authentication
  913. AuthenticationHandler.authString = self.auth
  914. if self.authrealm:
  915. AuthenticationHandler.realm = self.authrealm
  916. class AuthenticatedHandler(AuthenticationHandler, handler):
  917. pass
  918. handler = AuthenticatedHandler
  919. if self.useSSL:
  920. # secure handler
  921. @catchSSLErrors
  922. class AlreadySecuredHandler(SecureHandler, handler):
  923. pass
  924. handler = AlreadySecuredHandler
  925. return handler
  926. class AuthenticationHandler():
  927. # base64 encoded user:password string for authentication
  928. authString = None
  929. realm = "Restricted area"
  930. def handle_one_request(self):
  931. """ Overloaded function to handle one request.
  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. authorized = False
  941. if "Authorization" in self.headers:
  942. if self.headers["Authorization"] == ("Basic " + self.authString):
  943. authorized = True
  944. if authorized:
  945. mname = 'do_' + self.command
  946. if not hasattr(self, mname):
  947. self.send_error(501, "Unsupported method (%r)" % self.command)
  948. return
  949. method = getattr(self, mname)
  950. method()
  951. else:
  952. self.send_response(401)
  953. self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm)
  954. self.send_header("Connection", "close")
  955. errorMsg = ("<html><head><title>401 - Unauthorized</title></head>"
  956. "<body><h1>401 - Unauthorized</h1></body></html>")
  957. self.send_header("Content-Length", str(len(errorMsg)))
  958. self.end_headers()
  959. self.wfile.write(errorMsg.encode())
  960. def main():
  961. parser = argparse.ArgumentParser(prog='servefile', description='Serve a single file via HTTP.')
  962. parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
  963. parser.add_argument('target', metavar='file/directory', type=str)
  964. parser.add_argument('-p', '--port', type=int, default=8080,
  965. help='Port to listen on')
  966. parser.add_argument('-u', '--upload', action="store_true", default=False,
  967. help="Enable uploads to a given directory")
  968. parser.add_argument('-s', '--max-upload-size', type=str,
  969. help="Limit upload size in kB. Size modifiers are allowed, e.g. 2G, 12MB, 1B")
  970. parser.add_argument('-l', '--list-dir', action="store_true", default=False,
  971. help="Show directory indexes and allow access to all subdirectories")
  972. parser.add_argument('--ssl', action="store_true", default=False,
  973. help="Enable SSL. If no key/cert is specified one will be generated")
  974. parser.add_argument('--key', type=str,
  975. help="Keyfile to use for SSL. If no cert is given with --cert the keyfile "
  976. "will also be searched for a cert")
  977. parser.add_argument('--cert', type=str,
  978. help="Certfile to use for SSL")
  979. parser.add_argument('-a', '--auth', type=str, metavar='user:password',
  980. help="Set user and password for HTTP basic authentication")
  981. parser.add_argument('--realm', type=str, default=None,
  982. help="Set a realm for HTTP basic authentication")
  983. parser.add_argument('-t', '--tar', action="store_true", default=False,
  984. help="Enable on the fly tar creation for given file or directory. "
  985. "Note: Download continuation will not be available")
  986. parser.add_argument('-c', '--compression', type=str, metavar='method',
  987. default="none",
  988. help="Set compression method, only in combination with --tar. "
  989. "Can be one of %s" % ", ".join(TarFileHandler.compressionMethods))
  990. parser.add_argument('-4', '--ipv4-only', action="store_true", default=False,
  991. help="Listen on IPv4 only")
  992. parser.add_argument('-6', '--ipv6-only', action="store_true", default=False,
  993. help="Listen on IPv6 only")
  994. args = parser.parse_args()
  995. maxUploadSize = 0
  996. # check for invalid option combinations/preparse stuff
  997. if args.max_upload_size and not args.upload:
  998. print("Error: Maximum upload size can only be specified when in upload mode.")
  999. sys.exit(1)
  1000. if args.upload and args.list_dir:
  1001. print("Error: Upload and dirlisting can't be enabled together.")
  1002. sys.exit(1)
  1003. if args.max_upload_size:
  1004. sizeRe = re.match(r"^(\d+(?:[,.]\d+)?)(?:([bkmgtpe])(?:(?<!b)b?)?)?$", args.max_upload_size.lower())
  1005. if not sizeRe:
  1006. print("Error: Your max upload size param is broken. Try something like 3M or 2.5Gb.")
  1007. sys.exit(1)
  1008. uploadSize, modifier = sizeRe.groups()
  1009. uploadSize = float(uploadSize.replace(",", "."))
  1010. sizes = ["b", "k", "m", "g", "t", "p", "e"]
  1011. maxUploadSize = int(uploadSize * pow(1024, sizes.index(modifier or "k")))
  1012. if maxUploadSize < 0:
  1013. print("Error: Your max upload size can't be negative")
  1014. sys.exit(1)
  1015. if args.ssl and not HAVE_SSL:
  1016. print("Error: SSL is not available, please install pyopenssl (python3-openssl).")
  1017. sys.exit(1)
  1018. if args.cert and not args.key:
  1019. print("Error: Please specify a key along with your cert.")
  1020. sys.exit(1)
  1021. if not args.ssl and (args.cert or args.key):
  1022. print("Error: You need to enable ssl with --ssl when specifying certs/keys.")
  1023. sys.exit(1)
  1024. if args.auth:
  1025. dpos = args.auth.find(":")
  1026. if dpos <= 0 or dpos == (len(args.auth)-1):
  1027. print("Error: User and password for HTTP basic authentication need to be both "
  1028. "at least one character and have to be separated by a \":\".")
  1029. sys.exit(1)
  1030. if args.realm and not args.auth:
  1031. print("You can only specify a realm when HTTP basic authentication is enabled.")
  1032. sys.exit(1)
  1033. if args.compression != "none" and not args.tar:
  1034. print("Error: Please use --tar if you want to tar everything.")
  1035. sys.exit(1)
  1036. if args.tar and args.upload:
  1037. print("Error: --tar mode will not work with uploads.")
  1038. sys.exit(1)
  1039. if args.tar and args.list_dir:
  1040. print("Error: --tar mode will not work with directory listings.")
  1041. sys.exit(1)
  1042. compression = None
  1043. if args.compression:
  1044. if args.compression in TarFileHandler.compressionMethods:
  1045. compression = args.compression
  1046. else:
  1047. print("Error: Compression mode '%s' is unknown." % args.compression)
  1048. sys.exit(1)
  1049. if args.ipv4_only and args.ipv6_only:
  1050. print("You can't listen both on IPv4 and IPv6 \"only\".")
  1051. sys.exit(1)
  1052. if args.ipv6_only and not socket.has_ipv6:
  1053. print("Your system does not support IPv6.")
  1054. sys.exit(1)
  1055. mode = None
  1056. if args.upload:
  1057. mode = ServeFile.MODE_UPLOAD
  1058. elif args.list_dir:
  1059. mode = ServeFile.MODE_LISTDIR
  1060. elif args.tar:
  1061. mode = ServeFile.MODE_SINGLETAR
  1062. else:
  1063. mode = ServeFile.MODE_SINGLE
  1064. server = None
  1065. try:
  1066. server = ServeFile(args.target, args.port, mode, args.ssl)
  1067. if maxUploadSize > 0:
  1068. server.setMaxUploadSize(maxUploadSize)
  1069. if args.ssl and args.key:
  1070. cert = args.cert or args.key
  1071. server.setSSLKeys(cert, args.key)
  1072. if args.auth:
  1073. user, password = args.auth.split(":", 1)
  1074. server.setAuth(user, password, args.realm)
  1075. if compression and compression != "none":
  1076. server.setCompression(compression)
  1077. if args.ipv4_only or not socket.has_ipv6:
  1078. server.setIPv6(False)
  1079. if args.ipv6_only:
  1080. server.setIPv4(False)
  1081. server.serve()
  1082. except ServeFileException as e:
  1083. print(e)
  1084. sys.exit(1)
  1085. print("Good bye.")
  1086. if __name__ == '__main__':
  1087. main()