# HG changeset patch # User Mads Kiilerich # Date 1602365900 -7200 # Node ID 89f11587b2dc824a31ede3f4ae794cccaa750788 # Parent e410c43aec4264fd5bc5fcdfa30f0a577d51ee35 config: move WSGI middleware apps from lib to config These middlewares are full WSGI applications - that is not so lib-ish. The middleware is referenced from the application in config - that seems like a good place for them to live. diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/application.py --- a/kallithea/config/application.py Tue Oct 13 19:07:59 2020 +0200 +++ b/kallithea/config/application.py Sat Oct 10 23:38:20 2020 +0200 @@ -14,11 +14,11 @@ """WSGI middleware initialization for the Kallithea application.""" from kallithea.config.app_cfg import base_config -from kallithea.lib.middleware.https_fixup import HttpsFixup -from kallithea.lib.middleware.permanent_repo_url import PermanentRepoUrl -from kallithea.lib.middleware.simplegit import SimpleGit -from kallithea.lib.middleware.simplehg import SimpleHg -from kallithea.lib.middleware.wrapper import RequestWrapper +from kallithea.config.middleware.https_fixup import HttpsFixup +from kallithea.config.middleware.permanent_repo_url import PermanentRepoUrl +from kallithea.config.middleware.simplegit import SimpleGit +from kallithea.config.middleware.simplehg import SimpleHg +from kallithea.config.middleware.wrapper import RequestWrapper from kallithea.lib.utils2 import asbool diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/__init__.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/appenlight.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/appenlight.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +kallithea.lib.middleware.appenlight +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +middleware to handle appenlight publishing of errors + +This file was forked by the Kallithea project in July 2014. +Original author and date, and relevant copyright and licensing information is below: +:created_on: October 18, 2012 +:author: marcink +:copyright: (c) 2013 RhodeCode GmbH, and others. +:license: GPLv3, see LICENSE.md for more details. +""" + + +try: + from appenlight_client import make_appenlight_middleware +except ImportError: + AppEnlight = None +else: + AppEnlight = make_appenlight_middleware diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/https_fixup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/https_fixup.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +kallithea.lib.middleware.https_fixup +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +middleware to handle https correctly + +This file was forked by the Kallithea project in July 2014. +Original author and date, and relevant copyright and licensing information is below: +:created_on: May 23, 2010 +:author: marcink +:copyright: (c) 2013 RhodeCode GmbH, and others. +:license: GPLv3, see LICENSE.md for more details. +""" + + +from kallithea.lib.utils2 import asbool + + +class HttpsFixup(object): + + def __init__(self, app, config): + self.application = app + self.config = config + + def __call__(self, environ, start_response): + self.__fixup(environ) + debug = asbool(self.config.get('debug')) + is_ssl = environ['wsgi.url_scheme'] == 'https' + + def custom_start_response(status, headers, exc_info=None): + if is_ssl and asbool(self.config.get('use_htsts')) and not debug: + headers.append(('Strict-Transport-Security', + 'max-age=8640000; includeSubDomains')) + return start_response(status, headers, exc_info) + + return self.application(environ, custom_start_response) + + def __fixup(self, environ): + """ + Function to fixup the environ as needed. In order to use this + middleware you should set this header inside your + proxy ie. nginx, apache etc. + """ + # DETECT PROTOCOL ! + if 'HTTP_X_URL_SCHEME' in environ: + proto = environ.get('HTTP_X_URL_SCHEME') + elif 'HTTP_X_FORWARDED_SCHEME' in environ: + proto = environ.get('HTTP_X_FORWARDED_SCHEME') + elif 'HTTP_X_FORWARDED_PROTO' in environ: + proto = environ.get('HTTP_X_FORWARDED_PROTO') + else: + proto = 'http' + org_proto = proto + + # if we have force, just override + if asbool(self.config.get('force_https')): + proto = 'https' + + environ['wsgi.url_scheme'] = proto + environ['wsgi._org_proto'] = org_proto diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/permanent_repo_url.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/permanent_repo_url.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +kallithea.lib.middleware.permanent_repo_url +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +middleware to handle permanent repo URLs, replacing PATH_INFO '/_123/yada' with +'/name/of/repo/yada' after looking 123 up in the database. +""" + + +from kallithea.lib.utils import fix_repo_id_name +from kallithea.lib.utils2 import safe_bytes, safe_str + + +class PermanentRepoUrl(object): + + def __init__(self, app, config): + self.application = app + self.config = config + + def __call__(self, environ, start_response): + # Extract path_info as get_path_info does, but do it explicitly because + # we also have to do the reverse operation when patching it back in + path_info = safe_str(environ['PATH_INFO'].encode('latin1')) + if path_info.startswith('/'): # it must + path_info = '/' + fix_repo_id_name(path_info[1:]) + environ['PATH_INFO'] = safe_bytes(path_info).decode('latin1') + + return self.application(environ, start_response) diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/pygrack.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/pygrack.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +kallithea.lib.middleware.pygrack +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Python implementation of git-http-backend's Smart HTTP protocol + +Based on original code from git_http_backend.py project. + +Copyright (c) 2010 Daniel Dotsenko +Copyright (c) 2012 Marcin Kuzminski + +This file was forked by the Kallithea project in July 2014. +""" + +import logging +import os +import socket +import traceback + +from dulwich.server import update_server_info +from dulwich.web import GunzipFilter, LimitedInputFilter +from webob import Request, Response, exc + +import kallithea +from kallithea.lib.utils2 import ascii_bytes +from kallithea.lib.vcs import subprocessio + + +log = logging.getLogger(__name__) + + +class FileWrapper(object): + + def __init__(self, fd, content_length): + self.fd = fd + self.content_length = content_length + self.remain = content_length + + def read(self, size): + if size <= self.remain: + try: + data = self.fd.read(size) + except socket.error: + raise IOError(self) + self.remain -= size + elif self.remain: + data = self.fd.read(self.remain) + self.remain = 0 + else: + data = None + return data + + def __repr__(self): + return '' % ( + self.fd, self.content_length, self.content_length - self.remain + ) + + +class GitRepository(object): + git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) + commands = ['git-upload-pack', 'git-receive-pack'] + + def __init__(self, repo_name, content_path): + files = set([f.lower() for f in os.listdir(content_path)]) + if not (self.git_folder_signature.intersection(files) + == self.git_folder_signature): + raise OSError('%s missing git signature' % content_path) + self.content_path = content_path + self.valid_accepts = ['application/x-%s-result' % + c for c in self.commands] + self.repo_name = repo_name + + def _get_fixedpath(self, path): + """ + Small fix for repo_path + + :param path: + """ + assert path.startswith('/' + self.repo_name + '/') + return path[len(self.repo_name) + 2:].strip('/') + + def inforefs(self, req, environ): + """ + WSGI Response producer for HTTP GET Git Smart + HTTP /info/refs request. + """ + + git_command = req.GET.get('service') + if git_command not in self.commands: + log.debug('command %s not allowed', git_command) + return exc.HTTPMethodNotAllowed() + + # From Documentation/technical/http-protocol.txt shipped with Git: + # + # Clients MUST verify the first pkt-line is `# service=$servicename`. + # Servers MUST set $servicename to be the request parameter value. + # Servers SHOULD include an LF at the end of this line. + # Clients MUST ignore an LF at the end of the line. + # + # smart_reply = PKT-LINE("# service=$servicename" LF) + # ref_list + # "0000" + server_advert = '# service=%s\n' % git_command + packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower() + _git_path = kallithea.CONFIG.get('git_path', 'git') + cmd = [_git_path, git_command[4:], + '--stateless-rpc', '--advertise-refs', self.content_path] + log.debug('handling cmd %s', cmd) + try: + out = subprocessio.SubprocessIOChunker(cmd, + starting_values=[ascii_bytes(packet_len + server_advert + '0000')] + ) + except EnvironmentError as e: + log.error(traceback.format_exc()) + raise exc.HTTPExpectationFailed() + resp = Response() + resp.content_type = 'application/x-%s-advertisement' % str(git_command) + resp.charset = None + resp.app_iter = out + return resp + + def backend(self, req, environ): + """ + WSGI Response producer for HTTP POST Git Smart HTTP requests. + Reads commands and data from HTTP POST's body. + returns an iterator obj with contents of git command's + response to stdout + """ + _git_path = kallithea.CONFIG.get('git_path', 'git') + git_command = self._get_fixedpath(req.path_info) + if git_command not in self.commands: + log.debug('command %s not allowed', git_command) + return exc.HTTPMethodNotAllowed() + + if 'CONTENT_LENGTH' in environ: + inputstream = FileWrapper(environ['wsgi.input'], + req.content_length) + else: + inputstream = environ['wsgi.input'] + + gitenv = dict(os.environ) + # forget all configs + gitenv['GIT_CONFIG_NOGLOBAL'] = '1' + cmd = [_git_path, git_command[4:], '--stateless-rpc', self.content_path] + log.debug('handling cmd %s', cmd) + try: + out = subprocessio.SubprocessIOChunker( + cmd, + inputstream=inputstream, + env=gitenv, + cwd=self.content_path, + ) + except EnvironmentError as e: + log.error(traceback.format_exc()) + raise exc.HTTPExpectationFailed() + + if git_command in ['git-receive-pack']: + # updating refs manually after each push. + # Needed for pre-1.7.0.4 git clients using regular HTTP mode. + + from kallithea.lib.vcs import get_repo + repo = get_repo(self.content_path) + if repo: + update_server_info(repo._repo) + + resp = Response() + resp.content_type = 'application/x-%s-result' % git_command.encode('utf-8') + resp.charset = None + resp.app_iter = out + return resp + + def __call__(self, environ, start_response): + req = Request(environ) + _path = self._get_fixedpath(req.path_info) + if _path.startswith('info/refs'): + app = self.inforefs + elif req.accept.acceptable_offers(self.valid_accepts): + app = self.backend + try: + resp = app(req, environ) + except exc.HTTPException as e: + resp = e + log.error(traceback.format_exc()) + except Exception as e: + log.error(traceback.format_exc()) + resp = exc.HTTPInternalServerError() + return resp(environ, start_response) + + +class GitDirectory(object): + + def __init__(self, repo_root, repo_name): + repo_location = os.path.join(repo_root, repo_name) + if not os.path.isdir(repo_location): + raise OSError(repo_location) + + self.content_path = repo_location + self.repo_name = repo_name + self.repo_location = repo_location + + def __call__(self, environ, start_response): + content_path = self.content_path + try: + app = GitRepository(self.repo_name, content_path) + except (AssertionError, OSError): + content_path = os.path.join(content_path, '.git') + if os.path.isdir(content_path): + app = GitRepository(self.repo_name, content_path) + else: + return exc.HTTPNotFound()(environ, start_response) + return app(environ, start_response) + + +def make_wsgi_app(repo_name, repo_root): + app = GitDirectory(repo_root, repo_name) + return GunzipFilter(LimitedInputFilter(app)) diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/simplegit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/simplegit.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +kallithea.lib.middleware.simplegit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +SimpleGit middleware for handling Git protocol requests (push/clone etc.) +It's implemented with basic auth function + +This file was forked by the Kallithea project in July 2014. +Original author and date, and relevant copyright and licensing information is below: +:created_on: Apr 28, 2010 +:author: marcink +:copyright: (c) 2013 RhodeCode GmbH, and others. +:license: GPLv3, see LICENSE.md for more details. + +""" + + +import logging +import re + +from kallithea.config.middleware.pygrack import make_wsgi_app +from kallithea.lib.base import BaseVCSController, get_path_info +from kallithea.lib.hooks import log_pull_action +from kallithea.lib.utils import make_ui +from kallithea.model.db import Repository + + +log = logging.getLogger(__name__) + + +GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)$') + + +cmd_mapping = { + 'git-receive-pack': 'push', + 'git-upload-pack': 'pull', +} + + +class SimpleGit(BaseVCSController): + + scm_alias = 'git' + + @classmethod + def parse_request(cls, environ): + path_info = get_path_info(environ) + m = GIT_PROTO_PAT.match(path_info) + if m is None: + return None + + class parsed_request(object): + # See https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols#_the_smart_protocol + repo_name = m.group(1).rstrip('/') + cmd = m.group(2) + + query_string = environ['QUERY_STRING'] + if cmd == 'info/refs' and query_string.startswith('service='): + service = query_string.split('=', 1)[1] + action = cmd_mapping.get(service) + else: + service = None + action = cmd_mapping.get(cmd) + + return parsed_request + + def _make_app(self, parsed_request): + """ + Return a pygrack wsgi application. + """ + pygrack_app = make_wsgi_app(parsed_request.repo_name, self.basepath) + + def wrapper_app(environ, start_response): + if (parsed_request.cmd == 'info/refs' and + parsed_request.service == 'git-upload-pack' + ): + baseui = make_ui() + repo = Repository.get_by_repo_name(parsed_request.repo_name) + scm_repo = repo.scm_instance + # Run hooks, like Mercurial outgoing.pull_logger does + log_pull_action(ui=baseui, repo=scm_repo._repo) + # Note: push hooks are handled by post-receive hook + + return pygrack_app(environ, start_response) + + return wrapper_app diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/simplehg.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/simplehg.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +kallithea.lib.middleware.simplehg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +SimpleHg middleware for handling Mercurial protocol requests (push/clone etc.). +It's implemented with basic auth function + +This file was forked by the Kallithea project in July 2014. +Original author and date, and relevant copyright and licensing information is below: +:created_on: Apr 28, 2010 +:author: marcink +:copyright: (c) 2013 RhodeCode GmbH, and others. +:license: GPLv3, see LICENSE.md for more details. + +""" + + +import logging +import os +import urllib.parse + +import mercurial.hgweb + +from kallithea.lib.base import BaseVCSController, get_path_info +from kallithea.lib.utils import make_ui +from kallithea.lib.utils2 import safe_bytes + + +log = logging.getLogger(__name__) + + +def get_header_hgarg(environ): + """Decode the special Mercurial encoding of big requests over multiple headers. + >>> get_header_hgarg({}) + '' + >>> get_header_hgarg({'HTTP_X_HGARG_0': ' ', 'HTTP_X_HGARG_1': 'a','HTTP_X_HGARG_2': '','HTTP_X_HGARG_3': 'b+c %20'}) + 'ab+c %20' + """ + chunks = [] + i = 1 + while True: + v = environ.get('HTTP_X_HGARG_%d' % i) + if v is None: + break + chunks.append(v) + i += 1 + return ''.join(chunks) + + +cmd_mapping = { + # 'batch' is not in this list - it is handled explicitly + 'between': 'pull', + 'branches': 'pull', + 'branchmap': 'pull', + 'capabilities': 'pull', + 'changegroup': 'pull', + 'changegroupsubset': 'pull', + 'changesetdata': 'pull', + 'clonebundles': 'pull', + 'debugwireargs': 'pull', + 'filedata': 'pull', + 'getbundle': 'pull', + 'getlfile': 'pull', + 'heads': 'pull', + 'hello': 'pull', + 'known': 'pull', + 'lheads': 'pull', + 'listkeys': 'pull', + 'lookup': 'pull', + 'manifestdata': 'pull', + 'narrow_widen': 'pull', + 'protocaps': 'pull', + 'statlfile': 'pull', + 'stream_out': 'pull', + 'pushkey': 'push', + 'putlfile': 'push', + 'unbundle': 'push', + } + + +class SimpleHg(BaseVCSController): + + scm_alias = 'hg' + + @classmethod + def parse_request(cls, environ): + http_accept = environ.get('HTTP_ACCEPT', '') + if not http_accept.startswith('application/mercurial'): + return None + path_info = get_path_info(environ) + if not path_info.startswith('/'): # it must! + return None + + class parsed_request(object): + repo_name = path_info[1:].rstrip('/') + + query_string = environ['QUERY_STRING'] + + action = None + for qry in query_string.split('&'): + parts = qry.split('=', 1) + if len(parts) == 2 and parts[0] == 'cmd': + cmd = parts[1] + if cmd == 'batch': + hgarg = get_header_hgarg(environ) + if not hgarg.startswith('cmds='): + action = 'push' # paranoid and safe + break + action = 'pull' + for cmd_arg in hgarg[5:].split(';'): + cmd, _args = urllib.parse.unquote_plus(cmd_arg).split(' ', 1) + op = cmd_mapping.get(cmd, 'push') + if op != 'pull': + assert op == 'push' + action = 'push' + break + else: + action = cmd_mapping.get(cmd, 'push') + break # only process one cmd + + return parsed_request + + def _make_app(self, parsed_request): + """ + Make an hgweb wsgi application. + """ + repo_name = parsed_request.repo_name + repo_path = os.path.join(self.basepath, repo_name) + baseui = make_ui(repo_path=repo_path) + hgweb_app = mercurial.hgweb.hgweb(safe_bytes(repo_path), name=safe_bytes(repo_name), baseui=baseui) + + def wrapper_app(environ, start_response): + environ['REPO_NAME'] = repo_name # used by mercurial.hgweb.hgweb + return hgweb_app(environ, start_response) + + return wrapper_app diff -r e410c43aec42 -r 89f11587b2dc kallithea/config/middleware/wrapper.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/config/middleware/wrapper.py Sat Oct 10 23:38:20 2020 +0200 @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +kallithea.lib.middleware.wrapper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Wrap app to measure request and response time ... all the way to the response +WSGI iterator has been closed. + +This file was forked by the Kallithea project in July 2014. +Original author and date, and relevant copyright and licensing information is below: +:created_on: May 23, 2013 +:author: marcink +:copyright: (c) 2013 RhodeCode GmbH, and others. +:license: GPLv3, see LICENSE.md for more details. +""" + +import logging +import time + +from kallithea.lib.base import _get_ip_addr, get_path_info + + +log = logging.getLogger(__name__) + + +class Meter: + + def __init__(self, start_response): + self._start_response = start_response + self._start = time.time() + self.status = None + self._size = 0 + + def duration(self): + return time.time() - self._start + + def start_response(self, status, response_headers, exc_info=None): + self.status = status + write = self._start_response(status, response_headers, exc_info) + def metered_write(s): + self.measure(s) + write(s) + return metered_write + + def measure(self, chunk): + self._size += len(chunk) + + def size(self): + return self._size + + +class ResultIter: + + def __init__(self, result, meter, description): + self._result_close = getattr(result, 'close', None) or (lambda: None) + self._next = iter(result).__next__ + self._meter = meter + self._description = description + + def __iter__(self): + return self + + def __next__(self): + chunk = self._next() + self._meter.measure(chunk) + return chunk + + def close(self): + self._result_close() + log.info("%s responded %r after %.3fs with %s bytes", self._description, self._meter.status, self._meter.duration(), self._meter.size()) + + +class RequestWrapper(object): + + def __init__(self, app, config): + self.application = app + self.config = config + + def __call__(self, environ, start_response): + meter = Meter(start_response) + description = "Request from %s for %s" % ( + _get_ip_addr(environ), + get_path_info(environ), + ) + log.info("%s received", description) + try: + result = self.application(environ, meter.start_response) + finally: + log.info("%s responding %r after %.3fs", description, meter.status, meter.duration()) + return ResultIter(result, meter, description) diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/__init__.py --- a/kallithea/lib/middleware/__init__.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/appenlight.py --- a/kallithea/lib/middleware/appenlight.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -kallithea.lib.middleware.appenlight -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -middleware to handle appenlight publishing of errors - -This file was forked by the Kallithea project in July 2014. -Original author and date, and relevant copyright and licensing information is below: -:created_on: October 18, 2012 -:author: marcink -:copyright: (c) 2013 RhodeCode GmbH, and others. -:license: GPLv3, see LICENSE.md for more details. -""" - - -try: - from appenlight_client import make_appenlight_middleware -except ImportError: - AppEnlight = None -else: - AppEnlight = make_appenlight_middleware diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/https_fixup.py --- a/kallithea/lib/middleware/https_fixup.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -kallithea.lib.middleware.https_fixup -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -middleware to handle https correctly - -This file was forked by the Kallithea project in July 2014. -Original author and date, and relevant copyright and licensing information is below: -:created_on: May 23, 2010 -:author: marcink -:copyright: (c) 2013 RhodeCode GmbH, and others. -:license: GPLv3, see LICENSE.md for more details. -""" - - -from kallithea.lib.utils2 import asbool - - -class HttpsFixup(object): - - def __init__(self, app, config): - self.application = app - self.config = config - - def __call__(self, environ, start_response): - self.__fixup(environ) - debug = asbool(self.config.get('debug')) - is_ssl = environ['wsgi.url_scheme'] == 'https' - - def custom_start_response(status, headers, exc_info=None): - if is_ssl and asbool(self.config.get('use_htsts')) and not debug: - headers.append(('Strict-Transport-Security', - 'max-age=8640000; includeSubDomains')) - return start_response(status, headers, exc_info) - - return self.application(environ, custom_start_response) - - def __fixup(self, environ): - """ - Function to fixup the environ as needed. In order to use this - middleware you should set this header inside your - proxy ie. nginx, apache etc. - """ - # DETECT PROTOCOL ! - if 'HTTP_X_URL_SCHEME' in environ: - proto = environ.get('HTTP_X_URL_SCHEME') - elif 'HTTP_X_FORWARDED_SCHEME' in environ: - proto = environ.get('HTTP_X_FORWARDED_SCHEME') - elif 'HTTP_X_FORWARDED_PROTO' in environ: - proto = environ.get('HTTP_X_FORWARDED_PROTO') - else: - proto = 'http' - org_proto = proto - - # if we have force, just override - if asbool(self.config.get('force_https')): - proto = 'https' - - environ['wsgi.url_scheme'] = proto - environ['wsgi._org_proto'] = org_proto diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/permanent_repo_url.py --- a/kallithea/lib/middleware/permanent_repo_url.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -kallithea.lib.middleware.permanent_repo_url -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -middleware to handle permanent repo URLs, replacing PATH_INFO '/_123/yada' with -'/name/of/repo/yada' after looking 123 up in the database. -""" - - -from kallithea.lib.utils import fix_repo_id_name -from kallithea.lib.utils2 import safe_bytes, safe_str - - -class PermanentRepoUrl(object): - - def __init__(self, app, config): - self.application = app - self.config = config - - def __call__(self, environ, start_response): - # Extract path_info as get_path_info does, but do it explicitly because - # we also have to do the reverse operation when patching it back in - path_info = safe_str(environ['PATH_INFO'].encode('latin1')) - if path_info.startswith('/'): # it must - path_info = '/' + fix_repo_id_name(path_info[1:]) - environ['PATH_INFO'] = safe_bytes(path_info).decode('latin1') - - return self.application(environ, start_response) diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/pygrack.py --- a/kallithea/lib/middleware/pygrack.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -kallithea.lib.middleware.pygrack -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Python implementation of git-http-backend's Smart HTTP protocol - -Based on original code from git_http_backend.py project. - -Copyright (c) 2010 Daniel Dotsenko -Copyright (c) 2012 Marcin Kuzminski - -This file was forked by the Kallithea project in July 2014. -""" - -import logging -import os -import socket -import traceback - -from dulwich.server import update_server_info -from dulwich.web import GunzipFilter, LimitedInputFilter -from webob import Request, Response, exc - -import kallithea -from kallithea.lib.utils2 import ascii_bytes -from kallithea.lib.vcs import subprocessio - - -log = logging.getLogger(__name__) - - -class FileWrapper(object): - - def __init__(self, fd, content_length): - self.fd = fd - self.content_length = content_length - self.remain = content_length - - def read(self, size): - if size <= self.remain: - try: - data = self.fd.read(size) - except socket.error: - raise IOError(self) - self.remain -= size - elif self.remain: - data = self.fd.read(self.remain) - self.remain = 0 - else: - data = None - return data - - def __repr__(self): - return '' % ( - self.fd, self.content_length, self.content_length - self.remain - ) - - -class GitRepository(object): - git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) - commands = ['git-upload-pack', 'git-receive-pack'] - - def __init__(self, repo_name, content_path): - files = set([f.lower() for f in os.listdir(content_path)]) - if not (self.git_folder_signature.intersection(files) - == self.git_folder_signature): - raise OSError('%s missing git signature' % content_path) - self.content_path = content_path - self.valid_accepts = ['application/x-%s-result' % - c for c in self.commands] - self.repo_name = repo_name - - def _get_fixedpath(self, path): - """ - Small fix for repo_path - - :param path: - """ - assert path.startswith('/' + self.repo_name + '/') - return path[len(self.repo_name) + 2:].strip('/') - - def inforefs(self, req, environ): - """ - WSGI Response producer for HTTP GET Git Smart - HTTP /info/refs request. - """ - - git_command = req.GET.get('service') - if git_command not in self.commands: - log.debug('command %s not allowed', git_command) - return exc.HTTPMethodNotAllowed() - - # From Documentation/technical/http-protocol.txt shipped with Git: - # - # Clients MUST verify the first pkt-line is `# service=$servicename`. - # Servers MUST set $servicename to be the request parameter value. - # Servers SHOULD include an LF at the end of this line. - # Clients MUST ignore an LF at the end of the line. - # - # smart_reply = PKT-LINE("# service=$servicename" LF) - # ref_list - # "0000" - server_advert = '# service=%s\n' % git_command - packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower() - _git_path = kallithea.CONFIG.get('git_path', 'git') - cmd = [_git_path, git_command[4:], - '--stateless-rpc', '--advertise-refs', self.content_path] - log.debug('handling cmd %s', cmd) - try: - out = subprocessio.SubprocessIOChunker(cmd, - starting_values=[ascii_bytes(packet_len + server_advert + '0000')] - ) - except EnvironmentError as e: - log.error(traceback.format_exc()) - raise exc.HTTPExpectationFailed() - resp = Response() - resp.content_type = 'application/x-%s-advertisement' % str(git_command) - resp.charset = None - resp.app_iter = out - return resp - - def backend(self, req, environ): - """ - WSGI Response producer for HTTP POST Git Smart HTTP requests. - Reads commands and data from HTTP POST's body. - returns an iterator obj with contents of git command's - response to stdout - """ - _git_path = kallithea.CONFIG.get('git_path', 'git') - git_command = self._get_fixedpath(req.path_info) - if git_command not in self.commands: - log.debug('command %s not allowed', git_command) - return exc.HTTPMethodNotAllowed() - - if 'CONTENT_LENGTH' in environ: - inputstream = FileWrapper(environ['wsgi.input'], - req.content_length) - else: - inputstream = environ['wsgi.input'] - - gitenv = dict(os.environ) - # forget all configs - gitenv['GIT_CONFIG_NOGLOBAL'] = '1' - cmd = [_git_path, git_command[4:], '--stateless-rpc', self.content_path] - log.debug('handling cmd %s', cmd) - try: - out = subprocessio.SubprocessIOChunker( - cmd, - inputstream=inputstream, - env=gitenv, - cwd=self.content_path, - ) - except EnvironmentError as e: - log.error(traceback.format_exc()) - raise exc.HTTPExpectationFailed() - - if git_command in ['git-receive-pack']: - # updating refs manually after each push. - # Needed for pre-1.7.0.4 git clients using regular HTTP mode. - - from kallithea.lib.vcs import get_repo - repo = get_repo(self.content_path) - if repo: - update_server_info(repo._repo) - - resp = Response() - resp.content_type = 'application/x-%s-result' % git_command.encode('utf-8') - resp.charset = None - resp.app_iter = out - return resp - - def __call__(self, environ, start_response): - req = Request(environ) - _path = self._get_fixedpath(req.path_info) - if _path.startswith('info/refs'): - app = self.inforefs - elif req.accept.acceptable_offers(self.valid_accepts): - app = self.backend - try: - resp = app(req, environ) - except exc.HTTPException as e: - resp = e - log.error(traceback.format_exc()) - except Exception as e: - log.error(traceback.format_exc()) - resp = exc.HTTPInternalServerError() - return resp(environ, start_response) - - -class GitDirectory(object): - - def __init__(self, repo_root, repo_name): - repo_location = os.path.join(repo_root, repo_name) - if not os.path.isdir(repo_location): - raise OSError(repo_location) - - self.content_path = repo_location - self.repo_name = repo_name - self.repo_location = repo_location - - def __call__(self, environ, start_response): - content_path = self.content_path - try: - app = GitRepository(self.repo_name, content_path) - except (AssertionError, OSError): - content_path = os.path.join(content_path, '.git') - if os.path.isdir(content_path): - app = GitRepository(self.repo_name, content_path) - else: - return exc.HTTPNotFound()(environ, start_response) - return app(environ, start_response) - - -def make_wsgi_app(repo_name, repo_root): - app = GitDirectory(repo_root, repo_name) - return GunzipFilter(LimitedInputFilter(app)) diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/simplegit.py --- a/kallithea/lib/middleware/simplegit.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -kallithea.lib.middleware.simplegit -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -SimpleGit middleware for handling Git protocol requests (push/clone etc.) -It's implemented with basic auth function - -This file was forked by the Kallithea project in July 2014. -Original author and date, and relevant copyright and licensing information is below: -:created_on: Apr 28, 2010 -:author: marcink -:copyright: (c) 2013 RhodeCode GmbH, and others. -:license: GPLv3, see LICENSE.md for more details. - -""" - - -import logging -import re - -from kallithea.lib.base import BaseVCSController, get_path_info -from kallithea.lib.hooks import log_pull_action -from kallithea.lib.middleware.pygrack import make_wsgi_app -from kallithea.lib.utils import make_ui -from kallithea.model.db import Repository - - -log = logging.getLogger(__name__) - - -GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)$') - - -cmd_mapping = { - 'git-receive-pack': 'push', - 'git-upload-pack': 'pull', -} - - -class SimpleGit(BaseVCSController): - - scm_alias = 'git' - - @classmethod - def parse_request(cls, environ): - path_info = get_path_info(environ) - m = GIT_PROTO_PAT.match(path_info) - if m is None: - return None - - class parsed_request(object): - # See https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols#_the_smart_protocol - repo_name = m.group(1).rstrip('/') - cmd = m.group(2) - - query_string = environ['QUERY_STRING'] - if cmd == 'info/refs' and query_string.startswith('service='): - service = query_string.split('=', 1)[1] - action = cmd_mapping.get(service) - else: - service = None - action = cmd_mapping.get(cmd) - - return parsed_request - - def _make_app(self, parsed_request): - """ - Return a pygrack wsgi application. - """ - pygrack_app = make_wsgi_app(parsed_request.repo_name, self.basepath) - - def wrapper_app(environ, start_response): - if (parsed_request.cmd == 'info/refs' and - parsed_request.service == 'git-upload-pack' - ): - baseui = make_ui() - repo = Repository.get_by_repo_name(parsed_request.repo_name) - scm_repo = repo.scm_instance - # Run hooks, like Mercurial outgoing.pull_logger does - log_pull_action(ui=baseui, repo=scm_repo._repo) - # Note: push hooks are handled by post-receive hook - - return pygrack_app(environ, start_response) - - return wrapper_app diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/simplehg.py --- a/kallithea/lib/middleware/simplehg.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -kallithea.lib.middleware.simplehg -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -SimpleHg middleware for handling Mercurial protocol requests (push/clone etc.). -It's implemented with basic auth function - -This file was forked by the Kallithea project in July 2014. -Original author and date, and relevant copyright and licensing information is below: -:created_on: Apr 28, 2010 -:author: marcink -:copyright: (c) 2013 RhodeCode GmbH, and others. -:license: GPLv3, see LICENSE.md for more details. - -""" - - -import logging -import os -import urllib.parse - -import mercurial.hgweb - -from kallithea.lib.base import BaseVCSController, get_path_info -from kallithea.lib.utils import make_ui -from kallithea.lib.utils2 import safe_bytes - - -log = logging.getLogger(__name__) - - -def get_header_hgarg(environ): - """Decode the special Mercurial encoding of big requests over multiple headers. - >>> get_header_hgarg({}) - '' - >>> get_header_hgarg({'HTTP_X_HGARG_0': ' ', 'HTTP_X_HGARG_1': 'a','HTTP_X_HGARG_2': '','HTTP_X_HGARG_3': 'b+c %20'}) - 'ab+c %20' - """ - chunks = [] - i = 1 - while True: - v = environ.get('HTTP_X_HGARG_%d' % i) - if v is None: - break - chunks.append(v) - i += 1 - return ''.join(chunks) - - -cmd_mapping = { - # 'batch' is not in this list - it is handled explicitly - 'between': 'pull', - 'branches': 'pull', - 'branchmap': 'pull', - 'capabilities': 'pull', - 'changegroup': 'pull', - 'changegroupsubset': 'pull', - 'changesetdata': 'pull', - 'clonebundles': 'pull', - 'debugwireargs': 'pull', - 'filedata': 'pull', - 'getbundle': 'pull', - 'getlfile': 'pull', - 'heads': 'pull', - 'hello': 'pull', - 'known': 'pull', - 'lheads': 'pull', - 'listkeys': 'pull', - 'lookup': 'pull', - 'manifestdata': 'pull', - 'narrow_widen': 'pull', - 'protocaps': 'pull', - 'statlfile': 'pull', - 'stream_out': 'pull', - 'pushkey': 'push', - 'putlfile': 'push', - 'unbundle': 'push', - } - - -class SimpleHg(BaseVCSController): - - scm_alias = 'hg' - - @classmethod - def parse_request(cls, environ): - http_accept = environ.get('HTTP_ACCEPT', '') - if not http_accept.startswith('application/mercurial'): - return None - path_info = get_path_info(environ) - if not path_info.startswith('/'): # it must! - return None - - class parsed_request(object): - repo_name = path_info[1:].rstrip('/') - - query_string = environ['QUERY_STRING'] - - action = None - for qry in query_string.split('&'): - parts = qry.split('=', 1) - if len(parts) == 2 and parts[0] == 'cmd': - cmd = parts[1] - if cmd == 'batch': - hgarg = get_header_hgarg(environ) - if not hgarg.startswith('cmds='): - action = 'push' # paranoid and safe - break - action = 'pull' - for cmd_arg in hgarg[5:].split(';'): - cmd, _args = urllib.parse.unquote_plus(cmd_arg).split(' ', 1) - op = cmd_mapping.get(cmd, 'push') - if op != 'pull': - assert op == 'push' - action = 'push' - break - else: - action = cmd_mapping.get(cmd, 'push') - break # only process one cmd - - return parsed_request - - def _make_app(self, parsed_request): - """ - Make an hgweb wsgi application. - """ - repo_name = parsed_request.repo_name - repo_path = os.path.join(self.basepath, repo_name) - baseui = make_ui(repo_path=repo_path) - hgweb_app = mercurial.hgweb.hgweb(safe_bytes(repo_path), name=safe_bytes(repo_name), baseui=baseui) - - def wrapper_app(environ, start_response): - environ['REPO_NAME'] = repo_name # used by mercurial.hgweb.hgweb - return hgweb_app(environ, start_response) - - return wrapper_app diff -r e410c43aec42 -r 89f11587b2dc kallithea/lib/middleware/wrapper.py --- a/kallithea/lib/middleware/wrapper.py Tue Oct 13 19:07:59 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -kallithea.lib.middleware.wrapper -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Wrap app to measure request and response time ... all the way to the response -WSGI iterator has been closed. - -This file was forked by the Kallithea project in July 2014. -Original author and date, and relevant copyright and licensing information is below: -:created_on: May 23, 2013 -:author: marcink -:copyright: (c) 2013 RhodeCode GmbH, and others. -:license: GPLv3, see LICENSE.md for more details. -""" - -import logging -import time - -from kallithea.lib.base import _get_ip_addr, get_path_info - - -log = logging.getLogger(__name__) - - -class Meter: - - def __init__(self, start_response): - self._start_response = start_response - self._start = time.time() - self.status = None - self._size = 0 - - def duration(self): - return time.time() - self._start - - def start_response(self, status, response_headers, exc_info=None): - self.status = status - write = self._start_response(status, response_headers, exc_info) - def metered_write(s): - self.measure(s) - write(s) - return metered_write - - def measure(self, chunk): - self._size += len(chunk) - - def size(self): - return self._size - - -class ResultIter: - - def __init__(self, result, meter, description): - self._result_close = getattr(result, 'close', None) or (lambda: None) - self._next = iter(result).__next__ - self._meter = meter - self._description = description - - def __iter__(self): - return self - - def __next__(self): - chunk = self._next() - self._meter.measure(chunk) - return chunk - - def close(self): - self._result_close() - log.info("%s responded %r after %.3fs with %s bytes", self._description, self._meter.status, self._meter.duration(), self._meter.size()) - - -class RequestWrapper(object): - - def __init__(self, app, config): - self.application = app - self.config = config - - def __call__(self, environ, start_response): - meter = Meter(start_response) - description = "Request from %s for %s" % ( - _get_ip_addr(environ), - get_path_info(environ), - ) - log.info("%s received", description) - try: - result = self.application(environ, meter.start_response) - finally: - log.info("%s responding %r after %.3fs", description, meter.status, meter.duration()) - return ResultIter(result, meter, description)