./others/woof.py

0.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
309.
310.
311.
312.
313.
314.
315.
316.
317.
318.
319.
320.
321.
322.
323.
324.
325.
326.
327.
328.
329.
330.
331.
332.
333.
334.
335.
336.
337.
338.
339.
340.
341.
342.
343.
344.
345.
346.
347.
348.
349.
350.
351.
352.
353.
354.
355.
356.
357.
358.
359.
360.
361.
362.
363.
364.
365.
366.
367.
368.
369.
370.
371.
372.
373.
374.
375.
376.
377.
378.
379.
380.
381.
382.
383.
384.
385.
386.
387.
388.
389.
390.
391.
392.
393.
394.
395.
396.
397.
398.
399.
400.
401.
402.
403.
404.
405.
406.
407.
408.
409.
410.
411.
412.
413.
414.
415.
416.
417.
418.
419.
420.
421.
422.
423.
424.
425.
426.
427.
428.
429.
430.
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
#  woof -- an ad-hoc single file webserver
#  Copyright (C) 2004-2009 Simon Budig  <[email protected]>
# 
#  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, <[email protected]>
# Solaris support by Colin Marquardt, <[email protected]>
# FreeBSD support with the help from Andy Gimblett, <[email protected]>
# Cygwin support by Stefan Reichör <[email protected]>
# tarfile usage suggested by Morgan Lefieux <[email protected]>
 
import sys, os, socket, getopt, commands
import urllib, BaseHTTPServer
import ConfigParser
import shutil, tarfile, zipfile
import struct
 
maxdownloads = 1
TM = object
cpid = -1
compressed = 'gz'
 
 
class EvilZipStreamWrapper(TM):
   def __init__ (self, victim):
      self.victim_fd = victim
      self.position = 0
      self.tells = []
      self.in_file_data = 0
 
   def tell (self):
      self.tells.append (self.position)
      return self.position
 
   def seek (self, offset, whence = 0):
      if offset != 0:
         if offset == self.tells[0] + 14:
            # the zipfile module tries to fix up the file header.
            # write Data descriptor header instead,
            # the next write from zipfile
            # is CRC, compressed_size and file_size (as required)
            self.write ("PK\007\010")
         elif offset == self.tells[1]:
            # the zipfile module goes to the end of the file. The next
            # data written definitely is infrastructure (in_file_data = 0)
            self.tells = []
            self.in_file_data = 0
         else:
            raise "unexpected seek for EvilZipStreamWrapper"
 
   def write (self, data):
      # only test for headers if we know that we're not writing
      # (potentially compressed) data.
      if self.in_file_data == 0:
         if data[:4] == zipfile.stringFileHeader:
            # fix the file header for extra Data descriptor
            hdr = list (struct.unpack (zipfile.structFileHeader, data[:30]))
            hdr[3] |= (1 << 3)
            data = struct.pack (zipfile.structFileHeader, *hdr) + data[30:]
            self.in_file_data = 1
         elif data[:4] == zipfile.stringCentralDir:
            # fix the directory entry to match file header.
            hdr = list (struct.unpack (zipfile.structCentralDir, data[:46]))
            hdr[5] |= (1 << 3)
            data = struct.pack (zipfile.structCentralDir, *hdr) + data[46:]
 
      self.position += len (data)
      self.victim_fd.write (data)
 
   def __getattr__ (self, name):
      return getattr (self.victim_fd, name)
 
 
# Utility function to guess the IP (as a string) where the server can be
# reached from the outside. Quite nasty problem actually.
 
def find_ip ():
   if sys.platform == "cygwin":
      ipcfg = os.popen("ipconfig").readlines()
      for l in ipcfg:
         try:
            candidat = l.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", "NetBSD"):
      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"]
   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":
      ifcfg = commands.getoutput ("LC_MESSAGES=C ifconfig "
                                  + defiface[0]).split ("inet addr:")
   elif platform in ("Darwin", "FreeBSD", "SunOS", "NetBSD"):
      ifcfg = commands.getoutput ("LC_MESSAGES=C ifconfig "
                                  + defiface[0]).split ("inet ")
 
   if len (ifcfg) != 2:
      return None
   ip_addr = ifcfg[1].split ()[0]
 
   # sanity check
   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
 
 
# 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.
 
