1 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
2 # This module is free software, and you may redistribute it and/or modify
3 # under the same terms as Python, so long as this copyright message and
4 # disclaimer are retained in their original form.
5 #
6 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
7 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
8 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
9 # POSSIBILITY OF SUCH DAMAGE.
10 #
11 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
12 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
14 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16 #
17 """ HTTP Server that serves roundup.
19 $Id: roundup_server.py,v 1.21 2003-03-26 04:54:59 richard Exp $
20 """
22 # python version check
23 from roundup import version_check
25 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
26 import BaseHTTPServer
28 # Roundup modules of use here
29 from roundup.cgi import cgitb, client
30 import roundup.instance
31 from roundup.i18n import _
33 #
34 ## Configuration
35 #
37 # This indicates where the Roundup trackers live. They're given as NAME ->
38 # TRACKER_HOME, where the NAME part is used in the URL to select the
39 # appropriate reacker.
40 # Make sure the NAME part doesn't include any url-unsafe characters like
41 # spaces, as these confuse the cookie handling in browsers like IE.
42 TRACKER_HOMES = {
43 'bar': '/tmp/bar',
44 }
46 ROUNDUP_USER = None
49 # Where to log debugging information to. Use an instance of DevNull if you
50 # don't want to log anywhere.
51 # TODO: actually use this stuff
52 #class DevNull:
53 # def write(self, info):
54 # pass
55 #LOG = open('/var/log/roundup.cgi.log', 'a')
56 #LOG = DevNull()
58 #
59 ## end configuration
60 #
62 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
63 TRACKER_HOMES = TRACKER_HOMES
64 ROUNDUP_USER = ROUNDUP_USER
66 def run_cgi(self):
67 """ Execute the CGI command. Wrap an innner call in an error
68 handler so all errors can be caught.
69 """
70 save_stdin = sys.stdin
71 sys.stdin = self.rfile
72 try:
73 self.inner_run_cgi()
74 except client.NotFound:
75 self.send_error(404, self.path)
76 except client.Unauthorised:
77 self.send_error(403, self.path)
78 except:
79 # it'd be nice to be able to detect if these are going to have
80 # any effect...
81 self.send_response(400)
82 self.send_header('Content-Type', 'text/html')
83 self.end_headers()
84 try:
85 reload(cgitb)
86 self.wfile.write(cgitb.breaker())
87 self.wfile.write(cgitb.html())
88 except:
89 s = StringIO.StringIO()
90 traceback.print_exc(None, s)
91 self.wfile.write("<pre>")
92 self.wfile.write(cgi.escape(s.getvalue()))
93 self.wfile.write("</pre>\n")
94 sys.stdin = save_stdin
96 do_GET = do_POST = run_cgi
98 def index(self):
99 ''' Print up an index of the available trackers
100 '''
101 self.send_response(200)
102 self.send_header('Content-Type', 'text/html')
103 self.end_headers()
104 w = self.wfile.write
105 w(_('<html><head><title>Roundup trackers index</title></head>\n'))
106 w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
107 keys = self.TRACKER_HOMES.keys()
108 keys.sort()
109 for tracker in keys:
110 w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
111 'tracker_url': urllib.quote(tracker),
112 'tracker_name': cgi.escape(tracker)})
113 w(_('</ol></body></html>'))
115 def inner_run_cgi(self):
116 ''' This is the inner part of the CGI handling
117 '''
119 rest = self.path
120 i = rest.rfind('?')
121 if i >= 0:
122 rest, query = rest[:i], rest[i+1:]
123 else:
124 query = ''
126 # no tracker - spit out the index
127 if rest == '/':
128 return self.index()
130 # figure the tracker
131 l_path = rest.split('/')
132 tracker_name = urllib.unquote(l_path[1])
134 # handle missing trailing '/'
135 if len(l_path) == 2:
136 self.send_response(301)
137 # redirect - XXX https??
138 protocol = 'http'
139 url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
140 self.send_header('Location', url)
141 self.end_headers()
142 self.wfile.write('Moved Permanently')
143 return
145 if self.TRACKER_HOMES.has_key(tracker_name):
146 tracker_home = self.TRACKER_HOMES[tracker_name]
147 tracker = roundup.instance.open(tracker_home)
148 else:
149 raise client.NotFound
151 # figure out what the rest of the path is
152 if len(l_path) > 2:
153 rest = '/'.join(l_path[2:])
154 else:
155 rest = '/'
157 # Set up the CGI environment
158 env = {}
159 env['TRACKER_NAME'] = tracker_name
160 env['REQUEST_METHOD'] = self.command
161 env['PATH_INFO'] = urllib.unquote(rest)
162 if query:
163 env['QUERY_STRING'] = query
164 host = self.address_string()
165 if self.headers.typeheader is None:
166 env['CONTENT_TYPE'] = self.headers.type
167 else:
168 env['CONTENT_TYPE'] = self.headers.typeheader
169 length = self.headers.getheader('content-length')
170 if length:
171 env['CONTENT_LENGTH'] = length
172 co = filter(None, self.headers.getheaders('cookie'))
173 if co:
174 env['HTTP_COOKIE'] = ', '.join(co)
175 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
176 env['SCRIPT_NAME'] = ''
177 env['SERVER_NAME'] = self.server.server_name
178 env['SERVER_PORT'] = str(self.server.server_port)
179 env['HTTP_HOST'] = self.headers['host']
181 decoded_query = query.replace('+', ' ')
183 # do the roundup thang
184 c = tracker.Client(tracker, self, env)
185 c.main()
187 def usage(message=''):
188 if message:
189 message = _('Error: %(error)s\n\n')%{'error': message}
190 print _('''%(message)sUsage:
191 roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=tracker home]*
193 -n: sets the host name
194 -p: sets the port to listen on
195 -l: sets a filename to log to (instead of stdout)
196 -d: daemonize, and write the server's PID to the nominated file
198 name=tracker home
199 Sets the tracker home(s) to use. The name is how the tracker is
200 identified in the URL (it's the first part of the URL path). The
201 tracker home is the directory that was identified when you did
202 "roundup-admin init". You may specify any number of these name=home
203 pairs on the command-line. For convenience, you may edit the
204 TRACKER_HOMES variable in the roundup-server file instead.
205 Make sure the name part doesn't include any url-unsafe characters like
206 spaces, as these confuse the cookie handling in browsers like IE.
207 ''')%locals()
208 sys.exit(0)
210 def daemonize(pidfile):
211 ''' Turn this process into a daemon.
212 - make sure the sys.std(in|out|err) are completely cut off
213 - make our parent PID 1
215 Write our new PID to the pidfile.
217 From A.M. Kuuchling (possibly originally Greg Ward) with
218 modification from Oren Tirosh, and finally a small mod from me.
219 '''
220 # Fork once
221 if os.fork() != 0:
222 os._exit(0)
224 # Create new session
225 os.setsid()
227 # Second fork to force PPID=1
228 pid = os.fork()
229 if pid:
230 pidfile = open(pidfile, 'w')
231 pidfile.write(str(pid))
232 pidfile.close()
233 os._exit(0)
235 os.chdir("/")
236 os.umask(0)
238 # close off sys.std(in|out|err), redirect to devnull so the file
239 # descriptors can't be used again
240 devnull = os.open('/dev/null', 0)
241 os.dup2(devnull, 0)
242 os.dup2(devnull, 1)
243 os.dup2(devnull, 2)
245 def abspath(path):
246 ''' Make the given path an absolute path.
248 Code from Zope-Coders posting of 2002-10-06 by GvR.
249 '''
250 if not os.path.isabs(path):
251 path = os.path.join(os.getcwd(), path)
252 return os.path.normpath(path)
254 def run():
255 ''' Script entry point - handle args and figure out what to to.
256 '''
257 hostname = ''
258 port = 8080
259 pidfile = None
260 logfile = None
261 try:
262 # handle the command-line args
263 try:
264 optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:h')
265 except getopt.GetoptError, e:
266 usage(str(e))
268 user = ROUNDUP_USER
269 for (opt, arg) in optlist:
270 if opt == '-n': hostname = arg
271 elif opt == '-p': port = int(arg)
272 elif opt == '-u': user = arg
273 elif opt == '-d': pidfile = abspath(arg)
274 elif opt == '-l': logfile = abspath(arg)
275 elif opt == '-h': usage()
277 if hasattr(os, 'getuid'):
278 # if root, setuid to the running user
279 if not os.getuid() and user is not None:
280 try:
281 import pwd
282 except ImportError:
283 raise ValueError, _("Can't change users - no pwd module")
284 try:
285 uid = pwd.getpwnam(user)[2]
286 except KeyError:
287 raise ValueError, _("User %(user)s doesn't exist")%locals()
288 os.setuid(uid)
289 elif os.getuid() and user is not None:
290 print _('WARNING: ignoring "-u" argument, not root')
292 # People can remove this check if they're really determined
293 if not os.getuid() and user is None:
294 raise ValueError, _("Can't run as root!")
296 # handle tracker specs
297 if args:
298 d = {}
299 for arg in args:
300 try:
301 name, home = arg.split('=')
302 except ValueError:
303 raise ValueError, _("Instances must be name=home")
304 d[name] = home
305 RoundupRequestHandler.TRACKER_HOMES = d
306 except SystemExit:
307 raise
308 except:
309 exc_type, exc_value = sys.exc_info()[:2]
310 usage('%s: %s'%(exc_type, exc_value))
312 # we don't want the cgi module interpreting the command-line args ;)
313 sys.argv = sys.argv[:1]
314 address = (hostname, port)
316 # fork?
317 if pidfile:
318 if not hasattr(os, 'fork'):
319 print "Sorry, you can't run the server as a daemon on this" \
320 'Operating System'
321 sys.exit(0)
322 daemonize(pidfile)
324 # redirect stdout/stderr to our logfile
325 if logfile:
326 # appending, unbuffered
327 sys.stdout = sys.stderr = open(logfile, 'a', 0)
329 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
330 print _('Roundup server started on %(address)s')%locals()
331 try:
332 httpd.serve_forever()
333 except KeyboardInterrupt:
334 print 'Keyboard Interrupt: exiting'
336 if __name__ == '__main__':
337 run()
339 # vim: set filetype=python ts=4 sw=4 et si