├── .gitignore ├── README.md ├── examples ├── host.key ├── run.sh ├── run_gevent.py └── run_socket.py ├── maria ├── __init__.py ├── __main__.py ├── colorlog.py ├── config.py ├── date.py ├── ghttp.py ├── git.py ├── gssh.py ├── loader.py ├── utils.py └── worker │ ├── __init__.py │ ├── base.py │ ├── ggevent.py │ └── socket.py ├── requirements.txt ├── setup.py └── tests ├── git_init_bare.sh ├── git_init_repo.sh └── test_maria.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv 3 | local_config.py 4 | build 5 | *egg-info/ 6 | .ropeproject 7 | dist 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Maria System 2 | ============= 3 | 4 | A way to serve git repos through ssh and http protocol like Github. 5 | 6 | ## Requirements 7 | 8 | You can install requirements from requirements.txt with ``pip install -r requirements.txt``. 9 | 10 | ## Features 11 | 12 | 1. support git clone/push/pull with ssh and http protocol. 13 | 2. auth by pub key, you can write your own verify code to use mysql and others easily. 14 | 3. people always like coroutine, powered by gevent. 15 | 4. safely, only allow commands in white list. 16 | 17 | ## Run it 18 | 19 | Firstly, you have to install requirements and maria itself in your python environment like: 20 | ```bash 21 | pip install -r requirements 22 | python setup.py develop 23 | ``` 24 | 25 | Then, you can find example in examples dir. In simple case, ``maria`` will start Maria System. 26 | You also can specify options by yourself like ``maria --debug`` or ``maria -b 0.0.0.0:2200``. 27 | Get options define use this command ``maria -h``. 28 | And ``maria -k host.key -b 127.0.0.1:2200 -w async run_socket:app`` will start maria system with all examples. 29 | 30 | Anyway, I think single process will be ok in production environment with supervisord or something like that. 31 | 32 | ## Test 33 | 34 | ### with unittest 35 | ```bash 36 | $ cd /path/to/maria/tests 37 | $ python test_maria.py 38 | ``` 39 | 40 | ### or with nose 41 | First, nosetests is required. Get it: 42 | ```bash 43 | # pip install nose 44 | ``` 45 | Then, this will run tests: 46 | ```bash 47 | $ cd /path/to/maria/tests 48 | $ nosetests -v 49 | ``` 50 | 51 | ## Maybe a bug 52 | 53 | I disable gevent subprocess monkey patch because I found the execute command function can not exit as I expect, can anyone test it? 54 | 55 | ## Thanks 56 | 57 | [gevent https://github.com/surfly/gevent/](https://github.com/surfly/gevent/) 58 | 59 | [paramiko https://github.com/paramiko/paramiko/](https://github.com/paramiko/paramiko/) 60 | -------------------------------------------------------------------------------- /examples/host.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz 3 | oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/ 4 | d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB 5 | gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0 6 | EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon 7 | soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H 8 | tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU 9 | avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA 10 | 4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g 11 | H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv 12 | qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV 13 | HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc 14 | nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /examples/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | maria -k host.key -b 127.0.0.1:2200 -w async run_socket:app 3 | -------------------------------------------------------------------------------- /examples/run_gevent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | try: 4 | from gevent.monkey import patch_all 5 | patch_all(subprocess=False, aggressive=False) 6 | from gevent.server import StreamServer 7 | except ImportError: 8 | print 'You need install gevent manually! System shutdown.' 9 | 10 | from maria import Maria 11 | from maria.config import config 12 | 13 | app = Maria(config=config) 14 | 15 | if __name__ == '__main__': 16 | server = StreamServer(('0.0.0.0', 2022), app) 17 | server.serve_forever() 18 | -------------------------------------------------------------------------------- /examples/run_socket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from SocketServer import TCPServer 4 | from maria import Maria 5 | from maria.config import config 6 | 7 | config.host_key_path = 'host.key' 8 | app = Maria(config=config) 9 | 10 | if __name__ == '__main__': 11 | server = TCPServer(('0.0.0.0', 2022), app) 12 | server.serve_forever() 13 | -------------------------------------------------------------------------------- /maria/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .gssh import GSSHServer 4 | from .ghttp import GHTTPServer 5 | 6 | Maria = GSSHServer 7 | Sina = GHTTPServer 8 | -------------------------------------------------------------------------------- /maria/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import paramiko 5 | from maria import Maria 6 | from maria.config import Config 7 | from maria.loader import load, load_class 8 | from maria.colorlog import ColorizingStreamHandler 9 | 10 | __all__ = ['main'] 11 | 12 | 13 | class Application(object): 14 | 15 | def __init__(self, usage=None, prog=None): 16 | self.usage = usage 17 | self.prog = prog 18 | self.config = None 19 | self.app = None 20 | self.logger = logging.getLogger(self.__class__.__name__) 21 | self.load_config() 22 | 23 | def init_log(self): 24 | logging.StreamHandler = ColorizingStreamHandler 25 | level = logging.DEBUG 26 | if not self.config.debug: 27 | level = logging.INFO 28 | logging.BASIC_FORMAT = "%(asctime)s [%(name)s] %(message)s" 29 | logging.basicConfig(level=level) 30 | if self.config.log_file: 31 | paramiko.util.log_to_file(self.config.log_file, level=level) 32 | 33 | def load_config(self): 34 | self.config = Config(usage=self.usage, prog=self.prog) 35 | parser = self.config.parse() 36 | args = parser.parse_args() 37 | self.config.load_options(args) 38 | self.init_log() 39 | 40 | # load xxx.xxx:app 41 | if len(args.apps) < 1: 42 | self.logger.info('No application module specified, using default setting') 43 | app = load('maria.gssh.GSSHServer') 44 | self.app = app(config=self.config) 45 | else: 46 | app = load(args.apps[0]) 47 | # command line options has priority over the app's 48 | for key in dir(args): 49 | if key.startswith('_') or key == 'apps': 50 | continue 51 | cmd_conf = getattr(args, key) 52 | app_conf = getattr(app.config, key) 53 | if cmd_conf == app_conf: 54 | continue 55 | setattr(app.config, key, cmd_conf) 56 | if key == 'host_key_path': 57 | self.logger.info('host key path got changed by command line') 58 | app.init_key() 59 | self.app = app 60 | self.config.worker = app.config.worker 61 | 62 | # choose worker 63 | def load_worker(self): 64 | if self.config.worker == 'sync': 65 | return load('maria.worker.socket.SocketServer') 66 | elif self.config.worker == 'async': 67 | return load('maria.worker.ggevent.GeventServer') 68 | else: 69 | raise Exception('Invalid Worker!') 70 | 71 | def run(self): 72 | server = self.load_worker() 73 | addr = self.app.config.get_addr() 74 | return server(addr, self.app).run() 75 | 76 | 77 | def main(): 78 | Application("%(prog)s [OPTIONAL_ARGUMENTS] [APP_MODULE]").run() 79 | 80 | 81 | if __name__ == '__main__': 82 | main() 83 | -------------------------------------------------------------------------------- /maria/colorlog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # most code is stolen from http://goo.gl/qTpR3 3 | 4 | import logging 5 | from logging import StreamHandler 6 | 7 | class ColorizingStreamHandler(StreamHandler): 8 | # color names to indices 9 | color_map = { 10 | 'black': 0, 11 | 'red': 1, 12 | 'green': 2, 13 | 'yellow': 3, 14 | 'blue': 4, 15 | 'magenta': 5, 16 | 'cyan': 6, 17 | 'white': 7, 18 | } 19 | 20 | #levels to (background, foreground, bold/intense) 21 | level_map = { 22 | logging.DEBUG: (None, None, False), 23 | logging.INFO: (None, 'green', False), 24 | logging.WARNING: (None, 'yellow', True), 25 | logging.ERROR: (None, 'red', True), 26 | logging.CRITICAL: ('red', 'white', True), 27 | } 28 | 29 | csi = '\x1b[' 30 | reset = '\x1b[0m' 31 | 32 | @property 33 | def is_tty(self): 34 | isatty = getattr(self.stream, 'isatty', None) 35 | return isatty and isatty() 36 | 37 | def format(self, record): 38 | message = StreamHandler.format(self, record) 39 | if self.is_tty: 40 | # Don't colorize any traceback 41 | parts = message.split('\n', 1) 42 | parts[0] = self.colorize(parts[0], record) 43 | message = '\n'.join(parts) 44 | return message 45 | 46 | def colorize(self, message, record): 47 | if record.levelno in self.level_map: 48 | bg, fg, bold = self.level_map[record.levelno] 49 | params = [] 50 | if bg in self.color_map: 51 | params.append(str(self.color_map[bg] + 40)) 52 | if fg in self.color_map: 53 | params.append(str(self.color_map[fg] + 30)) 54 | if bold: 55 | params.append('1') 56 | if params: 57 | message = ''.join((self.csi, ';'.join(params), 'm', message, 58 | self.reset)) 59 | return message 60 | -------------------------------------------------------------------------------- /maria/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import argparse 6 | 7 | 8 | def populate_argument_parser(parser): 9 | parser.add_argument("-b", "--bind", default="127.0.0.1:2200", dest="bind", 10 | help="bind host:port") 11 | parser.add_argument("-k", "--key", default="host.key", dest="host_key_path", 12 | help="key file path") 13 | parser.add_argument("-w", "--worker", default="sync", dest="worker", 14 | help="worker async(gevent), sync(socketserver)") 15 | parser.add_argument("--git-path", default="/usr/bin", dest="git_path", 16 | help="git command path") 17 | parser.add_argument("--repo-path", default="", dest="project_root", 18 | help="git repository root path") 19 | parser.add_argument("--debug", default=False, dest="debug", 20 | action="store_true", 21 | help="debug") 22 | parser.add_argument("--log-file", default="./maria.log", dest="log_file", 23 | help="log file path") 24 | parser.add_argument("--auth-timeout", default=20, dest="auth_timeout", 25 | type=int, 26 | help="auth timeout") 27 | parser.add_argument("--check-timeout", default=10, dest="check_timeout", 28 | type=int, 29 | help="check timeout") 30 | parser.add_argument("--select-timeout", default=10, dest="select_timeout", 31 | type=int, 32 | help="select timeout") 33 | 34 | 35 | class Config(object): 36 | 37 | def __init__(self, usage=None, prog=None): 38 | self.usage = usage 39 | self.prog = prog or os.path.basename(sys.argv[0]) 40 | self.host_key = None 41 | 42 | self.bind = '0.0.0.0:2200' 43 | self.host_key_path = 'host.key' 44 | self.worker = 'sync' 45 | self.git_path = '/usr/bin' 46 | self.project_root = '' 47 | self.debug = False 48 | self.log_file = './maria.log' 49 | self.auth_timeout = 20 50 | self.check_timeout = 10 51 | self.select_timeout = 10 52 | 53 | # set the config options by args 54 | def load_options(self, args): 55 | for key in dir(args): 56 | if key.startswith('_') or key == 'apps': 57 | continue 58 | new_conf = getattr(args, key) 59 | orig_conf = getattr(self, key, None) 60 | if orig_conf is None or orig_conf == new_conf: 61 | continue 62 | setattr(self, key, new_conf) 63 | 64 | # construct a ArgumentParser 65 | def parse(self): 66 | parser = argparse.ArgumentParser( 67 | prog = self.prog, 68 | usage = self.usage, 69 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 70 | populate_argument_parser(parser) 71 | parser.add_argument("apps", nargs="*", help=argparse.SUPPRESS) 72 | return parser 73 | 74 | # return (host, port) 75 | def get_addr(self): 76 | addr = self.bind.split(':') 77 | if len(addr) is not 2: 78 | raise ValueError('Unrecognized argument value: "%s"' % args.bind) 79 | return (addr[0], int(addr[1])) 80 | 81 | def get(self, key): 82 | if key not in self: 83 | raise AttributeError("No configuration setting for: %s" % key) 84 | return settings.__getattribute__(self, key) 85 | 86 | 87 | config = Config() 88 | -------------------------------------------------------------------------------- /maria/date.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | 5 | # Weekday and month names for HTTP date/time formatting; always English! 6 | WEEKDAY_NAME = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 7 | MONTH_NAME = [None, # Dummy so we can use 1-based month numbers 8 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", 9 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 10 | 11 | 12 | def format_date_time(timestamp): 13 | year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) 14 | return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (WEEKDAY_NAME[wd], 15 | day, 16 | MONTH_NAME[month], 17 | year, hh, mm, ss) 18 | -------------------------------------------------------------------------------- /maria/ghttp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import re 5 | import time 6 | from os import access 7 | from os.path import join, exists, getmtime, getsize 8 | from urllib import unquote 9 | from .git import Git 10 | from .date import format_date_time 11 | 12 | HTTP_STATUS = { 13 | 200: "200 OK", 14 | 400: "400 Bad Request", 15 | 401: "401 Unauthorized", 16 | 403: "403 Forbidden", 17 | 404: "404 Not Found", 18 | 405: "405 Method not allowed", 19 | } 20 | 21 | 22 | class GHTTPServer(object): 23 | 24 | VALID_SERVICE_TYPES = ['upload-pack', 'receive-pack'] 25 | 26 | SERVICES = [ 27 | ["POST", 'service_rpc', re.compile("(.*?)/git-upload-pack$"), 'upload-pack'], 28 | ["POST", 'service_rpc', re.compile("(.*?)/git-receive-pack$"), 'receive-pack'], 29 | 30 | ["GET", 'get_info_refs', re.compile("(.*?)/info/refs$")], 31 | ["GET", 'get_text_file', re.compile("(.*?)/HEAD$")], 32 | ["GET", 'get_text_file', re.compile("(.*?)/objects/info/alternates$")], 33 | ["GET", 'get_text_file', re.compile("(.*?)/objects/info/http-alternates$")], 34 | ["GET", 'get_info_packs', re.compile("(.*?)/objects/info/packs$")], 35 | ["GET", 'get_text_file', re.compile("(.*?)/objects/info/[^/]*$")], 36 | ["GET", 'get_loose_object', re.compile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$")], 37 | ["GET", 'get_pack_file', re.compile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$")], 38 | ["GET", 'get_idx_file', re.compile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$")], 39 | ] 40 | 41 | def __init__(self, config=None): 42 | self.set_config(config) 43 | self.git = Git(self.config.get('git_path')) 44 | 45 | def set_config(self, config): 46 | self.config = config or {} 47 | 48 | def set_config_setting(self, key, value): 49 | self.config[key] = value 50 | 51 | def __call__(self, environ, start_response): 52 | if hasattr(self, '_before_request_handler'): 53 | self._before_request_handler(environ) 54 | self.headers = {} 55 | self.env = environ 56 | body = self.call() 57 | start_response(self.status, self.headers.items()) 58 | if hasattr(self, '_after_request_handler'): 59 | self._after_request_handler(environ) 60 | return body 61 | 62 | def call(self): 63 | match = self.match_routing(self.env["PATH_INFO"].lstrip('/'), 64 | self.env["REQUEST_METHOD"]) 65 | if not match: 66 | return self.render_not_found() 67 | cmd, path, reqfile, rpc = match 68 | self.rpc = rpc 69 | self.reqfile = reqfile 70 | if cmd == "not_allowed": 71 | return self.render_method_not_allowed() 72 | 73 | if hasattr(self, '_has_permission_handler'): 74 | need_perm = self.get_permission(cmd, rpc) 75 | has_perm = self._has_permission_handler(self.env, path, need_perm) 76 | if not has_perm: 77 | return self.render_no_access() 78 | 79 | self.dir = self.get_git_dir(path) 80 | if not self.dir: 81 | return self.render_not_found() 82 | func = getattr(self, cmd) 83 | return func() 84 | 85 | def service_rpc(self): 86 | if not self.has_access(self.rpc, True): 87 | return self.render_no_access() 88 | input = self.read_body 89 | git_cmd = "upload_pack" if self.rpc == "upload-pack" else "receive_pack" 90 | self.status = HTTP_STATUS[200] 91 | self.headers["Content-Type"] = "application/x-git-%s-result" % self.rpc 92 | env = self.env.get('env') 93 | return getattr(self.git, git_cmd)(self.dir, {"msg": input, "env": env}) 94 | 95 | def get_info_refs(self): 96 | service_name = self.get_service_type() 97 | if self.has_access(service_name): 98 | git_cmd = "upload_pack" if service_name == "upload-pack" else "receive_pack" 99 | refs = getattr(self.git, git_cmd)(self.dir, {"advertise_refs": True}) 100 | self.status = HTTP_STATUS[200] 101 | self.headers["Content-Type"] = "application/x-git-%s-advertisement" % service_name 102 | self.hdr_nocache() 103 | 104 | def read_file(): 105 | yield self.pkt_write("# service=git-%s\n" % service_name) 106 | yield self.pkt_flush 107 | yield refs 108 | return read_file() 109 | else: 110 | return self.dumb_info_refs() 111 | 112 | def get_text_file(self): 113 | return self.send_file(self.reqfile, "text/plain") 114 | 115 | def dumb_info_refs(self): 116 | self.update_server_info() 117 | return self.send_file(self.reqfile, "text/plain; charset=utf-8") 118 | 119 | def get_info_packs(self): 120 | # objects/info/packs 121 | return self.send_file(self.reqfile, "text/plain; charset=utf-8") 122 | 123 | def get_loose_object(self): 124 | return self.send_file(self.reqfile, "application/x-git-loose-object", cached=True) 125 | 126 | def get_pack_file(self): 127 | return self.send_file(self.reqfile, "application/x-git-packed-objects", cached=True) 128 | 129 | def get_idx_file(self): 130 | return self.send_file(self.reqfile, "application/x-git-packed-objects-toc", cached=True) 131 | 132 | def get_service_type(self): 133 | def get_param(): 134 | for query in self.env["QUERY_STRING"].split('&'): 135 | param = tuple(query.split('=')) 136 | if param and param[0] == "service": 137 | return param[1] 138 | service_type = get_param() 139 | if not service_type: 140 | return False 141 | if service_type[0:4] != 'git-': 142 | return False 143 | return service_type.replace('git-', '') 144 | 145 | @classmethod 146 | def match_routing(cls, path_info, request_method): 147 | for service in cls.SERVICES: 148 | rpc = None 149 | if len(service) == 4: 150 | method, handler, re_match, rpc = service 151 | elif len(service) == 3: 152 | method, handler, re_match = service 153 | m = re_match.match(path_info) 154 | if m: 155 | if method != request_method: 156 | return ["not_allowed", None, None, None] 157 | cmd = handler 158 | path = m.group(1) 159 | file = path_info.replace(path + '/', '') 160 | return [cmd, path, file, rpc] 161 | return None 162 | 163 | def send_file(self, reqfile, content_type, cached=False): 164 | reqfile = join(self.dir, reqfile) 165 | if not self.is_subpath(reqfile, self.dir): 166 | return self.render_no_access() 167 | if not exists(reqfile) or not access(reqfile, os.R_OK): 168 | return self.render_not_found() 169 | 170 | self.status = HTTP_STATUS[200] 171 | self.headers["Content-Type"] = content_type 172 | self.headers["Last-Modified"] = format_date_time(getmtime(reqfile)) 173 | 174 | if cached: 175 | self.hdr_cache_forenver() 176 | else: 177 | self.hdr_nocache() 178 | 179 | size = getsize(reqfile) 180 | if size: 181 | self.headers["Content-Length"] = size 182 | 183 | def read_file(): 184 | with open(reqfile, "rb") as f: 185 | while True: 186 | part = f.read(8192) 187 | if not part: 188 | break 189 | yield part 190 | return read_file() 191 | else: 192 | with open(reqfile, "rb") as f: 193 | part = f.read() 194 | self.headers["Content-Length"] = str(len(part)) 195 | return [part] 196 | 197 | def update_server_info(self): 198 | self.git.update_server_info(self.dir) 199 | 200 | @property 201 | def read_chunked_body(self): 202 | # wsgiref with no chunked support 203 | environ = self.env 204 | input = environ.get('wsgi.input') 205 | length = environ.get('CONTENT_LENGTH', '0') 206 | length = 0 if length == '' else int(length) 207 | body = '' 208 | if length == 0: 209 | if input is None: 210 | return 211 | if environ.get('HTTP_TRANSFER_ENCODING', '0') == 'chunked': 212 | size = int(input.readline(), 16) 213 | while size > 0: 214 | body += input.read(size) 215 | input.read(2) 216 | size = int(input.readline(), 16) 217 | else: 218 | body = input.read(length) 219 | return body 220 | 221 | @property 222 | def read_body(self): 223 | if self.config.get('chunked'): 224 | return self.read_chunked_body 225 | input = self.env.get('wsgi.input') 226 | return input.read() 227 | 228 | # ------------------------------ 229 | # packet-line handling functions 230 | # ------------------------------ 231 | 232 | @property 233 | def pkt_flush(self): 234 | return '0000' 235 | 236 | def pkt_write(self, str): 237 | # TODO: use zfill 238 | PKT_FORMAT = "{0:{fill}{align}{width}{base}}{1}" 239 | return PKT_FORMAT.format(len(str) + 4, 240 | str, 241 | base='x', 242 | width=4, 243 | fill='0', 244 | align='>') 245 | 246 | # ------------------------ 247 | # header writing functions 248 | # ------------------------ 249 | 250 | def hdr_nocache(self): 251 | self.headers["Expires"] = "Fri, 01 Jan 1980 00:00:00 GMT" 252 | self.headers["Pragma"] = "no-cache" 253 | self.headers["Cache-Control"] = "no-cache, max-age=0, must-revalidate" 254 | 255 | def hdr_cache_forenver(self): 256 | now = int(time.time()) 257 | self.headers["Date"] = str(now) 258 | self.headers["Expires"] = str(now + 31536000) 259 | self.headers["Cache-Control"] = "public, max-age=31536000" 260 | 261 | # -------------------------------------- 262 | # HTTP error response handling functions 263 | # -------------------------------------- 264 | 265 | def render_method_not_allowed(self): 266 | env = [] 267 | if env["SERVER_PROTOCOL"] == "HTTP/1.1": 268 | self.status = HTTP_STATUS[405] 269 | self.headers["Content-Type"] = "text/plain" 270 | return ["Method Not Allowed"] 271 | else: 272 | self.status = HTTP_STATUS[400] 273 | self.headers["Content-Type"] = "text/plain" 274 | return ["Bad Request"] 275 | 276 | def render_not_found(self): 277 | self.status = HTTP_STATUS[404] 278 | self.headers["Content-Type"] = "text/plain" 279 | return ["Not Found"] 280 | 281 | def render_no_access(self): 282 | self.status = HTTP_STATUS[403] 283 | self.headers["Content-Type"] = "text/plain" 284 | return ["Forbidden"] 285 | 286 | def has_access(self, rpc, check_content_type=False): 287 | if check_content_type: 288 | if self.env["CONTENT_TYPE"] != "application/x-git-%s-request" % rpc: 289 | return False 290 | if rpc not in self.VALID_SERVICE_TYPES: 291 | return False 292 | if rpc == 'receive-pack': 293 | if "receive_pack" in self.config: 294 | return self.config.get("receive_pack") 295 | if rpc == 'upload-pack': 296 | if "upload_pack" in self.config: 297 | return self.config.get("upload_pack") 298 | return self.get_config_setting(rpc) 299 | 300 | def get_config_setting(self, service_name): 301 | service_name = service_name.replace('-', '') 302 | setting = self.git.get_config_setting(self.dir, 303 | "http.%s" % service_name) 304 | if service_name == 'uploadpack': 305 | return setting != 'false' 306 | else: 307 | return setting == 'true' 308 | 309 | def get_git_dir(self, path): 310 | if hasattr(self, '_get_repo_path_handler'): 311 | return self._get_repo_path_handler(self.env, path) 312 | root = self.get_project_root() 313 | path = join(root, path) 314 | if not self.is_subpath(path, root): 315 | return False 316 | if exists(path): # TODO: check is a valid git directory 317 | return path 318 | return False 319 | 320 | def get_project_root(self): 321 | root = self.config.get("project_root") or os.getcwd() 322 | return root 323 | 324 | def is_subpath(self, path, checkpath): 325 | path = unquote(path) 326 | checkpath = unquote(checkpath) 327 | # Remove trailing slashes from filepath 328 | checkpath = checkpath.replace("\/+$", '') 329 | if re.match("^%s(\/|$)" % checkpath, path): 330 | return True 331 | 332 | # decorator hook 333 | 334 | # args: environ 335 | def before_request(self, f): 336 | self._before_request_handler = f 337 | return f 338 | 339 | # args: environ 340 | def after_request(self, f): 341 | self._after_request_handler = f 342 | return f 343 | 344 | # args: environ, path 345 | def get_repo_path(self, f): 346 | self._get_repo_path_handler = f 347 | return f 348 | 349 | # args: environ, path, perm 350 | def has_permission(self, f): 351 | self._has_permission_handler = f 352 | return f 353 | 354 | def get_permission(self, cmd, rpc): 355 | if cmd != 'service_rpc': 356 | return 'read' 357 | if rpc == 'upload-pack': 358 | return 'read' 359 | return 'write' 360 | -------------------------------------------------------------------------------- /maria/git.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import shlex 5 | import select 6 | import subprocess 7 | from contextlib import contextmanager 8 | 9 | 10 | @contextmanager 11 | def chdir(dir): 12 | cwd = os.getcwd() 13 | os.chdir(dir) 14 | yield 15 | os.chdir(cwd) 16 | 17 | 18 | def callback(p): 19 | ofd = p.stdout.fileno() 20 | efd = p.stderr.fileno() 21 | while True: 22 | r_ready, w_ready, x_ready = select.select([ofd, efd], [], [], 30) 23 | 24 | if ofd in r_ready: 25 | data = os.read(ofd, 8192) 26 | if not data: 27 | break 28 | yield data 29 | 30 | if efd in r_ready: 31 | data = os.read(efd, 8192) 32 | yield data 33 | break 34 | 35 | output, err = p.communicate() 36 | if output: 37 | yield output 38 | if err: 39 | yield err 40 | 41 | 42 | class Git(object): 43 | 44 | def __init__(self, dir=''): 45 | self.git_path = os.path.join(dir, 'git') 46 | 47 | @property 48 | def command_options(self): 49 | return {"advertise_refs": "--advertise-refs"} 50 | 51 | def command(self, cmd, opts={}, env=None): 52 | cmd = "%s %s %s" % (self.git_path, cmd, " ".join(opts.get("args"))) 53 | cmd = shlex.split(cmd) 54 | p = subprocess.Popen(cmd, 55 | stdin=subprocess.PIPE, 56 | stdout=subprocess.PIPE, 57 | stderr=subprocess.PIPE, 58 | close_fds=True, 59 | env=env) 60 | data = opts.get("msg") 61 | if data: 62 | p.stdin.write(data) 63 | return callback(p) 64 | 65 | def upload_pack(self, repository_path, opts=None, env=None): 66 | cmd = "upload-pack" 67 | args = [] 68 | if not opts: 69 | opts = {} 70 | for k, v in opts.iteritems(): 71 | if k in self.command_options: 72 | args.append(self.command_options.get(k)) 73 | args.append("--stateless-rpc") 74 | args.append(repository_path) 75 | opts["args"] = args 76 | return self.command(cmd, opts, env=env) 77 | 78 | def receive_pack(self, repository_path, opts=None, env=None): 79 | cmd = "receive-pack" 80 | args = [] 81 | if not opts: 82 | opts = {} 83 | for k, v in opts.iteritems(): 84 | if k in self.command_options: 85 | args.append(self.command_options.get(k)) 86 | args.append("--stateless-rpc") 87 | args.append(repository_path) 88 | opts["args"] = args 89 | return self.command(cmd, opts, env=env) 90 | 91 | def update_server_info(self, repository_path, opts=None, env=None): 92 | cmd = "update-server-info" 93 | args = [] 94 | if not opts: 95 | opts = {} 96 | for k, v in opts.iteritems(): 97 | if k in self.command_options: 98 | args.append(self.command_options.get(k)) 99 | opts["args"] = args 100 | with chdir(repository_path): 101 | self.command(cmd, opts, env=env) 102 | 103 | def get_config_setting(self, repository_path, key): 104 | # TODO: user pygit2 or ellen 105 | path = self.get_config_location(repository_path) 106 | result = self.command("config", {"args": ["-f %s" % path, key]}) 107 | if result: 108 | return result.strip('\n').strip('\r') 109 | return result 110 | 111 | def get_config_location(self, repository_path): 112 | non_bare = os.path.join(repository_path, ".git") 113 | if os.path.exists(non_bare): 114 | non_bare_config = os.path.join(non_bare, "config") 115 | return non_bare_config if os.path.exists(non_bare_config) else None 116 | else: 117 | bare_config = os.path.join(repository_path, "config") 118 | return bare_config if os.path.exists(bare_config) else None 119 | -------------------------------------------------------------------------------- /maria/gssh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import select 5 | import logging 6 | import threading 7 | import subprocess 8 | import paramiko 9 | from . import utils 10 | from .colorlog import ColorizingStreamHandler 11 | 12 | 13 | class GSSHServer(object): 14 | 15 | def __init__(self, config=None): 16 | self.config = config 17 | self.logger = logging.getLogger(__name__) 18 | self.init_log() 19 | self.init_key() 20 | 21 | # SocketServer: 22 | # self.RequestHandlerClass(request, client_address, self) 23 | def __call__(self, socket, address, _=None): 24 | client = None 25 | try: 26 | client = paramiko.Transport(socket) 27 | try: 28 | client.load_server_moduli() 29 | except Exception: 30 | self.logger.exception('Failed to load moduli -- gex will be unsupported.') 31 | raise 32 | 33 | client.add_server_key(self.config.host_key) 34 | server = GSSHServerInterface(app=self) 35 | try: 36 | client.start_server(server=server) 37 | except paramiko.SSHException: 38 | self.logger.exception('SSH negotiation failed.') 39 | return 40 | 41 | channel = self.check_ssh_auth(client, address) 42 | if not channel: 43 | return 44 | 45 | if not self.check_ssh_command(server, address): 46 | return 47 | 48 | server.main_loop(channel) 49 | except Exception: 50 | self.logger.exception('Caught Exception') 51 | finally: 52 | if client: 53 | client.close() 54 | return 55 | 56 | def init_log(self): 57 | logging.StreamHandler = ColorizingStreamHandler 58 | level = logging.DEBUG 59 | if not self.config.debug: 60 | level = logging.INFO 61 | logging.BASIC_FORMAT = "%(asctime)s [%(name)s] %(message)s" 62 | logging.basicConfig(level=level) 63 | if self.config.log_file: 64 | paramiko.util.log_to_file(self.config.log_file, level=level) 65 | 66 | def init_key(self): 67 | path = os.path.realpath(self.config.host_key_path) 68 | self.config.host_key = paramiko.RSAKey(filename=path) 69 | self.logger.info('Host Key %s' % utils.hex_key(self.config.host_key)) 70 | 71 | def check_ssh_auth(self, client, address): 72 | channel = client.accept(self.config.auth_timeout) 73 | if channel is None: 74 | self.logger.info('Auth timeout %s:%d' % address) 75 | return None 76 | return channel 77 | 78 | def check_ssh_command(self, server, address): 79 | if not server.event.wait(self.config.check_timeout): 80 | self.logger.info('Check timeout %s:%d' % address) 81 | return False 82 | return True 83 | 84 | def parse_ssh_command(self, command): 85 | if not command: 86 | return [], '' 87 | # command eg: git-upload-pack 'code.git' 88 | args = command.split(' ') 89 | cmd = args[:-1] 90 | repo = args[-1].strip("'") 91 | return cmd, repo 92 | 93 | def check_ssh_user(self, name): 94 | if name == 'git': 95 | return True 96 | return False 97 | 98 | def check_ssh_key(self, key): 99 | # key_b = key.get_base64() 100 | # check key_b 101 | if not key: 102 | return False 103 | return True 104 | 105 | def check_git_repo(self, repo): 106 | # 'Error: Repository not found.\n' 107 | if not repo: 108 | return False 109 | return True 110 | 111 | def check_git_command(self, command): 112 | if not command[0]: 113 | return False 114 | if not command[0] in ('git-receive-pack', 115 | 'git-upload-pack'): 116 | return False 117 | if self.config.git_path: 118 | command[0] = os.path.join(self.config.git_path, command[0]) 119 | return True 120 | 121 | def get_permission(self, cmd): 122 | if cmd == 'git-receive-pack': 123 | return 'write' 124 | if cmd == 'git-upload-pack': 125 | return 'read' 126 | 127 | # args: path 128 | def get_repo_path(self, f): 129 | self._get_repo_path_handler = f 130 | return f 131 | 132 | # args: ssh_username, key 133 | def get_user(self, f): 134 | self._get_user_handler = f 135 | return f 136 | 137 | # args: user, path, perm 138 | def has_permission(self, f): 139 | self._has_permission_handler = f 140 | return f 141 | 142 | # args: user, path 143 | def get_environ(self, f): 144 | self._get_environ_handler = f 145 | return f 146 | 147 | 148 | class GSSHServerInterface(paramiko.ServerInterface): 149 | 150 | def __init__(self, app=None): 151 | self.app = app 152 | self.event = threading.Event() 153 | self.ssh_key = None 154 | self.ssh_username = '' 155 | self.repo_name = '' 156 | self.username = '' 157 | self.command = None 158 | self.message = '' # TODO: useless 159 | self.environ = None 160 | 161 | def get_allowed_auths(self, username): 162 | return 'publickey' 163 | 164 | def check_channel_request(self, kind, chanid): 165 | if kind == 'session': 166 | return paramiko.OPEN_SUCCEEDED 167 | return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED 168 | 169 | def check_auth_publickey(self, username, key): 170 | hex_fingerprint = utils.hex_key(key) 171 | self.app.logger.info('Auth attempt with key: %s' % hex_fingerprint) 172 | self.ssh_username = username 173 | if not self.app.check_ssh_user(username): 174 | return paramiko.AUTH_FAILED 175 | self.ssh_key = key 176 | if not self.app.check_ssh_key(key): 177 | return paramiko.AUTH_FAILED 178 | if hasattr(self.app, '_get_user_handler'): 179 | self.username = self.app._get_user_handler(self.ssh_username, self.ssh_key) 180 | if not self.username: 181 | return paramiko.AUTH_FAILED 182 | return paramiko.AUTH_SUCCESSFUL 183 | 184 | # not paramiko method 185 | def check_error_message(self, channel): 186 | message = self.message 187 | if message: 188 | channel.sendall_stderr(message) 189 | self.event.set() 190 | return True 191 | self.event.set() 192 | 193 | def check_channel_exec_request(self, channel, command): 194 | self.app.logger.info('Command %s received' % command) 195 | command, repo = self.app.parse_ssh_command(command) 196 | self.repo_name = repo 197 | 198 | try: 199 | if not self.app.check_git_repo(repo): 200 | return False 201 | if not self.app.check_git_command(command): 202 | return False 203 | 204 | if hasattr(self.app, '_has_permission_handler'): 205 | perm = self.app.get_permission(command[0]) 206 | if not self.app._has_permission_handler(self.username, repo, perm): 207 | return False 208 | 209 | except Exception as e: 210 | self.message = str(e) 211 | if self.check_error_message(channel): 212 | return True 213 | return False 214 | 215 | if hasattr(self.app, '_get_environ_handler'): 216 | self.environ = self.app._get_environ_handler(self.username, repo) 217 | 218 | if hasattr(self.app, '_get_repo_path_handler'): 219 | repo = self.app._get_repo_path_handler(repo) 220 | else: 221 | repo = os.path.join(self.app.config.project_root, repo) 222 | 223 | command.append(repo) 224 | self.command = command 225 | self.event.set() 226 | return True 227 | 228 | def main_loop(self, channel): 229 | if not self.command: 230 | return 231 | 232 | p = subprocess.Popen(self.command, 233 | stdin=subprocess.PIPE, 234 | stdout=subprocess.PIPE, 235 | stderr=subprocess.PIPE, 236 | close_fds=True, 237 | env=self.environ) 238 | 239 | ofd = p.stdout.fileno() 240 | efd = p.stderr.fileno() 241 | 242 | while True: 243 | r_ready, w_ready, x_ready = select.select([channel, ofd, efd], 244 | [], 245 | [], 246 | self.app.config.select_timeout) 247 | 248 | if channel in r_ready: 249 | data = channel.recv(16384) 250 | if not data and (channel.closed or channel.eof_received): 251 | break 252 | p.stdin.write(data) 253 | 254 | if ofd in r_ready: 255 | data = os.read(ofd, 16384) 256 | if not data: 257 | break 258 | channel.sendall(data) 259 | 260 | if efd in r_ready: 261 | data = os.read(efd, 16384) 262 | channel.sendall(data) 263 | break 264 | 265 | output, err = p.communicate() 266 | if output: 267 | channel.sendall(output) 268 | if err: 269 | channel.sendall_stderr(err) 270 | channel.send_exit_status(p.returncode) 271 | if not channel.recv(4): 272 | channel.shutdown(2) 273 | channel.close() 274 | self.app.logger.info('Command execute finished') 275 | -------------------------------------------------------------------------------- /maria/loader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import imp 6 | import inspect 7 | import traceback 8 | import pkg_resources 9 | 10 | try: 11 | from importlib import import_module 12 | except ImportError: 13 | def _resolve_name(name, package, level): 14 | """Return the absolute name of the module to be imported.""" 15 | if not hasattr(package, 'rindex'): 16 | raise ValueError("'package' not set to a string") 17 | dot = len(package) 18 | for x in range(level, 1, -1): 19 | try: 20 | dot = package.rindex('.', 0, dot) 21 | except ValueError: 22 | raise ValueError("attempted relative import beyond top-level " 23 | "package") 24 | return "%s.%s" % (package[:dot], name) 25 | 26 | def import_module(name, package=None): 27 | """Import a module. 28 | 29 | The 'package' argument is required when performing a relative import. It 30 | specifies the package to use as the anchor point from which to resolve the 31 | relative import to an absolute import. 32 | 33 | """ 34 | if name.startswith('.'): 35 | if not package: 36 | raise TypeError("relative imports require the 'package' argument") 37 | level = 0 38 | for character in name: 39 | if character != '.': 40 | break 41 | level += 1 42 | name = _resolve_name(name[level:], package, level) 43 | __import__(name) 44 | return sys.modules[name] 45 | 46 | def load(uri): 47 | parts = uri.split(":", 1) #str.split([sep[, maxsplit]]) 48 | if len(parts) == 1: 49 | return load_class(parts[0]) 50 | elif len(parts) == 2: 51 | module, obj = parts[0], parts[1] 52 | return load_app(module, obj) 53 | else: 54 | raise Exception("load error: uri is invalid") 55 | 56 | def load_class(uri, default="maria.worker.socket.SocketServer"): 57 | if inspect.isclass(uri): 58 | return uri 59 | else: 60 | components = uri.split('.') 61 | klass = components.pop(-1) 62 | try: 63 | mod = import_module('.'.join(components)) 64 | except: 65 | exc = traceback.format_exc() 66 | raise RuntimeError("class uri %r invalid " 67 | "or not found: \n\n[%s]" % (uri, 68 | exc)) 69 | 70 | return getattr(mod, klass) 71 | 72 | def load_app(module, obj): 73 | sys.path.insert(0, os.getcwd()) 74 | try: 75 | __import__(module) 76 | except: 77 | raise ImportError("Failed to import application") 78 | 79 | mod = sys.modules[module] 80 | 81 | try: 82 | app = eval(obj, mod.__dict__) 83 | except NameError: 84 | raise Exception("Failed to find application: %r" % module) 85 | 86 | if app is None: 87 | raise Exception("Failed to find application object: %r" % obj) 88 | 89 | if not callable(app): 90 | raise Exception("Application object must be callable.") 91 | return app 92 | -------------------------------------------------------------------------------- /maria/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from paramiko.util import hexify 4 | 5 | 6 | def hex_key(key): 7 | return hexify(key.get_fingerprint()) 8 | -------------------------------------------------------------------------------- /maria/worker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /maria/worker/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | 6 | class WorkerClass(object): 7 | 8 | def __init__(self, addr, app): 9 | self.addr = addr 10 | self.app = app 11 | self.logger = logging.getLogger(self.__class__.__name__) 12 | 13 | def run(self): 14 | raise NotImplementedError() 15 | -------------------------------------------------------------------------------- /maria/worker/ggevent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | 3 | try: 4 | from gevent.monkey import patch_all 5 | patch_all(subprocess=False, aggressive=False) 6 | from gevent.server import StreamServer 7 | except ImportError: 8 | raise RuntimeError("You need install gevent") 9 | 10 | from maria.worker.base import WorkerClass 11 | 12 | 13 | class GeventServer(WorkerClass): 14 | 15 | def run(self): 16 | server = StreamServer(self.addr, self.app) 17 | try: 18 | self.logger.info('Maria System Start at %s:%s' % self.addr) 19 | server.serve_forever() 20 | except KeyboardInterrupt: 21 | self.logger.info('Maria System Stopped') 22 | -------------------------------------------------------------------------------- /maria/worker/socket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | 3 | from SocketServer import TCPServer 4 | from maria.worker.base import WorkerClass 5 | 6 | 7 | class SocketServer(WorkerClass): 8 | 9 | def run(self): 10 | server = TCPServer(self.addr, self.app) 11 | try: 12 | self.logger.info('Maria System Start at %s:%s' % self.addr) 13 | server.serve_forever() 14 | except KeyboardInterrupt: 15 | self.logger.info('Maria System Stopped') 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ecdsa==0.11 2 | paramiko==1.12.2 3 | pycrypto==2.6.1 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | # package meta info 7 | NAME = "maria" 8 | VERSION = "0.1.0" 9 | DESCRIPTION = "A way to serve git repo through ssh protocol like github" 10 | AUTHOR = "CMGS" 11 | AUTHOR_EMAIL = "ilskdw@gmail.com" 12 | LICENSE = "BSD" 13 | URL = "https://github.com/CMGS/maria" 14 | KEYWORDS = "ssh proxy" 15 | CLASSIFIERS = [] 16 | 17 | # package contents 18 | MODULES = [] 19 | PACKAGES = find_packages(exclude=['tests.*', 'tests', 'examples.*', 'examples']) 20 | ENTRY_POINTS = """ 21 | [console_scripts] 22 | maria = maria.__main__:main 23 | """ 24 | 25 | # dependencies 26 | INSTALL_REQUIRES = ["paramiko>=1.12.0"] 27 | 28 | here = os.path.abspath(os.path.dirname(__file__)) 29 | 30 | 31 | def read_long_description(filename): 32 | path = os.path.join(here, filename) 33 | if os.path.exists(path): 34 | return open(path).read() 35 | return "" 36 | 37 | setup( 38 | name=NAME, 39 | version=VERSION, 40 | description=DESCRIPTION, 41 | long_description=read_long_description('README.md'), 42 | author=AUTHOR, 43 | author_email=AUTHOR_EMAIL, 44 | license=LICENSE, 45 | url=URL, 46 | keywords=KEYWORDS, 47 | classifiers=CLASSIFIERS, 48 | py_modules=MODULES, 49 | packages=PACKAGES, 50 | install_package_data=True, 51 | zip_safe=False, 52 | entry_points=ENTRY_POINTS, 53 | install_requires=INSTALL_REQUIRES, 54 | extras_require={ 55 | "gevent": ["Cython>=0.20.1", "gevent>=1.1"], 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /tests/git_init_bare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git_bare=$1 4 | 5 | if [ -d $git_bare ] 6 | then 7 | exit 1 8 | fi 9 | 10 | mkdir $git_bare 11 | cd $git_bare 12 | git init --bare 13 | -------------------------------------------------------------------------------- /tests/git_init_repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git_repo=$1 4 | git_bare=$2 5 | 6 | if [ ! -d $git_bare ] 7 | then 8 | exit 1 9 | fi 10 | 11 | if [ -d $git_repo ] 12 | then 13 | exit 2 14 | fi 15 | 16 | mkdir $git_repo 17 | cd $git_repo 18 | git init 19 | touch test.c 20 | git add test.c 21 | git commit -m "Add test.c" 22 | git remote add origin git@127.0.0.1:$git_bare 23 | git push origin master 24 | -------------------------------------------------------------------------------- /tests/test_maria.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import subprocess 5 | import maria 6 | import unittest 7 | 8 | 9 | class TestMaria(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.git_bare = '~/temp_bare.git' 13 | self.git_repo = '~/temp_repo.git' 14 | 15 | cmd = './git_init_bare.sh ' + self.git_bare 16 | status = subprocess.call(cmd, shell=True) 17 | assert status != 1, 'temp git_bare path existed!' 18 | assert status == 0, 'temp git_bare init error!' 19 | 20 | args = ['maria', 21 | '-k', '../examples/host.key', 22 | '-b', '127.0.0.1:2200'] 23 | self.p = subprocess.Popen(args) 24 | time.sleep(1) 25 | 26 | def tearDown(self): 27 | self.p.terminate() 28 | cmd = 'rm %s %s -rf' % (self.git_bare, self.git_repo) 29 | status = subprocess.call(cmd, shell=True) 30 | assert status == 0, 'delete temp git repos error!' 31 | 32 | def test_clone(self): 33 | cmd = 'git clone git@127.0.0.1:%s %s' \ 34 | % (self.git_bare, self.git_repo) 35 | pclone = subprocess.Popen(cmd, shell=True) 36 | assert pclone.wait() == 0, 'git clone error!' 37 | 38 | def test_push(self): 39 | cmd = './git_init_repo.sh ' \ 40 | + self.git_repo + ' ' + self.git_bare 41 | ppush = subprocess.Popen(cmd, shell=True) 42 | status = ppush.wait() 43 | assert status != 1, 'temp git_bare path does not exist!' 44 | assert status != 2, 'temp git_repo path existed!' 45 | assert status == 0, 'git push error!' 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | --------------------------------------------------------------------------------