class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler):
   server_version = "Simons FileServer"
   protocol_version = "HTTP/1.0"
 
   filename = "."
 
   def log_request (self, code='-', size='-'):
      if code == 200:
         BaseHTTPServer.BaseHTTPRequestHandler.log_request (self, code, size)
 
 
   def do_GET (self):
      global maxdownloads, cpid, compressed
 
      # Redirect any request to the filename of the file to serve.
      # This hands over the filename to the client.
 
      self.path = urllib.quote (urllib.unquote (self.path))
      location = "/" + urllib.quote (os.path.basename (self.filename))
      if os.path.isdir (self.filename):
         if compressed == 'gz':
            location += ".tar.gz"
         elif compressed == 'bz2':
            location += ".tar.bz2"
         elif compressed == 'zip':
            location += ".zip"
         else:
            location += ".tar"
 
      if self.path != location:
         txt = """\
                <html>
                   <head><title>302 Found</title></head>
                   <body>302 Found <a href="%s">here</a>.</body>
                </html>\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 ()
 
      if cpid == 0:
         # Child process
         child = None
         type = None
 
         if os.path.isfile (self.filename):
            type = "file"
         elif os.path.isdir (self.filename):
            type = "dir"
 
         if not type:
            print >> sys.stderr, "can only serve files or directories. Aborting."
            sys.exit (1)
 
         self.send_response (200)
         self.send_header ("Content-type", "application/octet-stream")
         if os.path.isfile (self.filename):
            self.send_header ("Content-Length",
                              os.path.getsize (self.filename))
         self.end_headers ()
 
         try:
            if type == "file":
               datafile = file (self.filename)
               shutil.copyfileobj (datafile, self.wfile)
               datafile.close ()
            elif type == "dir":
               if compressed == 'zip':
                  ezfile = EvilZipStreamWrapper (self.wfile)
                  zfile = zipfile.ZipFile (ezfile, 'w', zipfile.ZIP_DEFLATED)
		  stripoff = os.path.dirname (self.filename) + os.sep
 
                  for root, dirs, files in os.walk (self.filename):
                     for f in files:
                        filename = os.path.join (root, f)
			if filename[:len (stripoff)] != stripoff:
			   raise RuntimeException, "invalid filename assumptions, please report!"
                        zfile.write (filename, filename[len (stripoff):])
                  zfile.close ()
               else:
                  tfile = tarfile.open (mode=('w|' + compressed),
                                        fileobj=self.wfile)
                  tfile.add (self.filename,
                             arcname=os.path.basename(self.filename))
                  tfile.close ()
         except Exception, e:
            print e
            print >>sys.stderr, "Connection broke. Aborting"
 
 
def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080):
   global maxdownloads
 
   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
 
   try:
      httpd = BaseHTTPServer.HTTPServer ((ip_addr, port),
                                         FileServHTTPRequestHandler)
   except socket.error:
      print >>sys.stderr, "cannot bind to IP address '%s' port %d" % (ip_addr, port)
      sys.exit (1)
 
   if not ip_addr:
      ip_addr = find_ip ()
   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):
   name = os.path.basename (sys.argv[0])
   print >>sys.stderr, """
    Usage: %s [-i <ip_addr>] [-p <port>] [-c <count>] <file>
           %s [-i <ip_addr>] [-p <port>] [-c <count>] [-z|-j|-Z|-u] <dir>
           %s [-i <ip_addr>] [-p <port>] [-c <count>] -s
 
    Serves a single file <count> times via http on port <port> on IP
    address <ip_addr>.
    When a directory is specified, an tar archive gets served. By default
    it is gzip compressed. You can specify -z for gzip compression, 
    -j for bzip2 compression, -Z for ZIP compression or -u for no compression.
    You can configure your default compression method in the configuration 
    file described below.
 
    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. The compression methods are "off", "gz", "bz2" or "zip".
 
    Sample file:
 
        [main]
        port = 8008
        count = 2
        ip = 127.0.0.1
        compressed = gz
   """ % (name, name, name, name, defmaxdown, defport)
   if errmsg:
      print >>sys.stderr, errmsg
      print >>sys.stderr
   sys.exit (1)
 
 
 
def main ():
   global cpid, compressed
 
   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'):
      formats = { 'gz'    : 'gz',
                  'true'  : 'gz',
                  'bz'    : 'bz2',
                  'bz2'   : 'bz2',
                  'zip'   : 'zip',
                  'off'   : '',
                  'false' : '' }
      compressed = config.get ('main', 'compressed')
      compressed = formats.get (compressed, 'gz')
 
   defaultport = port
   defaultmaxdown = maxdown
 
   try:
      options, filenames = getopt.getopt (sys.argv[1:], "hszjZui: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 == '-z':
         compressed = 'gz'
      elif option == '-j':
         compressed = 'bz2'
      elif option == '-Z':
         compressed = 'zip'
      elif option == '-u':
         compressed = ''
 
      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])
 
   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:
      pass
 
 
 

Back