#!/usr/bin/env python2 """ A simple read-only WebDAV and web server with hub-style remote access support, ideal for sharing files over the internet. The web server's root directory is a list of "shares", which allow access to directories on the server's file system. In addition, an HTShare server (then called an "uplink") can register with another HTShare server (a "hub"). It will then appear as another share on the hub, and the hub will relay all accesses to that share to the uplink server. All TCP connections are initiated by the uplink; even if the uplink is behind a NAT or firewall that blocks incoming connections, it will still be possible to access it through the hub. Usage: htshare [OPTIONS...] [SHARES/HUBS...] General options: -h, -help Show a help text exit. -config Read further options from a text file. -log Enable one or more comma-separated log levels: access - brief access.log-like request logging [default] http - full HTTP request header logging hub - hub register/deregister logging conn - connection state logging -logfile File to write log information to. [default: stdout] -port TCP port to listen on [default: 8005] -nolisten Don't start a HTTP server at all, only connect to hubs. -nohub Don't allow uplinks to use this server as a hub. -encoding Override the encoding used for file names. -ping Override the hub<->uplink ping interval. [default: 60] Security options: -vhost Only react to HTTP requests for a specific virtual host. -rootpw Set the server's root directory password. -hubpw Set the password required to register with this hub. -pw Set root and hub password at once. -passwd Encode a password and exit. (Encoded passwords may be used instead of plaintext paswords on the command line and in config files. Note that this is NOT a kind of encryption! It just prevents users from accidentally reading passwords while examining the command line or config file.) Daemon options: -D, -daemonize Run as a daemon. -pidfile File to write the process ID into. -setuid Run as a different user (UID or user name). -setgid Run as a different group (GID or group name). -install Install as a service that runs at system start. -uninstall Uninstall the service. Syntax for specifying a share: [.][:]= or [:] The second form uses the last part of the local path as the share name. If the share name starts with a period ('.'), the share will be hidden. Spaces may be put around the ':' and '=' characters. Syntax for specifying a hub: [htshare://][[.][:]@][:] If the uplink name is omitted, the local host name will be used. If the uplink name starts with a period ('.'), the server will be hidden. Spaces may be put around the ':' and '@' characters. """ __version__ = "1.1.0" __author__ = "Martin J. Fiedler " __copyright__ = """ This software is published under the terms of KeyJ's Research License, version 0.2. Usage of this software is subject to the following conditions: 0. There's no warranty whatsoever. The author(s) of this software can not be held liable for any damages that occur when using this software. 1. This software may be used freely for both non-commercial and commercial purposes. 2. This software may be redistributed freely as long as no fees are charged for the distribution and this license information is included. 3. This software may be modified freely except for this license information, which must not be changed in any way. 4. If anything other than configuration, indentation or comments have been altered in the code, the original author(s) must receive a copy of the modified code. """ __history__ = """ 1.1.0 [2017-03-22] - added basic WebDAV support 1.0.1 [2016-03-29] - added ping timeout detection on uplink side; should help with unilateral hub connection losses on some systems - made hub<->uplink ping interval adjustable - fixed logrotate configuration in installer 1.0.0 [2013-03-24] - initial version """ import sys, os, stat, time, calendar, socket, threading, urllib, base64, re import random, hashlib, uuid, getpass, subprocess, tempfile DEFAULT_PORT = 8005 PROTOCOL_VERSION = "HTSHARE/1.1" SERVER_VERSION = "HTShare/%s Python/%s" % (__version__, sys.version.split(' ', 1)[0]) DIRLIST_HTML_PREFIX = """ Directory Listing """ DIRLIST_HTML_SUFFIX = """
""" VFS_PRESET_FILES = { "style.css": ("text/css", """ body { background: #f8f8f8; padding: 0; margin: 0; } h1 { margin-left: 8px; font-size:200%; } #dirlist { width: 100%; border-collapse:collapse; margin: 8px 0 8px 0; } #dirlist td, #dirlist th { white-space: nowrap; border: 0; margin: 0; padding: 2px; } #dirlist tr:hover { background: #f0f0f0; } #dirlist .name { width: 100%; padding-left:8px; } #dirlist .size, #dirlist .mtime { text-align: right; } #dirlist .dir { text-align: center; } #dirlist .size, #dirlist .mtime, #dirlist .dir { padding-right:8px; } a[target] { target-new: tab; } a { color: #00a; text-decoration: none; } a:visited { color: #80a; } a:hover { color: #44f; } """) } INSTALL_PATH = "/usr/local/bin/htshare" INSTALL_FILES = [ ("/etc/htshare.conf", "configuration file", """ ### /etc/htshare.conf: The configuration file for the HTShare HTTP server / hub. ### For more information, please run 'htshare -help'. ##### GENERAL SERVER SETUP ##### ### -port: Set the TCP port on which HTShare shall listen for HTTP requests. ### The default port is 8005. # -port 8005 ### -nolisten: Disable listening for HTTP altogether. ### In this mode, HTShare will still connect to upstream hubs and serve ### requests for their clients, but it will not start a local HTTP server ### itself. # -nolisten ### -nohub: Disallow HTShare uplink registration. ### By default, every HTShare server can be used as a hub, i.e. other HTShare ### servers ("uplinks") may create a remote virtual directory there. With the ### '-nohub' option enabled, only local files can be served and uplink ### registrations are rejected. # -nohub ### -encoding: Override the character encoding used for the local file system. ### On some systems, character set auto detection fails, but using this option, ### non-ASCII characters can be made working anyway. # -encoding utf-8 ##### LOGGING AND LOCAL SECURITY ##### ### -log: Set the log levels to log; run 'htshare -help' for details. ### The default is 'access', which generates a brief access log like other ### HTTP servers do. # -log access ### -logfile: Set the filename into which logging shall be done. -logfile /var/log/htshare.log ### -setuid / -setgid: Run HTShare with different credentials. ### It is recommended to run HTShare under a restricted user account, otherwise ### it will run as root. # -setuid nobody # -setgid nogroup ##### PASSWORDS AND REMOTE SECURITY ##### ### -rootpw: Set the password that HTTP clients (or remote clients coming from ### a HTShare hub) have to enter to see the root directory listing. ### It is recommended to use an "encoded" password here, which can be created ### by running 'htshare -passwd'. # -rootpw secret123 ### -hubpw: Set the password that HTShare uplink servers have to submit in ### order to create a remote directory on this server. ### It is recommended to use an "encoded" password here, which can be created ### by running 'htshare -passwd'. # -hubpw secret123 ### -vhost: Only respond to HTTP request that refer to a specific virtual host ### ('Host' header in HTTP). # -vhost localhost ##### LOCAL SHARES ##### ### Each share is displayed as a virtual directory in the root directory of ### this server (or in the server's subdirectory when seen connected through a ### HTShare hub) and maps to a directory on the local file system. ### The recommended syntax to specify a share is ### [.][:]= ### Where "share name" is name of the virtual directory to be created, ### "password" is the (optional) password that is required for accessing the ### files within the share and "local path" is the path in the local file ### system. The password, if present, should be encoded using 'htshare -passwd'. ### A leading period in the share name indicates that the directory shall be ### hidden, i.e. not mentioned in the root directory listing, but still ### accessible when the path is entered directly (*without* the period). ### Example: # Home Directories : secret123 = /home ##### HTSHARE HUB UPLINKS ##### ### The rewcommended syntax to specify a hub is ### [.][:]@[:] ### Where "uplink name" is the name of the virtual directory to be created for ### this uplink on the HTShare hub and "password" is the (optional) password ### that is required by the hub for registering the uplink. If the port number ### is absent, the standard port (8005) is assumed. ### A leading period in the uplink name indicates that the directory shall be ### hidden, i.e. not mentioned in the root directory listing, but still ### accessible when the path is entered directly (*without* the period). ### Example: # My Computer : secret123 @ htshare.example.com """), ("/etc/init.d/htshare", "startup script", """ #!/bin/sh ### BEGIN INIT INFO # Provides: htshare # Required-Start: $remote_fs $network $syslog $named # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Simple HTTP Server and hub. # Description: An HTTP server optimized for simple file sharing with # integrated "hub" functionality. ### END INIT INFO set -e NAME="htshare" DAEMON="/usr/local/bin/htshare" CONFIGFILE="/etc/htshare.conf" PIDFILE="/var/run/htshare.pid" . /lib/lsb/init-functions case "$1" in start) PID=`cat "$PIDFILE" 2>/dev/null` || true if [ -n "$PID" ] ; then if ps "$PID" >/dev/null 2>&1 ; then echo "$NAME is still running" exit 1 fi fi log_daemon_msg "Starting HTShare server" "$NAME" "$DAEMON" -c "$CONFIGFILE" -pidfile "$PIDFILE" -daemon log_end_msg $? ;; stop) PID=`cat "$PIDFILE" 2>/dev/null` || true if [ -n "$PID" ] ; then if ps "$PID" >/dev/null 2>&1 ; then log_daemon_msg "Stopping HTShare server" "$NAME" kill -2 "$PID" sleep 1 if ps "$PID" >/dev/null 2>&1 ; then kill "$PID" || log_end_msg 1 fi log_end_msg 0 exit 0 fi fi echo "HTShare is not running." exit 1 ;; restart|force-reload) $0 stop $0 start ;; status) status_of_proc -p "$PIDFILE" "$DAEMON" "$NAME" exit $? ;; *) echo "Usage: $0 {start|stop|restart|force-reload|status}" exit 1 ;; esac """), ("/etc/logrotate.d/htshare", "log rotator configuration", """ /var/log/htshare.log { weekly missingok notifempty compress delaycompress copytruncate rotate 4 } """) ] ############################################################################### CONTENT_TYPES_IN = """ txt diz sh c cpp h py rb csv diff patch java p pas pl pm tcl tk tex sty cls: text/plain xml: application/xml htm html: text/html js: application/javascript eml: message/rfc822 ics: text/calendar css: text/css gif: image/gif jpg jpeg jpg: image/jpeg png: image/png bmp: image/x-ms-bmp ico: vnd.microsoft.icon pdf: application/pdf ps eps: application/postscript rar: application/rar zip: application/zip 7z: application/x-7z-compressed msi: application/x-msi mid midi: audio/midi ogg oga: audio/ogg mpa mp2 mp3 m4a: audio/mpeg 3gp: video/3gpp mpg mpv mpeg: video/mpeg mp4: video/mp4 mov qt: video/quicktime ogv: video/ogg webm: video/webm flv: video/x-flv wmv: video/x-ms-wmv mkv: video/x-matroska """ CONTENT_TYPES = {} for line in CONTENT_TYPES_IN.strip().split('\n'): exts, content_type = map(str.strip, line.split(':')) for ext in filter(None, exts.split()): CONTENT_TYPES[ext] = content_type ############################################################################### HTTP_RESPONSE_CODES = { 100: "Continue", 101: "Switching Protocols", 200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 250: "Registered", 251: "Ping Received", 300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 400: "Bad Request", 401: "Authorization Required", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 406: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Request Entity Too Large", 414: "Request-URI Too Long", 415: "Unsupported Media Type", 416: "Requested Range Not Satisfiable", 417: "Expectation Failed", 450: "Name In Use", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported" } RFC822_WEEKDAY_NAMES = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") RFC822_MONTH_NAMES = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") RFC822_MONTH_LOOKUP = dict([(RFC822_MONTH_NAMES[i].lower(), i+1) for i in xrange(12)]) _re_time = re.compile(r'[a-z]+,\s*(\d+)\s+([a-z]+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+gmt$', re.I) def http_time(t=None): if not t: t = time.time() t = time.gmtime(int(t + 0.99)) return "%s, %02d %s %d %02d:%02d:%02d GMT" % \ (RFC822_WEEKDAY_NAMES[t.tm_wday], t.tm_mday, \ RFC822_MONTH_NAMES[t.tm_mon - 1], t.tm_year, \ t.tm_hour, t.tm_min, t.tm_sec) def http_parse_time(s): m = _re_time.match(s) if not m: return 0 m = list(m.groups()) month = RFC822_MONTH_LOOKUP.get(m.pop(1).lower(), 0) if not month: return 0 m = map(int, m) return calendar.timegm((m[1], month, m[0], m[2], m[3], m[4], -1, -1, -1)) def H(s): if type(s) == unicode: s = s.encode('utf-8', 'replace') return s.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') def Q(s): if type(s) == unicode: s = s.encode('utf-8', 'replace') s = urllib.quote(s) return ''.join([(c if (c < '\x7f') else ("%02x" % ord(c))) for c in s]) SystemID = [] SystemID.append(getpass.getuser()) SystemID.append(socket.gethostname()) SystemID.append("%012X" % uuid.getnode()) SystemID = hashlib.md5('@'.join(SystemID)).hexdigest() FSEncoding = sys.getfilesystemencoding() def from_fs(s): return unicode(s, FSEncoding, 'replace') def to_fs(s): if type(s) == str: s = unicode(s, 'utf-8', 'replace') return s.encode(FSEncoding, 'replace') try: import ctypes GetFileAttributesW = ctypes.windll.kernel32.GetFileAttributesW def is_hidden_file(filename): return GetFileAttributesW(unicode(filename)) & 2 except (ImportError, AttributeError): def is_hidden_file(filename): return False ############################################################################### LOG_LEVELS = [] LOG_FILE = sys.stdout _log_lock = threading.Lock() def log(level, message): global LOG_LEVELS, _log_lock if level and not(level in LOG_LEVELS): return if not(isinstance(message, basestring)): message = message() message = map(str.strip, message.strip().split('\n')) prefix = "[%s] " % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) _log_lock.acquire() try: for line in message: print >>LOG_FILE, prefix + line prefix = "| " finally: LOG_FILE.flush() _log_lock.release() ############################################################################### class Connection(object): def __init__(self, sock, addr, owner=None): self.sock = sock if type(addr) == str: self.addr = addr else: self.addr = "%s:%d" % addr self.owner = owner self.rbuf = "" def read(self, max_bytes): if not self.sock: data = self.rbuf self.rbuf = "" return data if self.rbuf: data = self.rbuf[:max_bytes] self.rbuf = self.rbuf[max_bytes:] return data try: return self.sock.recv(max_bytes) except (socket.error, IOError, EOFError): return "" def readall(self, bytes): if not self.sock: data = self.rbuf self.rbuf = "" return data while len(self.rbuf) < bytes: try: block = self.sock.recv(bytes - len(self.rbuf)) except (socket.error, IOError, EOFError): block = None if not block: break self.rbuf += block data = self.rbuf[:bytes] self.rbuf = self.rbuf[bytes:] return data def readline(self): while self.sock and not('\n' in self.rbuf): try: data = self.sock.recv(4096) self.rbuf += data if not data: break except (socket.error, IOError, EOFError): break try: eol = self.rbuf.index('\n') + 1 data = self.rbuf[:eol] self.rbuf = self.rbuf[eol:] return data except ValueError: data = self.rbuf self.rbuf = "" return data def write(self, data): if not self.sock: return -1 try: self.sock.sendall(data) return len(data) except (socket.error, IOError, EOFError): return -1 def close(self): if self.sock: self.sock.close() self.sock = None def shutdown(self): if self.sock: try: self.sock.shutdown(socket.SHUT_RDWR) except (socket.error, IOError, EOFError): pass def __del__(self): self.close() ############################################################################### class HTTPHeaders(object): def __init__(self): self.headers = [] self.hindex = {} def read(self, conn): self.headers = [] while True: line = conn.readline().strip() if not line: break try: key, value = map(str.strip, line.split(':', 1)) except ValueError: key = line value = "" self.headers.append((key, value)) self._rebuild_index() return self @staticmethod def _makekey(k): return k.replace('_', '').replace('-', '').lower() def _rebuild_index(self): self.hindex = {} for i in xrange(len(self.headers)): k, v = self.headers[i] self.hindex[self._makekey(k)] = i def __getitem__(self, key): try: return self.headers[self.hindex[self._makekey(key)]][1] except IndexError: raise KeyError def get(self, key, default=None): try: return self[key] except KeyError: return default def __setitem__(self, key, value): kk = self._makekey(key) try: i = self.hindex[kk] self.headers[i] = (key, value) except KeyError: self.headers.append((key, value)) self.hindex[kk] = len(self.headers) - 1 def __delitem__(self, key): del self.headers[self.hindex[self._makekey(key)]] self._rebuild_index() def __nonzero__(self): return not(not(self.method and self.uri and self.version)) def __len__(self): return len(self.headers) def __iter__(self): for k, v in self.headers: yield k def iterkeys(self): for k, v in self.headers: yield k def itervalues(self): for k, v in self.headers: yield k def iteritems(self): for k, v in self.headers: yield (k, v) def __iadd__(self, line): k, v = map(str.strip, line.split(':', 1)) self[k] = v return self def weak_set(self, key, value): kk = self._makekey(key) if not(kk in self.hindex): self.headers.append((key, value)) self.hindex[kk] = len(self.headers) - 1 def __str__(self): return ''.join("%s: %s\r\n" % item for item in self.headers) + "\r\n" def write(self, conn): return conn.write(str(self)) class Request(HTTPHeaders): def __init__(self, method="", uri="", version=""): HTTPHeaders.__init__(self) self.data = None if not(uri) and not(version): self._parse_request(method) else: self.method = method self.uri = uri self.full_uri = uri self.version = version def _parse_request(self, request): request = request.strip() try: self.method, request = map(str.strip, request.split(' ', 1)) except ValueError: self.method = request rest = "" try: self.uri, self.version = map(str.strip, request.rsplit(' ', 1)) except ValueError: self.uri = request self.version = "" self.uri = urllib.unquote(self.uri) self.full_uri = self.uri def read(self, conn): self._parse_request(conn.readline()) if self: HTTPHeaders.read(self, conn) try: clen = int(self.get("Content-Length", "0")) except ValueError: clen = 0 if clen: self.data = conn.readall(clen) full_uri = self.get("X-HTShare-Full-URI") if full_uri: self.full_uri = full_uri log('http', lambda: "HTTP request from %s:\n%s" % (conn.addr, str(self))) else: log('http', lambda: "HTTP request from %s timed out" % conn.addr) return self def send(self, conn): log('http', lambda: "HTTP request to %s:\n%s" % (conn.addr, str(self))) return conn.write(str(self)) def pop_root(self): uri = self.uri if uri.startswith('/'): uri = uri[1:] try: root, uri = uri.split('/', 1) except ValueError: root = uri uri = "" self.uri = '/' + uri return root def get_password(self): auth = self.get("Authorization") if not(auth) or not(auth.lower().startswith("basic")): return None try: return base64.b64decode(auth[5:].strip()).split(':', 1)[-1] except ValueError: return None def check_password(self, pwd): return not(pwd) or (pwd == self.get_password()) def __str__(self): return "%s %s %s\r\n" % (self.method, Q(self.uri), self.version) + HTTPHeaders.__str__(self) + (self.data or "") class Response(HTTPHeaders): def __init__(self, request_or_version=None, code=200): HTTPHeaders.__init__(self) self.suppress_data = False if not request_or_version: self.version = "HTTP/1.1" elif type(request_or_version) != str: self.version = request_or_version.version self.suppress_data = (request_or_version.method == "HEAD") else: self.version = request_or_version self.code = code self["Date"] = http_time() self["Server"] = SERVER_VERSION self["Connection"] = "close" def get_code_str(self): return "%d %s" % (self.code, HTTP_RESPONSE_CODES.get(self.code, "Other")) def __nonzero__(self): return not(not(self.code)) def __str__(self): return "%s %s\r\n" % (self.version, self.get_code_str()) + HTTPHeaders.__str__(self) def cache_control(self, req, mtime): """set the Last-Modified hader and generate a 304 Not Modified response in case of a cache hit""" self["Last-Modified"] = http_time(mtime) if req.method != "GET": return False checktime = req.get("If-Modified-Since") if not checktime: return False checktime = http_parse_time(checktime) if mtime < checktime: self.code = 304 return True else: return False def read(self, conn): res = conn.readline().strip() try: self.version, code, dummy = (res + " dummy").split(' ', 2) self.code = int(code) except ValueError: self.code = 0 HTTPHeaders.read(self, conn) log('http', lambda: "HTTP response from %s:\n%s" % (conn.addr, str(self))) return self def send(self, conn, document=None): if self.code == 304: document = "" self.suppress_data = True if self.code == 401: self.weak_set("WWW-Authenticate", "Basic realm=\"HTShare password (user name is ignored)\"") if document: self.weak_set("Content-Type", "text/html; charset=utf-8") self.weak_set("Content-Length", str(len(document))) if self.suppress_data: document = "" else: document = "" log('http', lambda: "HTTP response to %s:\n%s" % (conn.addr, str(self))) return conn.write(str(self) + document) def send_with_message(self, conn): self.send(conn, "HTTP Response

%s

\r\n" % self.get_code_str()) ############################################################################### _rt_list = [] _rt_lock = None class RegisteredThread(threading.Thread): def __init__(self, name): global _rt_list, _rt_lock threading.Thread.__init__(self, name=name) if not _rt_lock: _rt_lock = threading.Lock() def run(self): global _rt_list, _rt_lock _rt_lock.acquire() try: _rt_list.append(self) finally: _rt_lock.release() try: self.main() finally: _rt_lock.acquire() try: try: _rt_list.remove(self) except ValueError: pass finally: _rt_lock.release() def main(self): pass def shutdown(self): pass def close(self, timeout=None): self.shutdown() return self.join(timeout) @classmethod def find_rid(self, rid): global _rt_list, _rt_lock # TODO: use a hash to look this up _rt_lock.acquire() try: for t in _rt_list: if getattr(t, 'rid', None) == rid: return t finally: _rt_lock.release() @classmethod def close_all(self): global _rt_list, _rt_lock while _rt_list: _rt_list[0].close() ############################################################################### PARENT_DIRECTORY = -3 SUBDIRECTORY = -2 REMOTE_DIRECTORY = -1 NORMAL_FILE = 0 WEBDAV_MULTISTATUS_PREFIX = '\n\n' WEBDAV_MULTISTATUS_SUFFIX = '' def WebDAVResponse(uri, size=0, mtime=0, is_dir=False): s = "%s" % uri if is_dir: s += "" else: s += "" if size >= 0: s += "%d" % size if mtime: s += "%s" % http_time(mtime) return s + "HTTP/1.1 200 OK\n" class DirItem(object): def __init__(self, name, size=0, mtime=None, direct_url=None): self.name = name self.size = size self.mtime = mtime self.direct_url = direct_url if size < 0: self.name += '/' self.href = Q(self.name) @classmethod def from_filesystem(self, dirname, basename=None): if dirname and basename: filename = os.path.join(dirname, basename) else: filename = dirname or basename dirname, basename = os.path.split(filename) if (basename[0] in ".~$") or basename.endswith('~') or is_hidden_file(filename): raise OSError() s = os.stat(to_fs(filename)) size = s.st_size mtime = s.st_mtime if stat.S_ISDIR(s.st_mode): size = SUBDIRECTORY elif not stat.S_ISREG(s.st_mode): raise OSError() return self(os.path.basename(filename), size, mtime) def to_html(self): s = "%s" % (self.href, H(self.name)) if self.direct_url: s += " (direct connection)" % self.direct_url s += "" if self.size >= 0: sizestr = str(self.size) i = len(sizestr) - 3 while i > 0: sizestr = sizestr[:i] + ',' + sizestr[i:] i -= 3 s += "%s" % sizestr if self.mtime: s += "%s" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.mtime)) else: s += " " else: s += "(%s)" % { SUBDIRECTORY: "Subdirectory", PARENT_DIRECTORY: "Parent Directory", REMOTE_DIRECTORY: "Remote Directory" }.get(self.size, "Directory") return s + "\n" def to_davxml(self, basedir=""): if self.size == PARENT_DIRECTORY: return "" return WebDAVResponse(basedir + self.href, self.size, self.mtime, self.size < 0) def is_a(self, typecode): return min(self.size, 0) == typecode def __cmp__(self, other): return (min(self.size, 0) - min(other.size, 0)) \ or cmp(self.name.lower(), other.name.lower()) def __repr__(self): return "DirItem(%r, %r, %r)" % (self.name, self.size, self.mtime) def RespondWithDirectoryListing(res, conn, req, items): if req.method == "PROPFIND": basedir = req.full_uri if not basedir.endswith('/'): basedir += '/' res.code = 207 res["Content-Type"] = "text/xml; charset=utf-8" if req.get("Depth") == "0": return res.send(conn, WEBDAV_MULTISTATUS_PREFIX + WebDAVResponse(basedir, is_dir=True) + WEBDAV_MULTISTATUS_SUFFIX) else: return res.send(conn, WEBDAV_MULTISTATUS_PREFIX + WebDAVResponse(basedir, is_dir=True) + ''.join(item.to_davxml(basedir) for item in items) + WEBDAV_MULTISTATUS_SUFFIX) else: return res.send(conn, DIRLIST_HTML_PREFIX + ''.join([item.to_html() for item in sorted(items)]) + DIRLIST_HTML_SUFFIX) ############################################################################### FORCE_NO_PASSWORD = -1 class VFSObject(object): def __init__(self, hidden=False, password=None): self.hidden = hidden self.password = password self.direct_url = None self.sysid = "INVALID_SYSID" def get_password(self): global RootPassword return (self.password != FORCE_NO_PASSWORD) and (self.password or RootPassword) class VFSFile(VFSObject): def __init__(self, content_type, content, hidden=True, password=FORCE_NO_PASSWORD): VFSObject.__init__(self, hidden, password) self.content_type = content_type self.content = content def handle_download_request(self, conn, req): global LastProgramChange res = Response(req, 200) if res.cache_control(req, LastProgramChange): return res.send(conn) res["Content-Type"] = self.content_type res.send(conn, self.content) handle_HEAD = handle_download_request handle_GET = handle_download_request def vfs_updated(): global LastVFSChange LastVFSChange = time.time() VFS = dict([(name, VFSFile(*data)) for name, data in VFS_PRESET_FILES.iteritems()]) RootPassword = None HubPassword = None RestrictVHost = None NoHub = False PingInterval = 60 try: LastProgramChange = os.stat(sys.argv[0]).st_mtime except OSError: LastProgramChange = 0 LastVFSChange = LastProgramChange ############################################################################### class VFSDirectory(VFSObject): def __init__(self, root, hidden=False, password=None): VFSObject.__init__(self, hidden, password) self.root = unicode(root, sys.getfilesystemencoding(), 'replace') def handle_download_request(self, conn, req): fullpath = os.path.normpath(os.path.join(self.root, unicode(req.uri.lstrip('/'), 'utf-8', 'replace'))) try: s = os.stat(to_fs(fullpath)) except OSError: return Response(req, 404).send_with_message(conn) res = Response(req, 200) if res.cache_control(req, s.st_mtime): return res.send(conn) # handle directory if stat.S_ISDIR(s.st_mode): items = [DirItem("..", PARENT_DIRECTORY)] try: raw_items = map(from_fs, os.listdir(to_fs(fullpath))) except OSError: return Response(req, 500).send_with_message(conn) for item in raw_items: try: items.append(DirItem.from_filesystem(fullpath, item)) except OSError: pass return RespondWithDirectoryListing(res, conn, req, items) # handle WebDAV request for file if req.method == "PROPFIND": res.code = 207 res["Content-Type"] = "text/xml; charset=utf-8" return res.send(conn, WEBDAV_MULTISTATUS_PREFIX + WebDAVResponse(req.uri) + WEBDAV_MULTISTATUS_SUFFIX) # handle regular file if not stat.S_ISREG(s.st_mode): return Response(req, 404).send_with_message(conn) try: f = open(to_fs(fullpath), "rb") except IOError: return Response(req, 401).send_with_message(conn) # parse the range header offset = 0 bytes = s.st_size res["Accept-Ranges"] = "bytes" byterange = req.get("Range") if byterange and byterange.lower().startswith("bytes="): try: start, end = map(str.strip, byterange[6:].split(',', 1)[0].split('-')) assert start or end if start and end: start = int(start) end = int(end) elif start: start = int(start) end = bytes elif end: start = bytes - int(end) end = bytes if end >= bytes: end = bytes - 1 offset = start bytes = end - start + 1 try: f.seek(offset) except IOError: f.close() return Response(req, 400).send_with_message(conn) res["Content-Range"] = "bytes %d-%d/%d" % (start, end, s.st_size) res.code = 206 except (AssertionError, ValueError): pass # start the main transmission res["Content-Type"] = CONTENT_TYPES.get(os.path.splitext(fullpath)[-1].lstrip('.').lower(), "application/octet-stream") res["Content-Length"] = str(bytes) res.send(conn) if req.method == "HEAD": f.close() return while bytes: data = f.read(min(bytes, 4096)) if not data: break if conn.write(data) < 0: log('conn', "client connection from %s closed unexpectedly" % conn.addr) break bytes -= len(data) f.close() handle_HEAD = handle_download_request handle_GET = handle_download_request handle_PROPFIND = handle_download_request def handle_OPTIONS(self, conn, req): res = Response(req, 200) res["Allow"] = "OPTIONS, GET, HEAD, PROPFIND" res["DAV"] = "1" return res.send(conn) ############################################################################### class DirectAccessChecker(RegisteredThread): def __init__(self, vfsobj, host, port): RegisteredThread.__init__(self, "DirectCheck-%s-%d" % (host, port)) self.vfsobj = vfsobj self.host = host self.port = port self.sock = None self.shutdown_pending = False def main(self): try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) self.sock.connect((self.host, self.port)) conn = Connection(self.sock, "%s:%d" % (self.host, self.port), self) req = Request("VERIFY", self.vfsobj.sysid, PROTOCOL_VERSION) if req.send(conn) < 0: self.sock.close() self.sock = None return res = Response().read(conn) if res.code == 200: self.vfsobj.direct_url = "http://%s:%d/" % (self.host, self.port) vfs_updated() log('hub', "uplink %r is directly reachable via %s" % (self.vfsobj.remote.name, self.vfsobj.direct_url)) except (socket.error, IOError, EOFError): return def shutdown(self): self.shutdown_pending = True if self.sock: self.sock.shutdown() class VFSRemote(VFSObject): def __init__(self, remote, sysid=None, direct_url=None, hidden=False): VFSObject.__init__(self, hidden, FORCE_NO_PASSWORD) self.remote = remote self.sysid = sysid self.conn = None self.direct_url = direct_url def check_direct(self, host, port): DirectAccessChecker(self, host, port).start() def handle_request(self, conn, req): # generate request and associate it with the owner thread conn.owner.rid = self.remote.request(req, conn.addr) # wait for uplink to connect dt = 1.0 / 128 tw = 0.0 while not conn.owner.uplink: if tw > 15.0: return Response(req, 504).send_with_message(conn) time.sleep(dt) tw += dt dt += 1.0 / 128 # transfer data uplink = conn.owner.uplink while True: data = uplink.read(4096) if not data: break if conn.write(data) < 0: break class RemoteHandler(object): def __init__(self, name, owner): self.name = name self.owner = owner self.lock = threading.Lock() def run(self, conn): res = Response(PROTOCOL_VERSION, 250) res["Ping-Interval"] = str(PingInterval) res.send(conn) ping_countdown = 0 while not self.owner.shutdown_pending: if ping_countdown <= 0: ping_countdown = PingInterval self.lock.acquire() try: if Request("PING", "-", PROTOCOL_VERSION).send(conn) < 0: break finally: self.lock.release() res = Response().read(conn) if not res: log('conn', "connection to %s timed out" % conn.addr) return if res.code != 251: log('conn', "invalid return code %d on uplink connection to %s" % (res.code, conn.addr)) return time.sleep(1) ping_countdown -= 1 def request(self, req, addr=None): rid = ''.join([random.choice("abcdefghijklmnopqrstuvwxyz") for x in xrange(16)]) self.lock.acquire() try: req["X-HTShare-Request-ID"] = rid req["X-HTShare-Full-URI"] = req.full_uri if addr: req["X-Forwarded-For"] = addr if req.send(self.owner.conn) < 0: self.owner.shutdown() finally: self.lock.release() return rid ############################################################################### ServerPort = None class ClientThread(RegisteredThread): def __init__(self, conn, preset_request=None): RegisteredThread.__init__(self, "Client-%s" % conn.addr.replace('.', '-').replace(':', '-')) self.conn = conn self.conn.owner = self self.preset_request = preset_request self.rid = None self.uplink = None self.shutdown_pending = False def main(self): log('conn', "handling client connection from %s" % self.conn.addr) self.process_request() if self.conn: log('conn', "client connection from %s handled" % self.conn.addr) self.conn.close() if self.uplink: self.uplink.close() def get_access_log_tag(self, req): conn_addr = self.conn.addr.rsplit(':', 1)[0] real_addr = req.get("X-Forwarded-For", "").rsplit(':', 1)[0] if real_addr: return "%s (via %s)" % (real_addr, conn_addr) return conn_addr def process_request(self): global VFS, RootPassword, RestrictVHost, SystemID req = self.preset_request if req: log('req', "actual HTTP request from %s:\n%s" % (self.conn.addr, str(req))) else: req = Request().read(self.conn) # handle HTShare requests if req.version.startswith("HTSHARE"): handler = getattr(self, "handle_htshare_" + req.method, None) if not handler: return Response(req, 400).send(self.conn) return handler(req) # filter by host if RestrictVHost and (req.get("Host", "").rsplit(':', 1)[0].strip().lower() != RestrictVHost): return # don't even send a response # log access log('access', lambda: "%s - %s %r" % (self.get_access_log_tag(req), req.method, req.uri)) # handle OPTIONS for root if (req.method == "OPTIONS") and (req.uri == "/"): res = Response(req) res["Allow"] = "OPTIONS, GET, HEAD, PROPFIND" res["DAV"] = "1" return res.send(self.conn) # generate root directory listing if (req.method in ("GET", "HEAD", "PROPFIND")) and (req.uri == "/"): if not req.check_password(RootPassword): return Response(req, 401).send_with_message(self.conn) res = Response(req, 200) if res.cache_control(req, LastVFSChange): return res.send(self.conn) items = [] if req.get("X-HTShare-Request-ID"): items.append(DirItem("..", PARENT_DIRECTORY)) for name, vfsobj in VFS.iteritems(): if vfsobj.hidden: continue if type(vfsobj) == VFSDirectory: items.append(DirItem(name, SUBDIRECTORY)) elif type(vfsobj) == VFSFile: items.append(DirItem(name, len(vfsobj.content))) elif type(vfsobj) == VFSRemote: items.append(DirItem(name, REMOTE_DIRECTORY, direct_url=vfsobj.direct_url)) return RespondWithDirectoryListing(res, self.conn, req, items) # handle normal object root = req.pop_root() if not(root in VFS): return Response(req, 404).send_with_message(self.conn) vfsobj = VFS[root] if not req.check_password(vfsobj.get_password()): return Response(req, 401).send_with_message(self.conn) handler = getattr(vfsobj, "handle_" + req.method, None) if handler: return handler(self.conn, req) handler = getattr(vfsobj, "handle_request", None) if handler: return handler(self.conn, req) return Response(req, 400).send_with_message(self.conn) def handle_htshare_REPLY(self, req): downlink = RegisteredThread.find_rid(req.uri) if not downlink: return # request timed out already log('conn', "handing over client connection from %s to %s" % (self.conn.addr, downlink.conn.addr)) downlink.uplink = self.conn self.conn = None def handle_htshare_VERIFY(self, req): if req.uri == SystemID: return Response(req, 200).send(self.conn) else: return Response(req, 404).send(self.conn) def handle_htshare_REGISTER(self, req): if NoHub: log('access', "%s - REGISTER %r rejected" % (self.conn.addr, req.uri)) return Response(req, 403).send(self.conn) name = req.uri if not req.check_password(HubPassword): log('access', "%s - REGISTER %r rejected (password mismatch)" % (self.conn.addr, req.uri)) return Response(req, 401).send(self.conn) log('hub', "incoming uplink from %s with name %r" % (self.conn.addr, name)) sysid = req.get("System-ID") try: sport = int(req.get("Server-Port")) except (TypeError, ValueError): sport = None hidden = (req.get("Uplink-Visibility", "visible").lower() == "hidden") if name in VFS: if not(sysid) or (sysid != VFS[name].sysid): # VFS name collision -> reject uplink log('hub', "rejecting uplink from %s due to name collision" % self.conn.addr) log('access', "%s - REGISTER %r rejected (VFS collision)" % (self.conn.addr, req.method, req.uri)) return Response(req, 450).send(self.conn) # reconnect -> discard old remote log('hub', "uplink reconnect from %s, discarding old uplink" % self.conn.addr) VFS[name].remote.owner.close(2) log('access', "%s - REGISTER %r" % (self.conn.addr, req.uri)) try: remote = RemoteHandler(name, self) VFS[name] = VFSRemote(remote, sysid, hidden=hidden) vfs_updated() log('hub', "uplink %r registered for %s" % (name, self.conn.addr)) if sport: VFS[name].check_direct(self.conn.addr.rsplit(':', 1)[0], sport) remote.run(self.conn) finally: del VFS[name] vfs_updated() log('hub', "uplink %r unregistered" % name) log('access', "%s - unregistered uplink %r" % (self.conn.addr, req.uri)) return def shutdown(self, timeout=None): self.shutdown_pending = True if self.conn: self.conn.shutdown() if self.uplink: self.uplink.shutdown() class ListenerThread(RegisteredThread): def __init__(self, port=DEFAULT_PORT): global ServerPort RegisteredThread.__init__(self, "Listener") ServerPort = port self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind(('', self.port)) def main(self): self.sock.listen(5) log('conn', "listening on port %d." % self.port) try: while True: client_sock, client_addr = self.sock.accept() ClientThread(Connection(client_sock, client_addr)).start() except socket.error, e: pass ServerPort = None self.sock.close() def shutdown(self, timeout=None): try: self.sock.shutdown(socket.SHUT_RDWR) except (socket.error, IOError, EOFError): pass ############################################################################### class UplinkRequestThread(ClientThread): def __init__(self, req, host, port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) sock.connect((host, port)) conn = Connection(sock, (host, port), self) if Request("REPLY", req["X-HTShare-Request-ID"], PROTOCOL_VERSION).send(conn) < 0: raise IOError ClientThread.__init__(self, conn, req) class UplinkControlThread(RegisteredThread): def __init__(self, name, host, port=DEFAULT_PORT, hidden=False, hub_password=None): RegisteredThread.__init__(self, "UplinkControl-%s-%d" % (host, port)) self.name = name self.host = host self.port = port self.hidden = hidden self.hub_password = hub_password self.addr = "%s:%d" % (host, port) self.sock = None self.shutdown_pending = False def main(self): last_connect_time = 0 while not(self.shutdown_pending): log('conn', "connecting to %s ..." % self.addr) try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) self.sock.connect((self.host, self.port)) self.process_connection(Connection(self.sock, self.addr, self)) self.sock.close() reason = "closed" except (socket.error, IOError), e: reason = "failed (%s)" % e self.sock = None if self.shutdown_pending: return now = time.time() dt = now - last_connect_time if dt > 99: wait = 1 elif dt < 2: wait = 2 elif dt < 5: wait = 5 elif dt < 10: wait = 10 elif dt < 15: wait = 15 elif dt < 30: wait = 30 else: wait = 60 last_connect_time = now log('conn', "connection to %s %s, reconnecting in %d second(s)" % (self.addr, reason, wait)) while wait > 0: if self.shutdown_pending: return t = min(wait, 1) time.sleep(t) wait -= t def process_connection(self, conn): global ServerPort log('conn', "connected to %s, registering ..." % conn.addr) req = Request("REGISTER", self.name, PROTOCOL_VERSION) req["System-ID"] = SystemID if ServerPort: req["Server-Port"] = str(ServerPort) if self.hidden: req["Uplink-Visibility"] = "hidden" if self.hub_password: req["Authorization"] = "Basic " + base64.b64encode(':' + self.hub_password) req.send(conn) res = Response().read(conn) if res.code == 401: log('access', "%s - registering %r failed due to password mismatch" % (conn.addr, self.name)) log('conn', "hub %s answered with HTTP error %d, giving up" % (conn.addr, res.code)) self.shutdown_pending = True return if res.code == 403: log('access', "%s - registering %r failed because host doesn't allow uplink registration" % (conn.addr, self.name)) log('conn', "hub %s answered with HTTP error %d, giving up" % (conn.addr, res.code)) self.shutdown_pending = True return if res.code != 250: log('conn', "hub %s answered with HTTP error %d, closing connection" % (conn.addr, res.code)) return try: # if the server promises a specific ping interval, set a request # receive timeout of 2.5 times that (to give a little wiggle room) conn.sock.settimeout(int(res["Ping-Interval"]) * 5 / 2) except (KeyError, ValueError): pass log('access', "%s - registering %r succeeded" % (conn.addr, self.name)) while not self.shutdown_pending: req = Request().read(conn) if not req: break self.process_request(conn, req) if not self.shutdown_pending: log('access', "%s - lost hub connection" % (conn.addr)) def process_request(self, conn, req): if req.method == "PING": Response(req, 251).send(conn) return elif req.get("X-HTShare-Request-ID"): try: handler = UplinkRequestThread(req, self.host, self.port) except (socket.error, IOError, EOFError): return handler.start() return def shutdown(self, timeout=None): self.shutdown_pending = True try: if self.sock: self.sock.shutdown(socket.SHUT_RDWR) except (socket.error, IOError, EOFError): pass ############################################################################### def daemonize(): # simplified version of http://code.activestate.com/recipes/278731-creating-a-daemon-the-python-way/ def fail(e): print >>sys.stderr, "Error: failed to daemonize -", e sys._exit(1) try: pid = os.fork() # fork first child except OSError, e: fail(e) if pid: os._exit(0) # terminate parent os.setsid() # create new session try: pid = os.fork() # fork second child except OSError, e: fail(e) if pid: os._exit(0) # terminate first child os.chdir('/') # go to "safe" working directory for fd in xrange(128): try: os.close(fd) # close all files (at least a few of them) except OSError: pass # file wasn't open os.open(os.devnull, os.O_RDWR) # reopen stdin as /dev/null os.dup2(0, 1) # stdout = /dev/null too os.dup2(0, 2) # stderr = /dev/null too ############################################################################### def get_own_path(): me = os.path.normpath(os.path.abspath(sys.argv[0])) if not os.path.isfile(me): found = False for path in os.getenv("PATH").split(os.pathsep): me = os.path.join(path, sys.argv[0]) found = os.path.isfile(me) and os.access(me, os.X_OK) if found: break if not found: me = None return me def run_cmd(cmdline, ignore_fail=False): print "executing '%s' ..." % (' '.join(cmdline)) try: res = subprocess.Popen(cmdline).wait() print "OK." return 0 except (OSError, AssertionError): res = -1 if res: if ignore_fail: print "failed (ignored)." else: print "FAILED." sys.exit(1) return res def do_install(): me = get_own_path() if me and (me != INSTALL_PATH): try: f = open(me, "rb") code = f.read() f.close() except IOError: pass INSTALL_FILES.insert(0, (INSTALL_PATH, "this program", code)) print "This operation will do the following:" for filename, description, content in INSTALL_FILES: print " - install", description, "in", filename print " - run update-rc.d to activate the startup script" if os.geteuid(): print "Root privileges are required for this. Please run" print " sudo", sys.argv[0], "-install" return 1 if raw_input("Proceed? (y/N) ").upper() != 'Y': print "Aborted." return 2 print print "Checking existing files ..." overwrite = 0 keep = set() for filename, description, content in INSTALL_FILES: print filename + ':', try: f = open(filename, "rb") data = f.read().strip() f.close() except IOError: print "does not exist" continue if data == content.strip(): print "exists, unchanged" continue print "exists, but different from version that will be installed" while True: a = raw_input("Overwrite %s? (y/N/d/q) " % filename).upper() if a == 'Q': print "Aborted." return 0 elif not(a) or (a == 'N'): keep.add(filename) break elif a == 'Y': overwrite += 1 break elif a == 'D': try: f = tempfile.NamedTemporaryFile(delete=False) f.write(content.lstrip().replace('\r', '')) f.close() subprocess.Popen(["diff", "-u", filename, f.name]).wait() os.unlink(f.name) except: print "Failed to display diff :(" if overwrite or keep: if raw_input("%d files will be overwritten, %d not updated. Proceed? (y/N) " % (overwrite, len(keep))).upper() != 'Y': print "Aborted." return 2 print for filename, description, content in INSTALL_FILES: if filename in keep: print "skipping %s." % filename continue print "installing", filename, "...", content = content.lstrip().replace('\r', '') try: os.unlink(filename) except OSError: pass try: f = open(filename, 'wb') f.write(content) f.close() except IOError: print "FAILED." sys.exit(1) if not content.startswith("#!"): print "OK" continue try: s = os.stat(filename) os.chmod(filename, s.st_mode | stat.S_IEXEC) except OSError: print "failed to make executable." sys.exit(1) print "OK" run_cmd(["update-rc.d", "htshare", "defaults"]) print print "Installation completed. Now you should" print " - review and update the configuration file:" print " sudo", os.getenv("EDITOR", "editor" if os.path.exists("/usr/bin/editor") else "vi"), "/etc/htshare.conf" print " - start HTShare:" print " sudo service htshare start" def do_uninstall(): me = get_own_path() if me and (me != INSTALL_PATH): INSTALL_FILES.insert(0, (INSTALL_PATH, "this program", None)) print "This operation will do the following:" print " - stop HTShare if it's still running" for filename, description, content in INSTALL_FILES: print " - delete", description, "from", filename print " - run update-rc.d to remove the startup script" if os.geteuid(): print "Root privileges are required for this. Please run" print " sudo", sys.argv[0], "-uninstall" return 1 if raw_input("Proceed? (y/N) ").upper() != 'Y': print "Aborted." return 2 print print "Checking existing files ..." keep = set() for filename, description, content in INSTALL_FILES: print filename + ':', try: f = open(filename, "rb") data = f.read().strip() f.close() except IOError: print "does not exist" continue if not content: print "exists" continue if data == content.strip(): print "exists, unchanged" continue print "exists, but different from default" a = raw_input("Delete %s anyway? (y/N/q) " % filename).upper() if a == 'Q': print "Aborted." return 0 elif a != 'Y': keep.add(filename) print run_cmd(["service", "stop", "htshare"], ignore_fail=True) for filename, description, content in INSTALL_FILES: if filename in keep: print "keeping %s." % filename continue if not os.path.exists(filename): print "%s: already deleted" % filename continue print "deleting", filename, "...", try: os.unlink(filename) except OSError: print "FAILED." sys.exit(1) print "OK" run_cmd(["update-rc.d", "htshare", "remove"]) print "Done." ############################################################################### class ProgramOptions(object): def __init__(self): self.port = DEFAULT_PORT self.logs = set(['access']) self.logfile = None self.pidfile = None self.daemon = False self.uid = None self.gid = None self.shares = [] self.hubs = [] self.prefix = "" def error(self, msg): print >>sys.stderr, msg sys.exit(2) def parse(self, arg, location="cmdline", allow_prefix=True): arg = (self.prefix + arg).strip() self.prefix = "" if arg.startswith('-'): opt = arg.lstrip('-').replace('\t', ' ') if ' ' in opt: opt, arg = opt.split(' ', 1) arg = arg.strip() else: arg = None opt = opt.lower() handler = getattr(self, "handle_" + opt, None) if handler: if arg: self.error("%s: option '-%s' doesn't expect an argument" % (location, opt)) return handler(location) handler = getattr(self, "parse_" + opt, None) if not handler: self.error("%s: unrecognized option '-%s'" % (location, opt)) if arg: return handler(arg, location) if allow_prefix: self.prefix = "-%s " % opt return else: self.error("%s: missing argument for '%s'" % (location, self.prefix.strip())) if arg.lower().startswith("http://"): return self.parse_hub(arg[7:], location) if arg.lower().startswith("htshare://"): return self.parse_hub(arg[10:], location) if '=' in arg: return self.parse_share(arg, location) if '@' in arg: return self.parse_hub(arg, location) if os.path.isdir(arg): return self.parse_share(arg, location) if ':' in arg: dirname, pw = arg.rsplit(':', 1) return self.parse_share(":%s=%s" % (pw, dirname), location) return self.parse_hub(arg, location) @staticmethod def decode_password(raw_pw): if not raw_pw: return None if raw_pw.startswith('^'): try: pw = base64.b64decode(raw_pw[1:] + '=' * ((1 - len(raw_pw)) & 3)) except TypeError: return raw_pw if pw.startswith(':'): return pw[1:] return raw_pw def handle_help(self, location="cmdline"): print __doc__.lstrip() sys.exit(0) handle_h = handle_help def handle_passwd(self, location="cmdline"): try: pw1 = getpass.getpass("Enter password: ") pw2 = getpass.getpass("Enter password again: ") except (IOError, EOFError, KeyboardInterrupt): sys.exit(1) if pw1 == pw2: print '^' + base64.b64encode(':' + pw1.strip()).rstrip('=') sys.exit(0) else: print >>sys.stderr, "Error: passwords didn't match" sys.exit(1) def handle_nolisten(self, location="cmdline"): self.port = 0 def unix_check(self, opt, location="cmdline"): if os.name == 'nt': print >>sys.stderr, "%s: '-%s' option is not available on Windows" % (location, opt) sys.exit(1) def handle_daemon(self, location="cmdline"): self.unix_check('daemon', location) self.daemon = True def handle_install(self, location="cmdline"): self.unix_check('install', location) sys.exit(do_install() or 0) def handle_uninstall(self, location="cmdline"): self.unix_check('uninstall', location) sys.exit(do_uninstall() or 0) def parse_setuid(self, arg, location="cmdline"): self.unix_check('setuid', location) try: uid = int(arg) except ValueError: import pwd try: uid = pwd.getpwnam(arg).pw_uid except KeyError: print >>sys.stderr, "%s: user '%s' doesn't exist" % (location, arg) sys.exit(1) self.uid = uid def parse_setgid(self, arg, location="cmdline"): self.unix_check('setgid', location) try: gid = int(arg) except ValueError: import grp try: gid = grp.getgrnam(arg).gr_gid except KeyError: print >>sys.stderr, "%s: group '%s' doesn't exist" % (location, arg) sys.exit(1) self.gid = gid def decode_port(self, arg, location="cmdline"): try: port = int(arg, 0) assert (port > 0) and (port < 65535) except (ValueError, AssertionError): self.error("%s: invalid port number '%s'" % (location, arg)) return port def parse_port(self, arg, location="cmdline"): self.port = self.decode_port(arg, location) def parse_rootpw(self, arg, location="cmdline"): global RootPassword RootPassword = self.decode_password(arg) def parse_hubpw(self, arg, location="cmdline"): global HubPassword HubPassword = self.decode_password(arg) def parse_pw(self, arg, location="cmdline"): global RootPassword, HubPassword pw = self.decode_password(arg) RootPassword = pw HubPassword = pw def parse_vhost(self, arg, location="cmdline"): global RestrictVHost RestrictVHost = arg def handle_nohub(self, arg, location="cmdline"): global NoHub NoHub = arg def parse_encoding(self, arg, location="cmdline"): global FSEncoding try: dummy = unicode('dummy', arg, 'replace') except LookupError: self.error("%s: unrecognized encoding '%s'" % (location, arg)) FSEncoding = arg def parse_ping(self, arg, location="cmdline"): global PingInterval try: arg = int(arg) if arg < 1: raise ValueError except ValueError: self.error("%s: invalid ping interval '%s'" % (location, arg)) PingInterval = arg def parse_log(self, arg, location="cmdline"): self.logs = set(map(str.strip, arg.lower().split(','))) def parse_logfile(self, arg, location="cmdline"): try: f = open(arg, 'a') f.close() except IOError: self.error("%s: could not open log file '%s'" % (location, arg)) self.logfile = arg def parse_pidfile(self, arg, location="cmdline"): self.pidfile = arg def parse_config(self, arg, location="cmdline"): self.end(location) try: f = open(arg) n = 0 for line in f: n += 1 line = line.strip() if not(line) or line.startswith('#'): continue self.parse(line, "%s:%d" % (arg, n), False) f.close() except IOError: self.error("%s: could not open config file '%s'" % (location, arg)) parse_c = parse_config parse_cfg = parse_config def parse_share(self, arg, location="cmdline"): if '=' in arg: name_part, path = map(str.strip, arg.rsplit('=', 1)) else: name_part = None path = arg path = os.path.normpath(os.path.abspath(path)) if not os.path.isdir(path): self.error("%s: share path '%s' does not exist" % (location, path)) if not name_part: name = None pw = None elif ':' in name_part: name, pw = map(str.strip, name_part.split(':', 1)) else: name = name_part pw = None hidden = (name and name.startswith('.')) if hidden: name = name[1:].strip() if not name: name = os.path.basename(path.rstrip(os.path.sep)) self.shares.append((name, path, hidden, self.decode_password(pw))) def parse_hub(self, arg, location="cmdline"): if '@' in arg: name_part, host_part = map(str.strip, arg.rsplit('@', 1)) else: name_part = "" host_part = arg if ':' in name_part: name, pw = map(str.strip, name_part.split(':', 1)) else: name = name_part pw = None if ':' in host_part: host, port = map(str.strip, host_part.split(':', 1)) port = self.decode_port(port, location) else: host = host_part port = DEFAULT_PORT hidden = (name and name.startswith('.')) if hidden: name = name[1:].strip() if not name: name = socket.gethostname() self.hubs.append((name, host, port, hidden, self.decode_password(pw))) def end(self, location="cmdline"): if self.prefix: self.error("%s: missing argument for '%s'" % (location, self.prefix.strip())) if __name__ == "__main__": opts = ProgramOptions() for i in xrange(1, len(sys.argv)): opts.parse(sys.argv[i], "cmdline:%d" % i) opts.end() if not(opts.port) and not(opts.hubs): opts.error("Error: no-listen mode, but no hubs - would not do anything anyway, so quitting now") if NoHub and not(opts.shares): opts.error("Error: no-hub mode, but no shares - would not do anything anyway, so quitting now") if opts.logfile == 'stdout': opts.logfile = None if opts.pidfile: opts.pidfile = os.path.normpath(os.path.abspath(opts.pidfile)) if opts.logfile: opts.logfile = os.path.normpath(os.path.abspath(opts.logfile)) if opts.daemon: daemonize() if opts.pidfile: try: f = open(opts.pidfile, 'w') print >>f, os.getpid() f.close() except IOError, e: print >>sys.stderr, "Error: could not write PID file '%s' - %s" % (opts.pidfile, e) sys.exit(1) LOG_LEVELS = opts.logs if opts.logfile: try: LOG_FILE = open(opts.logfile, 'a') except IOError, e: print >>sys.stderr, "Error: could not open log file '%s' - %s" % (opts.logfile, e) sys.exit(1) if opts.gid: try: os.setgid(opts.gid) except OSError, e: print >>sys.stderr, "Error: could not setgid(%d) - %s" % (opts.gid, e) if opts.uid: try: os.setuid(opts.uid) except OSError, e: print >>sys.stderr, "Error: could not setuid(%d) - %s" % (opts.uid, e) for name, path, hidden, password in opts.shares: VFS[name] = VFSDirectory(path, hidden=hidden, password=password) threads_to_start = [] if opts.port: try: listener = ListenerThread(opts.port) except (IOError, socket.error), e: print >>sys.stderr, "Error: could not initialize HTTP listener - %s" % e sys.exit(1) threads_to_start.append(listener) for name, host, port, hidden, password in opts.hubs: try: uplink = UplinkControlThread(name, host, port, hidden=hidden, hub_password=password) except (IOError, socket.error), e: print >>sys.stderr, "Error: could not initialize uplink %r - %s" % (name, e) sys.exit(1) threads_to_start.append(uplink) log(None, "HTShare %s is starting up" % __version__) while threads_to_start: threads_to_start.pop(0).start() try: while True: time.sleep(10) except (SystemExit, KeyboardInterrupt): print if os.name != 'nt': RegisteredThread.close_all() log(None, "done.") if opts.pidfile: try: os.unlink(opts.pidfile) except OSError: pass if os.name == 'nt': sys.exit(0)