#!/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 , 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(""" Moved Temporarily

Redirecting to ViewVC

You will be automatically redirected to ViewVC. If this doesn't work, please click on the link above.

""" % (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(""" Authentication failed

Authentication failed

Authentication has failed. Please retry with the correct username and password.

""") 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)