From e5b0ac78a1b1d659b6f97638422b06f4207fa7d5 Mon Sep 17 00:00:00 2001 From: Ivan Mahonin Date: Sep 24 2019 15:57:34 +0000 Subject: git repositories --- diff --git a/.gitignore b/.gitignore index e65f3d5..144db77 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /log/ /earthworm.kdev4 __pycache__ +/repo/ diff --git a/action/actions.py b/action/actions.py index 3094761..6753c36 100644 --- a/action/actions.py +++ b/action/actions.py @@ -1,9 +1,11 @@ from util import mergedict -import action.user as user +import action.user +import action.repo actions = dict() -mergedict(actions, user.actions, 'user.') +mergedict(actions, action.user.actions, 'user.') +mergedict(actions, action.repo.actions, 'repo.') diff --git a/action/repo.py b/action/repo.py new file mode 100644 index 0000000..5b8ea4e --- /dev/null +++ b/action/repo.py @@ -0,0 +1,81 @@ + + +import exception +from action.action import Action +from action.user import UserBase + + +class RepoBase(UserBase): + def parse_repository_id(self, request): + repository_id = 0 + try: + repository_id = int(request.postvars.get('repository_id', 0)) + except Exception: + raise exception.ActionError( request.t('Repository Id incorrect') ) + if not repository_id: + raise exception.ActionError( request.t('Repository Id incorrect') ) + return repository_id + + +class RepoCreate(RepoBase): + def process(self, request): + user_id = self.parse_user_id(request) + name = str(request.postvars.get('name', '')) + repotype = str(request.postvars.get('type', '')) + title = str(request.postvars.get('title', '')) + description = str(request.postvars.get('description', '')) + + repo = None + try: + repo = request.model.repositories.create(user_id, name, repotype, title, description) + except Exception as e: + self.propagate_exception(e) + + request.connection.commit() + return request.answer.complete_redirect(['user', str(repo.get_user().login), 'repo', str(repo.name)]) + + +class RepoUpdate(RepoBase): + def process(self, request): + repository_id = self.parse_repository_id(request) + title = str(request.postvars.get('title', '')) + description = str(request.postvars.get('description', '')) + + repo = request.model.repositories.get_by_id(repository_id) + if not repo: + raise exception.ActionError( request.t('Repository not found') ) + + try: + repo.update(title, description) + except Exception as e: + self.propagate_exception(e) + + request.connection.commit() + return request.answer.complete_redirect(['user', str(repo.get_user().login), 'repo', str(repo.name)]) + + +class RepoDelete(RepoBase): + def process(self, request): + repository_id = self.parse_repository_id(request) + + repo = request.model.repositories.get_by_id(repository_id) + if not repo: + raise exception.ActionError( request.t('Repository not found') ) + login = repo.get_user().login + + try: + repo.delete() + except Exception as e: + self.propagate_exception(e) + + request.connection.commit() + return request.answer.complete_redirect(['user', login, 'repos']) + + +actions = { + 'create' : RepoCreate(), + 'update' : RepoUpdate(), + 'delete' : RepoDelete(), +} + + diff --git a/answer.py b/answer.py index 70f6b5c..82789ac 100644 --- a/answer.py +++ b/answer.py @@ -10,7 +10,6 @@ class Result: def __init__(self, code, headers, data): assert(type(code) is str) assert(type(headers) is list) - assert(type(data) is list) self.code = code self.headers = headers self.data = data @@ -45,9 +44,9 @@ class Answer(Translator): self.objects = dict() def htmlescape(self, text): - return html.escape(str(text)) + return html.escape(str('' if text is None else text)) def urlescape(self, text): - return urllib.parse.quote(str(text)) + return urllib.parse.quote(str('' if text is None else text)) def e(self, text): return self.htmlescape(text) @@ -68,7 +67,7 @@ class Answer(Translator): def complete_redirect(self, path = None): self.status = '303 See Other' - self.headers.append(('Location', self.request.get_urlpath_escaped(path))) + self.headers.append( ('Location', self.request.get_urlpath_escaped(path)) ) return Result(self.status, self.headers, list()) def complete_error(self, status = None): @@ -102,7 +101,7 @@ class Answer(Translator): def complete_html(self, html = None): if not html is None: self.html = html - self.headers += [('Content-Type','text/html')]; + self.headers.append( ('Content-Type','text/html') ); return self.complete_text( self.html ) def complete_content(self, content = None, template = None): diff --git a/config.py b/config.py index cb8363b..926930d 100644 --- a/config.py +++ b/config.py @@ -1,5 +1,7 @@ config = { + 'domain' : 'earthworm.local', + 'db': { 'prefix' : 'ew_', 'connection': { @@ -16,5 +18,12 @@ config = { 'selfcreate' : True, 'selfdelete' : True, }, + + 'repositories': { + 'git': { + 'internalurl' : 'http://localhost:8011/repo/git', + 'path' : '/home/bw/work/dev/earthworm/repo/git' + }, + }, } diff --git a/db/cache.py b/db/cache.py index 479754c..96072cd 100644 --- a/db/cache.py +++ b/db/cache.py @@ -115,24 +115,31 @@ class Cache: assert(type(table) is str) sql = self.build_select(connection, table, fields) - with self.lock: - tbl = self.tables.get(table) - if not tbl: - self.tables[table] = tbl = CacheTable(self.maxcount) - rows = tbl.get(sql) + rows = None + + # cache does not support transactions and disabled + # + #with self.lock: + # tbl = self.tables.get(table) + # if not tbl: + # self.tables[table] = tbl = CacheTable(self.maxcount) + # rows = tbl.get(sql) if rows is None: rows = connection.query_dict(sql) assert(type(rows) is list) - with self.lock: - tbl = self.tables.get(table) - if not tbl: - self.tables[table] = tbl = CacheTable(self.maxcount) - tbl.set(sql, rows) + #with self.lock: + # tbl = self.tables.get(table) + # if not tbl: + # self.tables[table] = tbl = CacheTable(self.maxcount) + # tbl.set(sql, rows) + return rows def reset(self, connection, table, fields = None): assert(type(table) is str) + assert(not connection.readonly) + return sql = None if not fields is None: sql = self.build_select(connection, table, fields) diff --git a/db/connection.py b/db/connection.py index 5ffd29f..0bb86f1 100644 --- a/db/connection.py +++ b/db/connection.py @@ -62,6 +62,8 @@ class Connection: self.readonly = readonly self.finished = True self.begin() + self.on_commit = list() + self.on_rollback = list() def parse(self, text, *args, **kvargs): i = iter(text) @@ -140,13 +142,38 @@ class Connection: self.execute("START TRANSACTION READ WRITE") self.now = datetime.datetime.now(datetime.timezone.utc) + def process_events(self, events, skip_errors = False): + while events: + events_copy = list(events) + events.clear() + for event in events_copy: + try: + event[0](*event[1], *event[2]) + except Exception as e: + print("exception in event") + print(traceback.format_exc()) + print(e) + if not skip_errors: + raise e + + def call_on_commit(self, function, *args, **kvargs): + self.on_commit.append((function, args, kvargs)) + def call_on_rollback(self, function, *args, **kvargs): + self.on_rollback.append((function, args, kvargs)) + def commit(self): assert not self.finished + self.process_events(self.on_commit) + self.on_commit.clear() + self.on_rollback.clear() self.internal.commit() self.finished = True def rollback(self): assert not self.finished + self.process_events(self.on_rollback, skip_errors = True) + self.on_commit.clear() + self.on_rollback.clear() self.internal.rollback() self.finished = True diff --git a/db/types.py b/db/types.py index 6d9213a..3120a99 100644 --- a/db/types.py +++ b/db/types.py @@ -40,7 +40,7 @@ class Int(Type): class String(Type): def from_raw(self, value): - return str(value) + return '' if value is None else str(value) def to_db(self, connection, value): return "'" + connection.escape_string(self.from_raw(value)) + "'" @@ -52,7 +52,7 @@ class Float(Type): class Date(Type): def from_raw(self, value): - return str_to_date(date_to_str(value_raw) if type(value_raw) is datetime.datetime else str(value_raw)) + return str_to_date(date_to_str(value) if type(value) is datetime.datetime else str(value)) def from_db(self, connection, value): return set_timezone(value) diff --git a/deps.txt b/deps.txt index 2fac286..412c325 100644 --- a/deps.txt +++ b/deps.txt @@ -8,14 +8,19 @@ time datetime hashlib +os +subprocess threading traceback ### modules from python std library +base64 cgi html +http.client +shutil urllib.parse uuid diff --git a/doc/plan.ods b/doc/plan.ods index 70ee2ee..e682e2f 100644 Binary files a/doc/plan.ods and b/doc/plan.ods differ diff --git a/generator/form.py b/generator/form.py new file mode 100644 index 0000000..bf07aab --- /dev/null +++ b/generator/form.py @@ -0,0 +1,105 @@ + + +class Form(): + FORM_BEGIN = '
\n' + FORM_END = '
\n' + GROUP_BEGIN = '
\n' + GROUP_END = '
\n' + TITLE_BEGIN = '' + TITLE_END = '\n' + FIELD_BEGIN = '
' + FIELD_END = '
\n' + NAME_BEGIN = '' + NAME_END = '\n' + VALUE_BEGIN = '
' + VALUE_END = '
\n' + + BEGIN = FORM_BEGIN + GROUP_BEGIN + END = GROUP_END + FORM_END + + def __init__(self, request): + self.request = request + self.answer = self.request.answer + self.content = '' + + def title(self, title): + return self.TITLE_BEGIN \ + + self.answer.te(title) \ + + self.TITLE_END + + def name(self, name): + return self.NAME_BEGIN \ + + self.answer.te(name) \ + + self.NAME_END + + def value_raw(self, raw): + return self.VALUE_BEGIN \ + + raw \ + + self.VALUE_END + + def input(self, name, type, value = None): + type = self.answer.e(type) + name = self.answer.e(name) + value = self.answer.encodedfield(name) if value is None else self.answer.e(value) + content = ' 1: + return None + row = rows[0] + if not user: + user = self.model.users.get_by_id(row['user_id']) + if not user: + return None + return Repository(self, row, user) + + def get_list(self, user): + assert(user) + result = list() + rows = self.connection.query_dict('SELECT * FROM %T WHERE `user_id`=%d ORDER BY `name`', self.TABLE, user.id) + for row in rows: + result.append(Repository(self, row, user)) + return result + diff --git a/model/users.py b/model/users.py index 296718b..3b0dbba 100644 --- a/model/users.py +++ b/model/users.py @@ -88,7 +88,7 @@ class Users(ModelBase): return hashlib.sha512(bytes(str(id) + '|' + str(salt) + '|' + password, 'utf8')).hexdigest() def verify_login(self, login): - return type(login) is str and len(login) < 200 and login.isidentifier + return self.model.identifier(login) def can_create(self): return self.rights.issuperuser() or self.server.config['users']['selfcreate'] @@ -115,8 +115,6 @@ class Users(ModelBase): return False row = rows[0] id = int(row['id']) - print(row) - print(self.gen_password_hash(id, password)) if str(row['password']) == self.gen_password_hash(id, password): return id return 0 diff --git a/page/repo.py b/page/repo.py new file mode 100644 index 0000000..b3dc1ee --- /dev/null +++ b/page/repo.py @@ -0,0 +1,172 @@ + +import exception +from generator.form import Form + +from page.page import Page + + +class RepoCreatePage(Page): + def process(self, request, path, prevpath): + user = request.answer.objects['user'] + if path: + raise exception.HttpNotFound() + if not request.model.repositories.can_create(user.id): + raise exception.HttpNotFound() + answer = request.answer + answer.chain_title( answer.te('Create repository') ) + + form = Form(request) + form.begin('Create repository', 'repo.create') + form.add_hidden('user_id', user.id) + form.add_select('type:', 'type', { k: v.name for k, v in request.server.repotypes.items() }) + form.add_input('name:', 'name', 'text') + form.add_input('title:', 'title', 'text') + form.add_textarea('description:', 'description') + form.add_submit() + form.end() + answer.content += form.content + + return answer.complete_content() + + +class RepoPage(Page): + def __init__(self): + super().__init__() + self.view = RepoViewPage() + + def process(self, request, path, prevpath): + if not path: + raise exception.HttpNotFound() + + user = request.answer.objects['user'] + repo = None + if path[0].isdecimal(): + repo = request.model.repositories.get_by_id(int(path[0]), user) + else: + repo = request.model.repositories.get_by_name(user.id, str(path[0]), user) + if not repo: + raise exception.HttpNotFound() + + request.answer.objects['repo'] = repo + request.answer.chain_title( request.answer.e(repo.title) ) + return self.view.sub_process(request, path, prevpath) + + +class RepoViewPage(Page): + def __init__(self): + super().__init__() + self.edit = RepoUpdatePage() + self.delete = RepoDeletePage() + + def process(self, request, path, prevpath): + repo = request.answer.objects['repo'] + + answer = request.answer + answer.chain_title( answer.e(repo.name) ) + + if path: + if path[0] == 'edit': + return self.edit.sub_process(request, path, prevpath) + if path[0] == 'delete': + return self.delete.sub_process(request, path, prevpath) + raise exception.HttpNotFound() + + answer.content += '

' + answer.te('Repositiry') + '

\n' + answer.content += '

' + answer.te(repo.repotype.name) + '

\n' + answer.content += '

' + answer.e(repo.name) + '

\n' + answer.content += '

' + answer.e(repo.title) + '

\n' + answer.content += '

' + answer.e(repo.description) + '

\n' + answer.content += '

' + request.protocol + request.domain + '/repo/' + repo.gen_subpath() + '

\n' + + if repo.can_update(): + answer.content += '

' + self.make_link(answer, prevpath + ['edit'], 'Edit repository') + '

\n' + + return answer.complete_content() + + +class RepoUpdatePage(Page): + def process(self, request, path, prevpath): + if path: + raise exception.HttpNotFound() + + answer = request.answer + repo = answer.objects['repo'] + if not repo.can_update(): + raise exception.HttpNotFound() + + answer.chain_title( answer.te('Edit repository') ) + + form = Form(request) + form.begin('Edit repository', 'repo.update') + form.add_hidden('repository_id', repo.id) + form.add_input('title:', 'title', 'text', repo.title) + form.add_textarea('description:', 'description', repo.description) + form.add_submit() + form.end() + answer.content += form.content + + + if repo.can_delete() and prevpath: + url = request.get_urlpath_escaped(prevpath[:-1] + ['delete']) + form = Form(request) + form.content += '
' + form.GROUP_BEGIN + form.content += form.title('Deletion') + form.add_submit('Delete repository') + form.end() + answer.content += form.content + + return answer.complete_content() + + +class RepoDeletePage(Page): + def process(self, request, path, prevpath): + if path: + raise exception.HttpNotFound() + + answer = request.answer + repo = answer.objects['repo'] + if not repo.can_delete(): + raise exception.HttpNotFound() + + form = Form(request) + form.begin('Delete repository', 'repo.delete') + form.add_hidden('repository_id', repo.id) + form.content += '

' + answer.te(repo.repotype.name) + '

\n' + form.content += '

' + answer.e(repo.name) + '

\n' + form.content += '

' + answer.e(repo.title) + '

\n' + form.content += '

' + answer.e(repo.description) + '

\n' + form.content += '

' + answer.e(repo.description) + '

\n' + form.content += '

' + request.protocol + request.domain + '/repo/' + repo.gen_subpath() + '

\n' + form.add_submit('Confirm delete') + form.end() + + answer.content += form.content + return answer.complete_content() + + +class RepoListPage(Page): + def process(self, request, path, prevpath): + user = request.answer.objects['user'] + if path: + raise exception.HttpNotFound() + if not prevpath: + raise exception.HttpNotFound() + + repositories = request.model.repositories.get_list(user) + + answer = request.answer + answer.chain_title( answer.te('Repositories') ) + + answer.content += '

' + answer.te('Repositories') + '

\n' + for repo in repositories: + url = request.get_urlpath_escaped(prevpath[:-1] + ['repo', str(repo.name)]) + answer.content += '

' \ + + answer.e(repo.name) + ' ' \ + + answer.e(repo.title) + '

\n' + + if request.model.repositories.can_create(user.id): + answer.content += '

' + self.make_link(answer, prevpath[:-1] + ['repo_create'], 'Create repository') + '

\n' + + return answer.complete_content() + + diff --git a/page/root.py b/page/root.py index 904168f..2f4d184 100644 --- a/page/root.py +++ b/page/root.py @@ -3,9 +3,7 @@ import exception from page.page import Page -from page.user import UserPage -from page.user import UserCreatePage -from page.user import UserListPage +from page.user import UserPage, UserCreatePage, UserListPage class RootPage(Page): @@ -26,7 +24,11 @@ class RootPage(Page): return self.user_create.sub_process(request, path, prevpath) if path[0] == 'users': return self.users.sub_process(request, path, prevpath) - raise exception.HttpNotFound() + if path[0] == 'auth': + answer.headers.append( ('WWW-Authenticate', 'Basic realm="Repo", charset="UTF-8"') ) + answer.status = '401 Unauthorized' + else: + raise exception.HttpNotFound() answer.content += '

' + answer.te('root page') + '

' answer.content += '

' + answer.te('Welcome!') + '

' diff --git a/page/user.py b/page/user.py index befa0fc..28c9862 100644 --- a/page/user.py +++ b/page/user.py @@ -1,8 +1,9 @@ -from page.page import Page - import exception +from page.page import Page +from page.repo import RepoPage, RepoCreatePage, RepoListPage + class UserCreatePage(Page): def process(self, request, path, prevpath): @@ -42,6 +43,7 @@ class UserPage(Page): raise exception.HttpNotFound() request.answer.objects['user'] = user + request.answer.chain_title( request.answer.e(user.name) ) return self.profile.sub_process(request, path, prevpath) @@ -50,6 +52,9 @@ class UserProfilePage(Page): super().__init__() self.edit = UserUpdatePage() self.delete = UserDeletePage() + self.repo = RepoPage() + self.repo_create = RepoCreatePage() + self.repos = RepoListPage() def process(self, request, path, prevpath): user = request.answer.objects['user'] @@ -61,14 +66,26 @@ class UserProfilePage(Page): return self.edit.sub_process(request, path, prevpath) if path[0] == 'delete': return self.delete.sub_process(request, path, prevpath) + if path[0] == 'repo': + return self.repo.sub_process(request, path, prevpath) + if path[0] == 'repo_create': + return self.repo_create.sub_process(request, path, prevpath) + if path[0] == 'repos': + return self.repos.sub_process(request, path, prevpath) raise exception.HttpNotFound() + can_update = user.can_update() + + answer.content += '

' + answer.te('User profile') + '

\n' answer.content += '

' + answer.e(user.login) + '

\n' answer.content += '

' + answer.e(user.name) + '

\n' if user.superuser: answer.content += '

' + answer.te('Site admin') + '

\n' - if user.can_update(): + if can_update: answer.content += '

' + self.make_link(answer, prevpath + ['edit'], 'Edit user') + '

\n' + + answer.content += '

' + self.make_link(answer, prevpath + ['repos'], 'Repositories') + '

\n' + return answer.complete_content() diff --git a/repoproxy.py b/repoproxy.py new file mode 100644 index 0000000..f9eae16 --- /dev/null +++ b/repoproxy.py @@ -0,0 +1,175 @@ + + +import base64 +import http.client + +import db.holder +from model.model import Model +from answer import Result +from translator import Translator + + +class RepoProxy: + SKIP_HEADERS = { + 'connection', + 'keep-alive', + 'proxy-authenticate', + 'proxy-authorization', + 'proxy-connection', + 'te', + 'trailers', + 'transfer-encoding', + 'upgrade' } + + def __init__(self, server): + self.server = server + + def unauthorized(self): + return Result( + '401 Unauthorized', + [('WWW-Authenticate', 'Basic ream="Authorization area", charset="UTF-8"')], + list('401 Unauthorized') ) + + def forbidden(self): + return Result('403 Forbidden', list(), list('403 Forbidden')) + + def badgateway(self): + return Result('502 Bad Gateway', list(), list('502 Bad Gateway')) + + def split(self, text, separator, left = False): + x = text.split(separator, 1) + if len(x) < 2: + return ('', x[0]) if left else (x[0], '') + return x + + def input_reader(self, request): + data = request.env['wsgi.input'] + while True: + chunk = data.read(4096) + if not chunk: break + yield chunk + + def reader(self, connection, response): + while True: + chunk = response.read(4096) + if not chunk: break + yield chunk + connection.close() + + def proxy(self, request, fullurl): + s = fullurl + protocol, s = self.split(s, '://') + host, s = self.split(s, '/') + url = '/' + s + auth, host = self.split(host, '@', True) + host, port = self.split(host, ':') + if not port: port = None + + if not host or not url: + print('repoproxy: bad url [' + host + '][' + url + '][' + fullurl + ']') + return self.badgateway() + + if auth: + print('repoproxy: authorization in url is not supported [' + auth + '][' + fullurl + ']') + return self.badgateway() + + connection_class = None + if protocol == 'https': + connection_class = http.client.HTTPSConnection + elif protocol == 'http': + connection_class = http.client.HTTPConnection + if not connection_class: + print('repoproxy: protocol is not supported [' + protocol + '][' + fullurl + ']') + return self.badgateway() + + body = self.input_reader(request) + + header_prefix = 'HTTP_' + proxy_headers = dict() + for k, v in request.env.items(): + if k.startswith(header_prefix): + kk = k[len(header_prefix):].lower().replace('_', '-') + if not kk in self.SKIP_HEADERS: + proxy_headers[kk] = v + if 'CONTENT_TYPE' in request.env: + proxy_headers['content-type'] = request.env['CONTENT_TYPE'] + proxy_headers['host'] = host + + connection = None + try: + connection = connection_class(host = host, port = port) + connection.request( + request.method, + url, + body = body, + headers = proxy_headers ) + except Exception as e: + print('repoproxy: connection failed [' + fullurl + ']') + print(e) + return self.badgateway() + + response = connection.getresponse() + headers = list() + for k, v in response.getheaders(): + if not k.lower() in self.SKIP_HEADERS: + headers.append( (k, v) ) + + return Result(str(response.status) + ' ' + str(response.reason), headers, self.reader(connection, response)) + + def process(self, request, path): + if len(path) < 2: + return self.forbidden() + repoowner = path[0] + reponame = path[1] + nextpath = path[2:] + + if request.method != 'GET' and request.method != 'POST': + return self.forbidden() + + post = request.method == 'POST' + login = '' + password = '' + if 'HTTP_AUTHORIZATION' in request.env: + try: + authtype, credentials = str(request.env['HTTP_AUTHORIZATION']).split() + assert authtype.lower() == 'basic' + login, password = base64.b64decode(credentials).decode('utf8').split(':') + except Exception: + return self.unauthorized() + + url = None + with db.holder.Holder(request.server.dbpool, readonly = True) as connection: + request.connection = connection + request.model = Model(connection, Translator(), superuser = True) + + user = None + if login: + user_id = request.model.users.check_password(login, password) + if not user_id: + return self.unauthorized() + user = request.model.users.get_by_id(user_id) + assert(user) + elif post: + return self.unauthorized() + + owner = request.model.users.get_by_login( repoowner ) + if not owner: + return self.forbidden() + + repo = request.model.repositories.get_by_name( owner.id, reponame, owner ) + if not repo: + return self.forbidden() + + if post: + if user.id != repo.user_id and not request.model.rights.get_superuser(user.id): + return self.unauthorized() + + url = repo.gen_internalurl() + + getvars = request.env.get('QUERY_STRING', '') + if nextpath: + url += '/' + '/'.join(nextpath) + if getvars: + url += '?' + getvars + return self.proxy(request, url) + diff --git a/repotypes/base.py b/repotypes/base.py new file mode 100644 index 0000000..c9dcb89 --- /dev/null +++ b/repotypes/base.py @@ -0,0 +1,53 @@ + + +import os + + +class RepotypeBase: + def __init__(self, name): + self.name = name + self.server = None + self.config = None + self.path = None + + def check_config(self, server, config): + config['internalurl'] = str(config['internalurl'] or '') + while config['internalurl'][-1] == '/': + del config['internalurl'][-1] + assert config['internalurl'] + config['path'] = server.fixpath(config['path'], root = True) + + def configure(self, server, config): + self.server = server + self.config = config + self.check_config(self.server, self.config) + self.internalurl = config['internalurl'] + self.path = config['path'] + + def parentdir(self, path): + assert type(path) is str + assert path[0] == '/' + assert path[-1] != '/' + return '/'.join(path.split('/')[:-1]) + + def mkdir(self, path): + assert type(path) is str + assert path[0] == '/' + assert path[-1] != '/' + os.makedirs(path, exist_ok = True) + + def gen_path(self, subpath): + assert type(subpath) is str + assert subpath[-1] != '/' + return self.path + '/' + subpath + + def gen_internalurl(self, subpath): + assert type(subpath) is str + assert subpath[-1] != '/' + return self.internalurl + '/' + subpath + + def create(self, subpath): + raise Exception() + + def delete(self, subpath): + raise Exception() diff --git a/repotypes/git.py b/repotypes/git.py new file mode 100644 index 0000000..734e082 --- /dev/null +++ b/repotypes/git.py @@ -0,0 +1,40 @@ + + +import os +import shutil +import subprocess + +from repotypes.base import RepotypeBase + + +class RepotypeGit(RepotypeBase): + def __init__(self): + super().__init__('GIT') + + def check_config(self, server, config): + super().check_config(server, config) + config['command'] = server.fixpath(config['command'], root = True) + config['command'] = server.fixpath(config['command'], root = True) + config['environment'] = server.fixenv(config['environment']) + + def configure(self, server, config): + super().configure(server, config) + self.command = self.config['command'] + self.environment = self.config['environment'] + assert self.command[0] == '/' + assert not self.environment is None + + def create(self, subpath): + path = self.gen_path(subpath) + print(self.name + ': Creating repository by path: ' + path) + parentpath = self.parentdir(path) + self.mkdir(parentpath) + subprocess.run([self.command, 'init', '--bare', path], env = self.environment, cwd = parentpath) + subprocess.run([self.command, 'config', 'http.receivepack', 'true'], env = self.environment, cwd = path) + + def delete(self, subpath): + path = self.gen_path(subpath) + print(self.name + ': Deleting repository by path: ' + path) + if os.path.exists(path): + shutil.rmtree( self.gen_path(subpath) ) + diff --git a/request.py b/request.py index af79cd1..30a2fb0 100644 --- a/request.py +++ b/request.py @@ -12,6 +12,8 @@ class Request: self.server = server self.env = env self.method = str(self.env["REQUEST_METHOD"]) + self.protocol = self.server.config['protocol'] + self.domain = self.server.config['domain'] self.prefix = self.server.config['urlprefix'] self.urlvars = dict() @@ -32,33 +34,12 @@ class Request: assert k not in self.urlvars self.urlvars[k] = v - # read post vars - if self.method == 'POST': - postvars = cgi.FieldStorage( - fp = self.env["wsgi.input"], - environ = self.env ) - duplicates = set() - for k in postvars: - kk = str(k) - if kk in self.postvars: - duplicates.add(kk) - del self.postvars[kk] - elif not kk in duplicates: - self.postvars[kk] = postvars[kk].value - - # get action - if 'action' in self.postvars: - action = self.postvars['action'] - del self.postvars['action'] - if not action is None: - self.action = str(action) - # read cookies self.cookie = self.env.get('HTTP_COOKIE', '') assert(type(self.cookie) is str) # read path - path = self.env["REQUEST_URI"] + path = self.env["REQUEST_URI"].split('?')[0] if path.startswith(self.prefix): path = path[len(self.prefix):] path_list = [x for x in path.split('/') if x] @@ -73,7 +54,28 @@ class Request: self.answer = answer.Answer(self) self.server.sessions.attach_session(self) - + + def read_posrvars(self): + if self.method == 'POST': + postvars = cgi.FieldStorage( + fp = self.env["wsgi.input"], + environ = self.env ) + duplicates = set() + for k in postvars: + kk = str(k) + if kk in self.postvars: + duplicates.add(kk) + del self.postvars[kk] + elif not kk in duplicates: + self.postvars[kk] = postvars[kk].value + + # get action + if 'action' in self.postvars: + action = self.postvars['action'] + del self.postvars['action'] + if not action is None: + self.action = str(action) + def create_model(self, connection): self.connection = connection self.model = Model(self.connection, self.answer, self.session.user_id if self.session else 0) @@ -82,14 +84,17 @@ class Request: if not self.user: self.server.sessions.close_session(self) - def get_urlpath(self, path = None): + def get_urlpath(self, path = None, domain = False): if path is None: path = self.path assert(type(path) is list) - return self.prefix + '/' + '/'.join(path) + urlpath = self.prefix + '/' + '/'.join(path) + if domain: + urlpath = 'https://' + self.domain + urlpath + return urlpath - def get_urlpath_escaped(self, path = None): - return self.answer.e( self.get_urlpath(path) ) + def get_urlpath_escaped(self, path = None, domain = False): + return self.answer.e( self.get_urlpath(path, domain) ) def translate(self, text): return self.answer.translate(text) diff --git a/server.py b/server.py index c6c480f..7ce5fc2 100644 --- a/server.py +++ b/server.py @@ -8,21 +8,59 @@ import page.root import page.error import action.actions import session +from repoproxy import RepoProxy +from repotypes.git import RepotypeGit class Server: def __init__(self, config): - urlprefix = str(config.get('urlprefix', '')) - while urlprefix[-1:] == '/': - urlprefix = urlprefix[:-1] + self.repotypes = { + 'git': RepotypeGit() } + self.config = self.apply_defaults(config) + self.check_config(self.config) + + self.dbtypebytype = db.types.bytype + self.dbtypebychar = db.types.bychar + self.dbpool = db.pool.Pool(self) + self.dbcache = db.cache.Cache(self) + + self.sessions = session.SessionManager(self) + + self.repoproxy = RepoProxy(self) + + self.pageroot = page.root.RootPage() + self.pageforbidden = page.error.ErrorPage('403', 'Forbidden') + self.pagenotfound = page.error.ErrorPage('404', 'Not Found') + + self.actions = action.actions.actions + + for key, repotype in self.repotypes.items(): + repotype.configure(self, self.config['repositories'][key]) + + + def get_defaults(self): + return self.apply_defaults(dict()) + + + def apply_defaults(self, config): config_db = config.get('db', dict()) config_db_pool = config_db.get('pool', dict()) config_db_cache = config_db.get('cache', dict()) + config_users = config.get('users', dict()) + config_session = config.get('session', dict()) - self.config = { + config_repositories = config.get('repositories', dict()) + config_repositories_git = config_repositories.get('git', dict()) + + urlprefix = self.fixpath( config.get('urlprefix', '') ) + + return { + 'protocol' : str(config.get('protocol', 'https://')), + 'domain' : str(config.get('domain', '')), + 'urlprefix' : urlprefix, 'urldataprefix' : str(config.get('urldataprefix', urlprefix + '/data')), @@ -50,30 +88,55 @@ class Server: 'session': { 'time' : float(config_session.get('time', 30*60)) }, + + 'repositories': { + 'git': { + 'internalurl' : str(config_repositories_git.get('internalurl', '')), + 'path' : str(config_repositories_git.get('path', '')), + 'command' : str(config_repositories_git.get('command', '/usr/bin/git')), + 'environment' : config_repositories_git.get('environment', dict()), + }, + }, } + + + def check_config(self, config): + assert config['protocol'] == 'http://' \ + or config['protocol'] == 'https://' + assert config['db']['retrytime'] >= 0 + assert config['db']['prefix'] == '' \ + or config['db']['prefix'].isidentifier() + assert config['db']['pool']['read'] > 0 + assert config['db']['pool']['write'] > 0 + assert config['db']['cache']['maxcount'] > 0 + assert config['session']['time'] > 0 - assert self.config['db']['retrytime'] >= 0 - assert self.config['db']['prefix'] == '' \ - or self.config['db']['prefix'].isidentifier() - assert self.config['db']['pool']['read'] > 0 - assert self.config['db']['pool']['write'] > 0 - assert self.config['db']['cache']['maxcount'] > 0 - assert self.config['session']['time'] > 0 - - if self.config['urlprefix'] != '': - assert self.config['urlprefix'][0] == '/' - for i in self.config['urlprefix'][1:].split('/'): + for i in config['domain'].split('.'): + assert i.isidentifier() + + config['urlprefix'] = self.fixpath(config['urlprefix']) + config['urldataprefix'] = self.fixpath(config['urldataprefix']) + + for key, repotype in self.repotypes.items(): + repotype.check_config(self, config['repositories'][key]) + + + def fixpath(self, path, root = False): + assert type(path) is str + while path and path[-1] == '/': + del path[-1] + if path != '': + assert path[0] == '/' + for i in path[1:].split('/'): assert i.isalnum() - - self.dbtypebytype = db.types.bytype - self.dbtypebychar = db.types.bychar - self.dbpool = db.pool.Pool(self) - self.dbcache = db.cache.Cache(self) + if root: + assert path and path[0] == '/' + return path - self.sessions = session.SessionManager(self) - - self.pageroot = page.root.RootPage() - self.pageforbidden = page.error.ErrorPage('403', 'Forbidden') - self.pagenotfound = page.error.ErrorPage('404', 'Page Not Found') - - self.actions = action.actions.actions + + def fixenv(self, environment): + assert type(environment) is dict + for k, v in environment: + assert type(k) is str and k.isidentifier() + assert type(v) is str + return environment diff --git a/translator.py b/translator.py index 4ef0528..3afeedc 100644 --- a/translator.py +++ b/translator.py @@ -2,6 +2,6 @@ class Translator: def translate(self, text): - return str(text) + return str('' if text is None else text) def t(self, text): return self.translate(text)