viewvc-4intranet/bin/standalone.py

584 lines
19 KiB
Python
Executable File

#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
#
# This program originally written by Peter Funk <pf@artcom-gmbh.de>, with
# contributions by Ka-Ping Yee.
#
# -----------------------------------------------------------------------
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, they will remain None.
#
LIBRARY_DIR = None
CONF_PATHNAME = None
import sys
import os
import os.path
import stat
import string
import urllib
import rfc822
import socket
import select
import base64
import BaseHTTPServer
if LIBRARY_DIR:
sys.path.insert(0, LIBRARY_DIR)
else:
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
import sapi
import viewvc
# The 'crypt' module is only available on Unix platforms. We'll try
# to use 'fcrypt' if it's available (for more information, see
# http://carey.geek.nz/code/python-fcrypt/).
has_crypt = False
try:
import crypt
has_crypt = True
def _check_passwd(user_passwd, real_passwd):
return real_passwd == crypt.crypt(user_passwd, real_passwd[:2])
except ImportError:
try:
import fcrypt
has_crypt = True
def _check_passwd(user_passwd, real_passwd):
return real_passwd == fcrypt.crypt(user_passwd, real_passwd[:2])
except ImportError:
def _check_passwd(user_passwd, real_passwd):
return False
class Options:
port = 49152 # default TCP/IP port used for the server
repositories = {} # use default repositories specified in config
host = sys.platform == 'mac' and '127.0.0.1' or 'localhost'
script_alias = 'viewvc'
config_file = None
htpasswd_file = None
class StandaloneServer(sapi.CgiServer):
"""Custom sapi interface that uses a BaseHTTPRequestHandler HANDLER
to generate output."""
def __init__(self, handler):
sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32")
self.handler = handler
def header(self, content_type='text/html', status=None):
if not self.headerSent:
self.headerSent = 1
if status is None:
statusCode = 200
statusText = 'OK'
else:
p = status.find(' ')
if p < 0:
statusCode = int(status)
statusText = ''
else:
statusCode = int(status[:p])
statusText = status[p+1:]
self.handler.send_response(statusCode, statusText)
self.handler.send_header("Content-type", content_type)
for (name, value) in self.headers:
self.handler.send_header(name, value)
self.handler.end_headers()
class NotViewVCLocationException(Exception):
"""The request location was not aimed at ViewVC."""
pass
class AuthenticationException(Exception):
"""Authentication requirements have not been met."""
pass
class ViewVCHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""Custom HTTP request handler for ViewVC."""
def do_GET(self):
"""Serve a GET request."""
self.handle_request('GET')
def do_POST(self):
"""Serve a POST request."""
self.handle_request('POST')
def handle_request(self, method):
"""Handle a request of type METHOD."""
try:
self.run_viewvc()
except NotViewVCLocationException:
# If the request was aimed at the server root, but there's a
# non-empty script_alias, automatically redirect to the
# script_alias. Otherwise, just return a 404 and shrug.
if (not self.path or self.path == "/") and options.script_alias:
new_url = self.server.url + options.script_alias + '/'
self.send_response(301, "Moved Permanently")
self.send_header("Content-type", "text/html")
self.send_header("Location", new_url)
self.end_headers()
self.wfile.write("""<html>
<head>
<meta http-equiv="refresh" content="10; url=%s" />
<title>Moved Temporarily</title>
</head>
<body>
<h1>Redirecting to ViewVC</h1>
<p>You will be automatically redirected to <a href="%s">ViewVC</a>.
If this doesn't work, please click on the link above.</p>
</body>
</html>
""" % (new_url, new_url))
else:
self.send_error(404)
except IOError: # ignore IOError: [Errno 32] Broken pipe
pass
except AuthenticationException:
self.send_response(401, "Unauthorized")
self.send_header("WWW-Authenticate", 'Basic realm="ViewVC"')
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write("""<html>
<head>
<title>Authentication failed</title>
</head>
<body>
<h1>Authentication failed</h1>
<p>Authentication has failed. Please retry with the correct username
and password.</p>
</body>
</html>""")
def is_viewvc(self):
"""Check whether self.path is, or is a child of, the ScriptAlias"""
if not options.script_alias:
return 1
if self.path == '/' + options.script_alias:
return 1
alias_len = len(options.script_alias)
if self.path[:alias_len+2] == '/' + options.script_alias + '/':
return 1
if self.path[:alias_len+2] == '/' + options.script_alias + '?':
return 1
return 0
def validate_password(self, htpasswd_file, username, password):
"""Compare USERNAME and PASSWORD against HTPASSWD_FILE."""
try:
lines = open(htpasswd_file, 'r').readlines()
for line in lines:
file_user, file_pass = line.rstrip().split(':', 1)
if username == file_user:
return _check_passwd(password, file_pass)
except:
pass
return False
def run_viewvc(self):
"""Run ViewVC to field a single request."""
### Much of this is adapter from Python's standard library
### module CGIHTTPServer.
# Is this request even aimed at ViewVC? If not, complain.
if not self.is_viewvc():
raise NotViewVCLocationException()
# If htpasswd authentication is enabled, try to authenticate the user.
self.username = None
if options.htpasswd_file:
authn = self.headers.get('authorization')
if not authn:
raise AuthenticationException()
try:
kind, data = authn.split(' ', 1)
if kind == 'Basic':
data = base64.b64decode(data)
username, password = data.split(':', 1)
except:
raise AuthenticationException()
if not self.validate_password(options.htpasswd_file, username, password):
raise AuthenticationException()
self.username = username
# Setup the environment in preparation of executing ViewVC's core code.
env = os.environ
scriptname = options.script_alias and '/' + options.script_alias or ''
viewvc_url = self.server.url[:-1] + scriptname
rest = self.path[len(scriptname):]
i = rest.rfind('?')
if i >= 0:
rest, query = rest[:i], rest[i+1:]
else:
query = ''
# Since we're going to modify the env in the parent, provide empty
# values to override previously set values
for k in env.keys():
if k[:5] == 'HTTP_':
del env[k]
for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
'HTTP_USER_AGENT', 'HTTP_COOKIE'):
if env.has_key(k):
env[k] = ""
# XXX Much of the following could be prepared ahead of time!
env['SERVER_SOFTWARE'] = self.version_string()
env['SERVER_NAME'] = self.server.server_name
env['GATEWAY_INTERFACE'] = 'CGI/1.1'
env['SERVER_PROTOCOL'] = self.protocol_version
env['SERVER_PORT'] = str(self.server.server_port)
env['REQUEST_METHOD'] = self.command
uqrest = urllib.unquote(rest)
env['PATH_INFO'] = uqrest
env['SCRIPT_NAME'] = scriptname
if query:
env['QUERY_STRING'] = query
env['HTTP_HOST'] = self.server.address[0]
host = self.address_string()
if host != self.client_address[0]:
env['REMOTE_HOST'] = host
env['REMOTE_ADDR'] = self.client_address[0]
if self.username:
env['REMOTE_USER'] = self.username
if self.headers.typeheader is None:
env['CONTENT_TYPE'] = self.headers.type
else:
env['CONTENT_TYPE'] = self.headers.typeheader
length = self.headers.getheader('content-length')
if length:
env['CONTENT_LENGTH'] = length
accept = []
for line in self.headers.getallmatchingheaders('accept'):
if line[:1] in string.whitespace:
accept.append(line.strip())
else:
accept = accept + line[7:].split(',')
env['HTTP_ACCEPT'] = ','.join(accept)
ua = self.headers.getheader('user-agent')
if ua:
env['HTTP_USER_AGENT'] = ua
modified = self.headers.getheader('if-modified-since')
if modified:
env['HTTP_IF_MODIFIED_SINCE'] = modified
etag = self.headers.getheader('if-none-match')
if etag:
env['HTTP_IF_NONE_MATCH'] = etag
# AUTH_TYPE
# REMOTE_IDENT
# XXX Other HTTP_* headers
# Preserve state, because we execute script in current process:
save_argv = sys.argv
save_stdin = sys.stdin
save_stdout = sys.stdout
save_stderr = sys.stderr
# For external tools like enscript we also need to redirect
# the real stdout file descriptor.
#
# FIXME: This code used to carry the following comment:
#
# (On windows, reassigning the sys.stdout variable is sufficient
# because pipe_cmds makes it the standard output for child
# processes.)
#
# But we no longer use pipe_cmds. So at the very least, the
# comment is stale. Is the code okay, though?
if sys.platform != "win32":
save_realstdout = os.dup(1)
try:
try:
sys.stdout = self.wfile
if sys.platform != "win32":
os.dup2(self.wfile.fileno(), 1)
sys.stdin = self.rfile
viewvc.main(StandaloneServer(self), cfg)
finally:
sys.argv = save_argv
sys.stdin = save_stdin
sys.stdout.flush()
if sys.platform != "win32":
os.dup2(save_realstdout, 1)
os.close(save_realstdout)
sys.stdout = save_stdout
sys.stderr = save_stderr
except SystemExit, status:
self.log_error("ViewVC exit status %s", str(status))
else:
self.log_error("ViewVC exited ok")
class ViewVCHTTPServer(BaseHTTPServer.HTTPServer):
"""Customized HTTP server for ViewVC."""
def __init__(self, host, port, callback):
self.address = (host, port)
self.url = 'http://%s:%d/' % (host, port)
self.callback = callback
BaseHTTPServer.HTTPServer.__init__(self, self.address, self.handler)
def serve_until_quit(self):
self.quit = 0
while not self.quit:
rd, wr, ex = select.select([self.socket.fileno()], [], [], 1)
if rd:
self.handle_request()
def server_activate(self):
BaseHTTPServer.HTTPServer.server_activate(self)
if self.callback:
self.callback(self)
def server_bind(self):
# set SO_REUSEADDR (if available on this platform)
if hasattr(socket, 'SOL_SOCKET') and hasattr(socket, 'SO_REUSEADDR'):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
BaseHTTPServer.HTTPServer.server_bind(self)
def serve(host, port, callback=None):
"""Start an HTTP server for HOST on PORT. Call CALLBACK function
when the server is ready to serve."""
ViewVCHTTPServer.handler = ViewVCHTTPRequestHandler
try:
# XXX Move this code out of this function.
# Early loading of configuration here. Used to allow tinkering
# with some configuration settings:
handle_config(options.config_file)
if options.repositories:
cfg.general.default_root = "Development"
for repo_name in options.repositories.keys():
repo_path = os.path.normpath(options.repositories[repo_name])
if os.path.exists(os.path.join(repo_path, "CVSROOT", "config")):
cfg.general.cvs_roots[repo_name] = repo_path
elif os.path.exists(os.path.join(repo_path, "format")):
cfg.general.svn_roots[repo_name] = repo_path
elif cfg.general.cvs_roots.has_key("Development") and \
not os.path.isdir(cfg.general.cvs_roots["Development"]):
sys.stderr.write("*** No repository found. Please use the -r option.\n")
sys.stderr.write(" Use --help for more info.\n")
raise KeyboardInterrupt # Hack!
os.close(0) # To avoid problems with shell job control
# always use default docroot location
cfg.options.docroot = None
# if cvsnt isn't found, fall back to rcs
if (cfg.conf_path is None and cfg.utilities.cvsnt):
import popen
cvsnt_works = 0
try:
fp = popen.popen(cfg.utilities.cvsnt, ['--version'], 'rt')
try:
while 1:
line = fp.readline()
if not line:
break
if line.find("Concurrent Versions System (CVSNT)") >= 0:
cvsnt_works = 1
while fp.read(4096):
pass
break
finally:
fp.close()
except:
pass
if not cvsnt_works:
cfg.utilities.cvsnt = None
ViewVCHTTPServer(host, port, callback).serve_until_quit()
except (KeyboardInterrupt, select.error):
pass
print 'server stopped'
def handle_config(config_file):
global cfg
cfg = viewvc.load_config(config_file or CONF_PATHNAME)
def usage():
clean_options = Options()
cmd = os.path.basename(sys.argv[0])
port = clean_options.port
host = clean_options.host
script_alias = clean_options.script_alias
sys.stderr.write("""Usage: %(cmd)s [OPTIONS]
Run a simple, standalone HTTP server configured to serve up ViewVC requests.
Options:
--config-file=FILE (-c) Read configuration options from FILE. If not
specified, ViewVC will look for a configuration
file in its installation tree, falling back to
built-in default values.
--daemon (-d) Background the server process.
--help Show this usage message and exit.
--host=HOSTNAME (-h) Listen on HOSTNAME. Required for access from a
remote machine. [default: %(host)s]
--htpasswd-file=FILE Authenticate incoming requests, validating against
against FILE, which is an Apache HTTP Server
htpasswd file. (CRYPT only; no DIGEST support.)
--port=PORT (-p) Listen on PORT. [default: %(port)d]
--repository=PATH (-r) Serve the Subversion or CVS repository located
at PATH. This option may be used more than once.
--script-alias=PATH (-s) Use PATH as the virtual script location (similar
to Apache HTTP Server's ScriptAlias directive).
For example, "--script-alias=repo/view" will serve
ViewVC at "http://HOSTNAME:PORT/repo/view".
[default: %(script_alias)s]
""" % locals())
sys.exit(0)
def badusage(errstr):
cmd = os.path.basename(sys.argv[0])
sys.stderr.write("ERROR: %s\n\n"
"Try '%s --help' for detailed usage information.\n"
% (errstr, cmd))
sys.exit(1)
def main(argv):
"""Command-line interface (looks at argv to decide what to do)."""
import getopt
short_opts = ''.join(['c:',
'd',
'h:',
'p:',
'r:',
's:',
])
long_opts = ['daemon',
'config-file=',
'help',
'host=',
'htpasswd-file=',
'port=',
'repository=',
'script-alias=',
]
opt_daemon = False
opt_host = None
opt_port = None
opt_htpasswd_file = None
opt_config_file = None
opt_script_alias = None
opt_repositories = []
# Parse command-line options.
try:
opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
for opt, val in opts:
if opt in ['--help']:
usage()
elif opt in ['-r', '--repository']: # may be used more than once
opt_repositories.append(val)
elif opt in ['-d', '--daemon']:
opt_daemon = 1
elif opt in ['-p', '--port']:
opt_port = val
elif opt in ['-h', '--host']:
opt_host = val
elif opt in ['-s', '--script-alias']:
opt_script_alias = val
elif opt in ['-c', '--config-file']:
opt_config_file = val
elif opt in ['--htpasswd-file']:
opt_htpasswd_file = val
except getopt.error, err:
badusage(str(err))
# Validate options that need validating.
class BadUsage(Exception): pass
try:
if opt_port is not None:
try:
options.port = int(opt_port)
except ValueError:
raise BadUsage("Port '%s' is not a valid port number" % (opt_port))
if not options.port:
raise BadUsage("You must supply a valid port.")
if opt_htpasswd_file is not None:
if not os.path.isfile(opt_htpasswd_file):
raise BadUsage("'%s' does not appear to be a valid htpasswd file."
% (opt_htpasswd_file))
if not has_crypt:
raise BadUsage("Unable to locate suitable `crypt' module for use "
"with --htpasswd-file option. If your Python "
"distribution does not include this module (as is "
"the case on many non-Unix platforms), consider "
"installing the `fcrypt' module instead (see "
"http://carey.geek.nz/code/python-fcrypt/).")
options.htpasswd_file = opt_htpasswd_file
if opt_config_file is not None:
if not os.path.isfile(opt_config_file):
raise BadUsage("'%s' does not appear to be a valid configuration file."
% (opt_config_file))
options.config_file = opt_config_file
if opt_host is not None:
options.host = opt_host
if opt_script_alias is not None:
options.script_alias = '/'.join(filter(None, opt_script_alias.split('/')))
for repository in opt_repositories:
if not options.repositories.has_key('Development'):
rootname = 'Development'
else:
rootname = 'Repository%d' % (len(options.repositories.keys()) + 1)
options.repositories[rootname] = repository
except BadUsage, err:
badusage(str(err))
# Fork if we're in daemon mode.
if opt_daemon:
pid = os.fork()
if pid != 0:
sys.exit()
# Finaly, start the server.
def ready(server):
print 'server ready at %s%s' % (server.url, options.script_alias)
serve(options.host, options.port, ready)
if __name__ == '__main__':
options = Options()
main(sys.argv)