#!/usr/bin/env python # -*- encoding: utf-8 -*- # # Copyright (C) 2004 Simon Budig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # A copy of the GNU General Public License is available at # http://www.fsf.org/licenses/gpl.txt, you can also write to the # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. # Darwin support with the help from Mat Caughron, # Solaris support by Colin Marquardt, # FreeBSD support with the help from Andy Gimblett, # Cygwin support by Stefan Reichör # NetBSD support by Geoffroy Weisenhorn, """ woof -- an ad-hoc single file webserver""" import sys, os, popen2, signal, select, socket, getopt, commands import stat import urllib, BaseHTTPServer import ConfigParser maxdownloads = 1 CPID = -1 COMPRESSED = True IPV6 = False def find_ip (): """ Utility function to guess the IP (as a string) where the server can be reached from the outside. Quite nasty problem actually. """ if sys.platform == "cygwin": ipcfg = os.popen("ipconfig").readlines() for aline in ipcfg: try: candidat = aline.split(":")[1].strip() if candidat[0].isdigit(): break except: pass return candidat os.environ["PATH"] = "/sbin:/usr/sbin:/usr/local/sbin:" + os.environ["PATH"] platform = os.uname()[0] if platform == "Linux": netstat = commands.getoutput ("LC_MESSAGES=C netstat -rn") defiface = [i.split ()[-1] for i in netstat.split('\n') if i.split()[0] == "0.0.0.0"] elif platform in ("Darwin", "FreeBSD"): netstat = commands.getoutput ("LC_MESSAGES=C netstat -rn") defiface = [i.split ()[-1] for i in netstat.split('\n') if len(i) > 2 and i.split()[0] == "default"] elif platform == "SunOS": netstat = commands.getoutput ("LC_MESSAGES=C netstat -arn") defiface = [i.split ()[-1] for i in netstat.split('\n') if len(i) > 2 and i.split()[0] == "0.0.0.0"] elif platform == "NetBSD": netstat = commands.getoutput ("LC_MESSAGES=C netstat -arn") defiface = [i.split()[-1] for i in netstat.split('\n') if len(i) > 2 and i.split()[0] == "default"] else: print >> sys.stderr, "Unsupported platform; \ please add support for your platform in find_ip()." return None if not defiface: return None if platform == "Linux": if IPV6: spliter = "inet6 addr:" else: spliter = "inet addr:" ifcfg = commands.getoutput ("LC_MESSAGES=C ifconfig " + defiface[0]).split (spliter) elif platform in ("Darwin", "FreeBSD", "SunOS", "NetBSD"): ifcfg = commands.getoutput ("LC_MESSAGES=C ifconfig " + defiface[0]).split ("inet ") if IPV6: ifcfg_len = 3 else: ifcfg_len = 2 if len (ifcfg) != ifcfg_len: return None ip_addr = ifcfg[1].split ()[0] if IPV6: ip_addr = ip_addr.split('/')[0] else: # sanity check, only for ipv4 at the moment try: ints = [ i for i in ip_addr.split (".") if 0 <= int(i) <= 255] if len (ints) != 4: return None except ValueError: return None return ip_addr def is_owner(stf): """Return true if the file belongs to the current user""" return stf.st_uid == os.getuid() def is_group(stf): """Return true if the file belongs to the group of current user""" return stf.st_gid == os.getgid() def is_read(filename): """ Check whether a file is readable """ result_st = os.stat(filename) if is_owner(result_st): return (result_st.st_mode & stat.S_IRUSR) elif is_group(result_st): return (result_st.st_mode & stat.S_IRGRP) else: return (result_st.st_mode & stat.S_IROTH) class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """ Main class implementing an HTTP-Requesthandler, that serves just a single file and redirects all other requests to this file (this passes the actual filename to the client). Currently it is impossible to serve different files with different instances of this class. """ server_version = "Simons FileServer" protocol_version = "HTTP/1.0" filename = "." def log_request(self, code='-', size='-'): """Log an accepted request.""" if code == 200: BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size) def do_GET(self): """ Redirect any request to the filename of the file to serve. This hands over the filename to the client. """ global maxdownloads, CPID, COMPRESSED self.path = urllib.quote(urllib.unquote(self.path)) location = "/" + urllib.quote(os.path.basename(self.filename)) if os.path.isdir(self.filename): if COMPRESSED: location += ".tar.gz" else: location += ".tar" if self.path != location: txt = """\ 302 Found 302 Found here. \n""" % location self.send_response (302) self.send_header ("Location", location) self.send_header ("Content-type", "text/html") self.send_header ("Content-Length", str (len (txt))) self.end_headers () self.wfile.write (txt) return maxdownloads -= 1 # let a separate process handle the actual download, so that # multiple downloads can happen simultaneously. CPID = os.fork() os.setpgrp() if CPID == 0: # Child process size = -1 datafile = None child = None if os.path.isfile (self.filename): size = os.path.getsize (self.filename) datafile = open (self.filename) elif os.path.isdir (self.filename): filepath = os.path.split(self.filename) os.environ['woof_dir'], os.environ['woof_file'] = filepath if COMPRESSED: arg = 'z' else: arg = '' tarcmd = 'cd "$woof_dir";tar c%sf - "$woof_file"' child = popen2.Popen3(tarcmd % arg) datafile = child.fromchild self.send_response (200) self.send_header ("Content-type", "application/octet-stream") if size >= 0: self.send_header ("Content-Length", size) self.end_headers () try: try: while 1: if select.select ([datafile], [], [], 2)[0]: chunk = datafile.read(1024) if chunk: self.wfile.write(chunk) else: datafile.close() print '%s - - [%s] "%s %s %s" Complete -' % ( self.client_address[0], self.log_date_time_string(), self.command, self.path, self.request_version) break except: print >> sys.stderr, "Connection broke. Aborting" finally: # for some reason tar doesnt stop working when the pipe breaks if child: if child.poll (): os.killpg (os.getpgid (child.pid), signal.SIGTERM) def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080): """ Launch HTTP server """ global maxdownloads, IPV6 maxdownloads = maxdown # We have to somehow push the filename of the file to serve to the # class handling the requests. This is an evil way to do this... FileServHTTPRequestHandler.filename = filename if IPV6: BaseHTTPServer.HTTPServer.address_family = socket.AF_INET6 try: httpd = BaseHTTPServer.HTTPServer ((ip_addr, port), FileServHTTPRequestHandler) except socket.error: print >> sys.stderr, ("cannot bind to port %d" % (port)) sys.exit (1) if not ip_addr: ip_addr = find_ip () ip_addr = IPV6 and '[%s]' % ip_addr or ip_addr if ip_addr: print "Now serving on http://%s:%s/" % (ip_addr, httpd.server_port) while CPID != 0 and maxdownloads > 0: httpd.handle_request () def usage (defport, defmaxdown, errmsg = None): """ Display usage message """ name = os.path.basename (sys.argv[0]) print >> sys.stderr, """ Usage: %s [-i ] [-p ] [-c ] [-u] [-6] %s [-i ] [-p ] [-c ] [-u] [-6] -s Serves a single file times via http on port on IP address . When a directory is specified, a .tar.gz archive gets served (or an uncompressed tar archive when -u is specified), when -s is specified instead of a filename, %s distributes itself. defaults: count = %d, port = %d You can specify different defaults in two locations: /etc/woofrc and ~/.woofrc can be INI-style config files containing the default port and the default count. The file in the home directory takes precedence. Sample file: [main] port = 8008 count = 2 ip = 127.0.0.1 compressed = true ipv6 = false """ % (name, name, name, defmaxdown, defport) if errmsg: print >> sys.stderr, errmsg print >> sys.stderr sys.exit (1) def main (): """ parse config file or options before launching the http server """ global CPID, COMPRESSED, IPV6 maxdown = 1 port = 8080 ip_addr = '' config = ConfigParser.ConfigParser() config.read (['/etc/woofrc', os.path.expanduser('~/.woofrc')]) if config.has_option ('main', 'port'): port = config.getint ('main', 'port') if config.has_option ('main', 'count'): maxdown = config.getint ('main', 'count') if config.has_option ('main', 'ip'): ip_addr = config.get ('main', 'ip') if config.has_option ('main', 'compressed'): COMPRESSED = config.getboolean ('main', 'compressed') if config.has_option ('main', 'ipv6'): IPV6 = config.getboolean ('main', 'ipv6') defaultport = port defaultmaxdown = maxdown try: options, filenames = getopt.getopt (sys.argv[1:], "h6sui:c:p:") except getopt.GetoptError, desc: usage (defaultport, defaultmaxdown, desc) for option, val in options: if option == '-c': try: maxdown = int (val) if maxdown <= 0: raise ValueError except ValueError: usage (defaultport, defaultmaxdown, "invalid download count: %r. " "Please specify an integer >= 0." % val) elif option == '-i': ip_addr = val elif option == '-p': try: port = int (val) except ValueError: usage (defaultport, defaultmaxdown,"Invalid port number: %r.\ Please specify an integer" % val) elif option == '-s': filenames.append (__file__) elif option == '-h': usage (defaultport, defaultmaxdown) elif option == '-u': COMPRESSED = False elif option == '-6': IPV6 = True else: usage (defaultport, defaultmaxdown, "Unknown option: %r" % option) if len (filenames) == 1: filename = os.path.abspath (filenames[0]) else: usage (defaultport, defaultmaxdown, "Can only serve single files/directories.") if not os.path.exists (filename): usage (defaultport, defaultmaxdown, "%s: No such file or directory" % filenames[0]) if not (os.path.isfile (filename) or os.path.isdir (filename)): usage (defaultport, defaultmaxdown, "%s: Neither file nor directory" % filenames[0]) if not is_read(filename): usage (defaultport, defaultmaxdown, "%s: Check read permission" % filenames[0]) if IPV6: ip_addr = '' serve_files (filename, maxdown, ip_addr, port) # wait for child processes to terminate if CPID != 0: try: while 1: os.wait () except OSError: pass if __name__ == '__main__': try: main () except KeyboardInterrupt: print >> sys.stderr, "Interrupted .."