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.22 2003-04-24 04:27:32 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 # time out after a minute if we can
258 import socket
259 if hasattr(socket, 'setdefaulttimeout'):
260 socket.setdefaulttimeout(60)
262 hostname = ''
263 port = 8080
264 pidfile = None
265 logfile = None
266 try:
267 # handle the command-line args
268 try:
269 optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:h')
270 except getopt.GetoptError, e:
271 usage(str(e))
273 user = ROUNDUP_USER
274 for (opt, arg) in optlist:
275 if opt == '-n': hostname = arg
276 elif opt == '-p': port = int(arg)
277 elif opt == '-u': user = arg
278 elif opt == '-d': pidfile = abspath(arg)
279 elif opt == '-l': logfile = abspath(arg)
280 elif opt == '-h': usage()
282 if hasattr(os, 'getuid'):
283 # if root, setuid to the running user
284 if not os.getuid() and user is not None:
285 try:
286 import pwd
287 except ImportError:
288 raise ValueError, _("Can't change users - no pwd module")
289 try:
290 uid = pwd.getpwnam(user)[2]
291 except KeyError:
292 raise ValueError, _("User %(user)s doesn't exist")%locals()
293 os.setuid(uid)
294 elif os.getuid() and user is not None:
295 print _('WARNING: ignoring "-u" argument, not root')
297 # People can remove this check if they're really determined
298 if not os.getuid() and user is None:
299 raise ValueError, _("Can't run as root!")
301 # handle tracker specs
302 if args:
303 d = {}
304 for arg in args:
305 try:
306 name, home = arg.split('=')
307 except ValueError:
308 raise ValueError, _("Instances must be name=home")
309 d[name] = home
310 RoundupRequestHandler.TRACKER_HOMES = d
311 except SystemExit:
312 raise
313 except:
314 exc_type, exc_value = sys.exc_info()[:2]
315 usage('%s: %s'%(exc_type, exc_value))
317 # we don't want the cgi module interpreting the command-line args ;)
318 sys.argv = sys.argv[:1]
319 address = (hostname, port)
321 # fork?
322 if pidfile:
323 if not hasattr(os, 'fork'):
324 print "Sorry, you can't run the server as a daemon on this" \
325 'Operating System'
326 sys.exit(0)
327 daemonize(pidfile)
329 # redirect stdout/stderr to our logfile
330 if logfile:
331 # appending, unbuffered
332 sys.stdout = sys.stderr = open(logfile, 'a', 0)
334 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
335 print _('Roundup server started on %(address)s')%locals()
336 try:
337 httpd.serve_forever()
338 except KeyboardInterrupt:
339 print 'Keyboard Interrupt: exiting'
341 if __name__ == '__main__':
342 run()
344 # vim: set filetype=python ts=4 sw=4 et si