39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2003-2006 Edgewall Software
|
|
4 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
|
|
5 # Copyright (C) 2005-2006 Matthew Good <trac@matt-good.net>
|
|
6 # Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
|
|
7 # All rights reserved.
|
|
8 #
|
|
9 # This software is licensed as described in the file COPYING, which
|
|
10 # you should have received as part of this distribution. The terms
|
|
11 # are also available at http://trac.edgewall.com/license.html.
|
|
12 #
|
|
13 # This software consists of voluntary contributions made by many
|
|
14 # individuals. For the exact contribution history, see the revision
|
|
15 # history and logs, available at http://projects.edgewall.com/trac/.
|
|
16 #
|
|
17 # Author: Jonas Borgström <jonas@edgewall.com>
|
|
18 # Matthew Good <trac@matt-good.net>
|
|
19 # Christopher Lenz <cmlenz@gmx.de>
|
|
20
|
|
21 import errno
|
|
22 import os
|
|
23 import sys
|
|
24 from SocketServer import ThreadingMixIn
|
|
25
|
|
26 from trac import __version__ as VERSION
|
|
27 from trac.util import autoreload, daemon
|
|
28 from trac.web.auth import BasicAuthentication, DigestAuthentication
|
|
29 from trac.web.main import dispatch_request
|
|
30 from trac.web.wsgi import WSGIServer, WSGIRequestHandler
|
|
31
|
|
32
|
|
33 class AuthenticationMiddleware(object):
|
|
34
|
|
35 def __init__(self, application, auths):
|
|
36 self.application = application
|
|
37 self.auths = auths
|
|
38
|
|
39 def __call__(self, environ, start_response):
|
|
40 path_info = environ.get('PATH_INFO', '')
|
|
41 path_parts = filter(None, path_info.split('/'))
|
|
42 if len(path_parts) > 1 and path_parts[1] == 'login':
|
|
43 env_name = path_parts[0]
|
|
44 if env_name:
|
|
45 auth = self.auths.get(env_name, self.auths.get('*'))
|
|
46 if auth:
|
|
47 remote_user = auth.do_auth(environ, start_response)
|
|
48 if not remote_user:
|
|
49 return []
|
|
50 environ['REMOTE_USER'] = remote_user
|
|
51 return self.application(environ, start_response)
|
|
52
|
|
53
|
|
54 class BasePathMiddleware(object):
|
|
55
|
|
56 def __init__(self, application, base_path):
|
|
57 self.base_path = '/' + base_path.strip('/')
|
|
58 self.application = application
|
|
59
|
|
60 def __call__(self, environ, start_response):
|
|
61 path = environ['SCRIPT_NAME'] + environ.get('PATH_INFO', '')
|
|
62 environ['PATH_INFO'] = path[len(self.base_path):]
|
|
63 environ['SCRIPT_NAME'] = self.base_path
|
|
64 return self.application(environ, start_response)
|
|
65
|
|
66
|
|
67 class TracEnvironMiddleware(object):
|
|
68
|
|
69 def __init__(self, application, env_parent_dir, env_paths):
|
|
70 self.application = application
|
|
71 self.environ = {}
|
|
72 self.environ['trac.env_path'] = None
|
|
73 if env_parent_dir:
|
|
74 self.environ['trac.env_parent_dir'] = env_parent_dir
|
|
75 else:
|
|
76 self.environ['trac.env_paths'] = env_paths
|
|
77
|
|
78 def __call__(self, environ, start_response):
|
|
79 for k,v in self.environ.iteritems():
|
|
80 environ.setdefault(k, v)
|
|
81 return self.application(environ, start_response)
|
|
82
|
|
83
|
|
84 class TracHTTPServer(ThreadingMixIn, WSGIServer):
|
|
85
|
|
86 def __init__(self, server_address, application, env_parent_dir, env_paths):
|
|
87 WSGIServer.__init__(self, server_address, application,
|
|
88 request_handler=TracHTTPRequestHandler)
|
|
89
|
|
90
|
|
91 class TracHTTPRequestHandler(WSGIRequestHandler):
|
|
92
|
|
93 server_version = 'tracd/' + VERSION
|
|
94
|
|
95
|
|
96 def main():
|
|
97 from optparse import OptionParser, OptionValueError
|
|
98 parser = OptionParser(usage='usage: %prog [options] [projenv] ...',
|
|
99 version='%%prog %s' % VERSION)
|
|
100
|
|
101 auths = {}
|
|
102 def _auth_callback(option, opt_str, value, parser, cls):
|
|
103 info = value.split(',', 3)
|
|
104 if len(info) != 3:
|
|
105 raise OptionValueError("Incorrect number of parameters for %s"
|
|
106 % option)
|
|
107
|
|
108 env_name, filename, realm = info
|
|
109 if env_name in auths:
|
|
110 print >>sys.stderr, 'Ignoring duplicate authentication option for ' \
|
|
111 'project: %s' % env_name
|
|
112 else:
|
|
113 auths[env_name] = cls(filename, realm)
|
|
114
|
|
115 def _validate_callback(option, opt_str, value, parser, valid_values):
|
|
116 if value not in valid_values:
|
|
117 raise OptionValueError('%s must be one of: %s, not %s'
|
|
118 % (opt_str, '|'.join(valid_values), value))
|
|
119 setattr(parser.values, option.dest, value)
|
|
120
|
|
121 parser.add_option('-a', '--auth', action='callback', type='string',
|
|
122 metavar='DIGESTAUTH', callback=_auth_callback,
|
|
123 callback_args=(DigestAuthentication,),
|
|
124 help='[projectdir],[htdigest_file],[realm]')
|
|
125 parser.add_option('--basic-auth', action='callback', type='string',
|
|
126 metavar='BASICAUTH', callback=_auth_callback,
|
|
127 callback_args=(BasicAuthentication,),
|
|
128 help='[projectdir],[htpasswd_file],[realm]')
|
|
129
|
|
130 parser.add_option('-p', '--port', action='store', type='int', dest='port',
|
|
131 help='the port number to bind to')
|
|
132 parser.add_option('-b', '--hostname', action='store', dest='hostname',
|
|
133 help='the host name or IP address to bind to')
|
|
134 parser.add_option('--protocol', action='callback', type="string",
|
|
135 dest='protocol', callback=_validate_callback,
|
|
136 callback_args=(('http', 'scgi', 'ajp'),),
|
|
137 help='http|scgi|ajp')
|
|
138 parser.add_option('-e', '--env-parent-dir', action='store',
|
|
139 dest='env_parent_dir', metavar='PARENTDIR',
|
|
140 help='parent directory of the project environments')
|
|
141 parser.add_option('--base-path', action='store', type='string', # XXX call this url_base_path?
|
|
142 dest='base_path',
|
|
143 help='base path')
|
|
144
|
|
145 parser.add_option('-r', '--auto-reload', action='store_true',
|
|
146 dest='autoreload',
|
|
147 help='restart automatically when sources are modified')
|
|
148
|
|
149 if os.name == 'posix':
|
|
150 parser.add_option('-d', '--daemonize', action='store_true',
|
|
151 dest='daemonize',
|
|
152 help='run in the background as a daemon')
|
|
153 parser.add_option('--pidfile', action='store',
|
|
154 dest='pidfile',
|
|
155 help='When daemonizing, file to which to write pid')
|
|
156
|
|
157 parser.set_defaults(port=None, hostname='', base_path='', daemonize=False,
|
|
158 protocol='http')
|
|
159 options, args = parser.parse_args()
|
|
160
|
|
161 if not args and not options.env_parent_dir:
|
|
162 parser.error('either the --env-parent-dir option or at least one '
|
|
163 'environment must be specified')
|
|
164
|
|
165 if options.port is None:
|
|
166 options.port = {
|
|
167 'http': 80,
|
|
168 'scgi': 4000,
|
|
169 'ajp': 8009,
|
|
170 }[options.protocol]
|
|
171 server_address = (options.hostname, options.port)
|
|
172
|
|
173 wsgi_app = TracEnvironMiddleware(dispatch_request,
|
|
174 options.env_parent_dir, args)
|
|
175 if auths:
|
|
176 wsgi_app = AuthenticationMiddleware(wsgi_app, auths)
|
|
177 base_path = options.base_path.strip('/')
|
|
178 if base_path:
|
|
179 wsgi_app = BasePathMiddleware(wsgi_app, base_path)
|
|
180
|
|
181 if options.protocol == 'http':
|
|
182 def serve():
|
|
183 httpd = TracHTTPServer(server_address, wsgi_app,
|
|
184 options.env_parent_dir, args)
|
|
185 httpd.serve_forever()
|
|
186 elif options.protocol in ('scgi', 'ajp'):
|
|
187 def serve():
|
|
188 server_cls = __import__('flup.server.%s' % options.protocol,
|
|
189 None, None, ['']).WSGIServer
|
|
190 ret = server_cls(wsgi_app, bindAddress=server_address).run()
|
|
191 sys.exit(ret and 42 or 0) # if SIGHUP exit with status 42
|
|
192
|
|
193 try:
|
|
194 if os.name == 'posix':
|
|
195 if options.pidfile:
|
|
196 options.pidfile = os.path.abspath(options.pidfile)
|
|
197 if os.path.exists(options.pidfile):
|
|
198 pidfile = open(options.pidfile)
|
|
199 try:
|
|
200 pid = int(pidfile.read())
|
|
201 finally:
|
|
202 pidfile.close()
|
|
203
|
|
204 try:
|
|
205 # signal the process to see if it is still running
|
|
206 os.kill(pid, 0)
|
|
207 except OSError, e:
|
|
208 if e.errno != errno.ESRCH:
|
|
209 raise
|
|
210 else:
|
|
211 sys.exit("tracd is already running with pid %s" % pid)
|
|
212 realserve = serve
|
|
213 def serve():
|
|
214 try:
|
|
215 pidfile = open(options.pidfile, 'w')
|
|
216 try:
|
|
217 pidfile.write(str(os.getpid()))
|
|
218 finally:
|
|
219 pidfile.close()
|
|
220 realserve()
|
|
221 finally:
|
|
222 if os.path.exists(options.pidfile):
|
|
223 os.remove(options.pidfile)
|
|
224
|
|
225 if options.daemonize:
|
|
226 daemon.daemonize()
|
|
227
|
|
228 if options.autoreload:
|
|
229 def modification_callback(file):
|
|
230 print>>sys.stderr, 'Detected modification of %s, restarting.' \
|
|
231 % file
|
|
232 autoreload.main(serve, modification_callback)
|
|
233 else:
|
|
234 serve()
|
|
235
|
|
236 except OSError:
|
|
237 sys.exit(1)
|
|
238 except KeyboardInterrupt:
|
|
239 pass
|
|
240
|
|
241 if __name__ == '__main__':
|
|
242 main()
|