summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormcrute <devnull@localhost>2011-02-24 00:11:42 -0500
committermcrute <devnull@localhost>2011-02-24 00:11:42 -0500
commit82aaff86a5aefbadc0e520949976ec11e312d2b6 (patch)
tree660a6a2ef9dabc1bb25959b048cde435a8f175c8
downloadhg_hosting-82aaff86a5aefbadc0e520949976ec11e312d2b6.tar.bz2
hg_hosting-82aaff86a5aefbadc0e520949976ec11e312d2b6.tar.xz
hg_hosting-82aaff86a5aefbadc0e520949976ec11e312d2b6.zip
Initial import
-rw-r--r--.hgignore5
-rwxr-xr-xlock-repo.py37
-rw-r--r--repolib.py275
-rw-r--r--scripts.log119
-rwxr-xr-xserver.fcgi11
-rwxr-xr-xsync-repo-config.py55
-rwxr-xr-xvalidate-login.py63
7 files changed, 565 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..88f8e1d
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,5 @@
1^\.ssh/
2^repos/
3\.pyc$
4^\.viminfo$
5^\.bash_history$
diff --git a/lock-repo.py b/lock-repo.py
new file mode 100755
index 0000000..f651d3a
--- /dev/null
+++ b/lock-repo.py
@@ -0,0 +1,37 @@
1#!/usr/bin/env python
2# vim: set filencoding=utf8
3"""
4Mercurial Shared SSH Repo Lock Script
5
6@author: Mike Crute (mcrute@gmail.com)
7@organization: SoftGroup Hosting
8@date: February 23, 2011
9"""
10
11import os
12import repolib
13
14def main(argv):
15 log = repolib.get_logger('validate-login')
16
17 if ('SSH_HG_REPO' not in os.environ or
18 'SSH_HG_USER' not in os.environ):
19 log.error("Failed to execute pre-lock checks")
20 return 1
21
22 try:
23 repo = repolib.Repository(os.environ['SSH_HG_REPO'])
24 repo.load_from_hgrc()
25 except IOError:
26 log.error("Could not load repository config")
27 return 1
28
29 if not repo.can_be_written_by(os.environ['SSH_HG_USER']):
30 log.error("You can not write to this repository")
31 return 1
32
33 return 0
34
35if __name__ == "__main__":
36 import sys
37 sys.exit(main(sys.argv[1:]))
diff --git a/repolib.py b/repolib.py
new file mode 100644
index 0000000..1ba4814
--- /dev/null
+++ b/repolib.py
@@ -0,0 +1,275 @@
1# vim: set filencoding=utf8
2"""
3Mercurial Repository Management Library
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Hosting
7@date: February 23, 2011
8"""
9
10import os.path
11import logging
12from StringIO import StringIO
13from ConfigParser import SafeConfigParser
14
15
16class Adornments(object):
17
18 BASE_CSS = ("padding: 0px 4px;" "font-size: 10px;"
19 "font-weight: normal;" "border: 1px solid;"
20 "background-color: #fafafa;"
21 "border-color: #ffffff #f0f0f0 #f0f0f0 #ffffff;")
22
23 CSS_YELLOW = ("background-color: #ffe500;" "color: #222222;"
24 "border-color: #fff066 #a89e43 #a89e43 #fff066;")
25
26 CSS_BROWN = ("background-color: #815555;" "color: #ffffff;"
27 "border-color: #ff3347 #541118 #541118 #ff3347;")
28
29 CSS_GREEN = ("background-color: #11a800;" "color: #ffffff;"
30 "border-color: #5eff4c #3ea832 #3ea832 #5eff4c;")
31
32 CSS_BLUE = ("background-color: #0099ff;" "color: #ffffff;"
33 "border-color: #4cb8ff #3279a8 #3279a8 #4cb8ff;")
34
35 CSS_RED = ("background-color: #ff0000;" "color: #ffffff;"
36 "border-color: #ff6666 #000000 #000000 #ff6666;")
37
38 BASE_HTML = '<span style="{0}{{css}}">{{tag}}</span> '.format(BASE_CSS)
39 LINK_CSS_DARK = 'color: #ffffff; text-decoration: none;'
40 LINK_CSS_LIGHT = 'color: #000000; text-decoration: none;'
41
42 def __init__(self, repo):
43 self.repo = repo
44
45 def __str__(self):
46 if self.repo.moved_to:
47 tag = '<a style="{link_css}" href="{url}">MOVED</a>'
48 tag = tag.format(link_css=self.LINK_CSS_DARK,
49 url=self.repo.moved_to)
50 return self.BASE_HTML.format(css=self.CSS_BLUE, tag=tag)
51
52 if self.repo.defunct:
53 return self.BASE_HTML.format(css=self.CSS_BROWN, tag="DEFUNCT")
54
55 if self.repo.upstream:
56 tag = '<a style="{link_css}" href="{url}">FORK</a>'
57 tag = tag.format(link_css=self.LINK_CSS_LIGHT,
58 url=self.repo.upstream)
59 return self.BASE_HTML.format(css=self.CSS_YELLOW, tag=tag)
60
61 return ""
62
63
64class Repository(object):
65
66 def __init__(self, path, description=None, contact=None):
67 self.path = path
68 self.description = description
69 self.contact = contact
70 self.readers = []
71 self.writers = []
72 self.defunct = False
73 self.moved_to = None
74 self.repo_path = None
75 self.upstream = None
76 self.lock_script = None
77
78 def can_be_read_by(self, user):
79 return user in self.readers
80
81 def can_be_written_by(self, user):
82 return user in self.writers
83
84 @property
85 def exists(self):
86 return os.path.exists(self.full_path)
87
88 @property
89 def full_path(self):
90 return os.path.join(self.repo_path, self.path)
91
92 @property
93 def hgrc_path(self):
94 return os.path.join(self.full_path, '.hg', 'hgrc')
95
96 @classmethod
97 def from_config(cls, config, repo_path, lock_script=None):
98 self = cls(config['path'], config.get('description', None),
99 config.get('contact', None))
100
101 self.readers = ConfigLoader.as_list(config.get('read', ''))
102 self.writers = ConfigLoader.as_list(config.get('write', ''))
103
104 self.defunct = ConfigLoader.as_bool(config.get('defunct', 'no'))
105 self.moved_to = config.get('moved_to', None)
106 self.upstream = config.get('upstream', None)
107 self.upstream = config.get('upstream', None)
108
109 self.repo_path = repo_path
110 self.lock_script = lock_script
111
112 return self
113
114 def build_hgrc(self, users):
115 buf = StringIO()
116
117 buf.write("# This file is generated by sync-repo-config\n")
118 buf.write("# Changes are over-written on each run\n\n")
119
120 if self.contact or self.description or self.writers:
121 buf.write("[web]\n")
122
123 if self.contact:
124 buf.write("contact = {0}\n".format(users[self.contact]))
125
126 if self.description:
127 buf.write("description = {adornments}"
128 " {self.description}\n".format(
129 adornments=Adornments(self), self=self))
130
131 if self.writers:
132 buf.write("allow_push = {0}\n".format(",".join(self.writers)))
133
134 buf.write("\n")
135
136 if self.lock_script:
137 buf.write("[hooks]\n")
138 buf.write("pretxnchangegroup.deny.lock = {0}\n".format(
139 self.lock_script))
140 buf.write("\n")
141
142 buf.write("[ssh]\n")
143 buf.write("readers = {0}\n".format(",".join(self.readers)))
144 buf.write("writers = {0}\n".format(",".join(self.writers)))
145
146 return buf.getvalue()
147
148 def write_hgrc(self, users):
149 if not self.exists:
150 raise IOError("Repository {0!r} does not exist".format(
151 self.full_path))
152
153 with open(self.hgrc_path, 'w') as hgrc:
154 hgrc.write(self.build_hgrc(users))
155
156 def load_hgrc(self):
157 hgrc = SafeConfigParser()
158 with open(self.hgrc_path, 'r') as fp:
159 hgrc.readfp(fp)
160
161 return hgrc
162
163 def load_from_hgrc(self):
164 hgrc = self.load_hgrc()
165
166 self.description = hgrc.get("web", "description")
167 self.contact = hgrc.get("web", "contact")
168 self.readers = ConfigLoader.as_list(hgrc.get("ssh", "readers"))
169 self.writers = ConfigLoader.as_list(hgrc.get("ssh", "writers"))
170
171
172class User(object):
173
174 def __init__(self, username, name, email):
175 self.username = username
176 self.name = name
177 self.email = email
178 self.can_create = False
179 self.ssh_key = None
180 self.login_script = None
181
182 def __str__(self):
183 return "{self.name} <{self.email}>".format(self=self)
184
185 @classmethod
186 def from_config(cls, config, login_script):
187 self = cls(config['username'], config['name'], config['email'])
188
189 self.can_create = ConfigLoader.as_bool(config['can_create'])
190 self.ssh_key = config['ssh_key']
191 self.login_script = login_script
192
193 return self
194
195 @property
196 def ssh_line(self):
197 return ('command="{self.login_script} {self.username}",'
198 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
199 ' {self.ssh_key} {self.email}\n').format(self=self)
200
201
202class ConfigLoader(object):
203
204 def __init__(self, cfg_file):
205 self.config = SafeConfigParser()
206 with open(cfg_file, 'r') as config_file:
207 self.config.readfp(config_file)
208
209 self.login_script = self.config.get('system', 'login_script')
210 self.repo_path = self.config.get('system', 'repo_path')
211 self.repo_user = self.config.get('system', 'repo_user')
212 self.lock_script = self.config.get('system', 'lock_script')
213
214 def _filtered_sections(self, section_name):
215 for section in self.config.sections():
216 if section.startswith(section_name + ":"):
217 yield section
218
219 @staticmethod
220 def as_bool(value):
221 return value.lower() in ('yes', 'true', 'on')
222
223 @staticmethod
224 def as_list(value):
225 return [item.strip() for item in value.split(",")
226 if value]
227
228 @staticmethod
229 def clean_section_name(name):
230 return name.split(':')[1]
231
232 @property
233 def repos(self):
234 for section in self._filtered_sections('repo'):
235 values = dict(self.config.items(section))
236 values['path'] = self.clean_section_name(section)
237
238 yield Repository.from_config(values, self.repo_path,
239 self.lock_script)
240
241 @property
242 def users(self):
243 for section in self._filtered_sections('user'):
244 values = dict(self.config.items(section))
245 values['username'] = self.clean_section_name(section)
246
247 yield User.from_config(values, self.login_script)
248
249 @property
250 def user_dict(self):
251 return dict((user.username, user) for user in self.users)
252
253 @property
254 def repo_user_authorized_keys(self):
255 home_dir = os.path.expanduser('~' + self.repo_user)
256
257 if '~' in home_dir:
258 raise ValueError("User {0!r} doesn't exist".format(self.repo_user))
259
260 return os.path.join(home_dir, '.ssh', 'authorized_keys')
261
262
263def get_logger(name):
264 format = "%(levelname)s: %(message)s"
265 logging.basicConfig(level=logging.DEBUG, format=format,
266 filename="scripts.log")
267
268 stream = logging.StreamHandler()
269 stream.setLevel(logging.WARNING)
270 stream.setFormatter(logging.Formatter(format))
271
272 logger = logging.getLogger(name)
273 logger.addHandler(stream)
274
275 return logger
diff --git a/scripts.log b/scripts.log
new file mode 100644
index 0000000..4684049
--- /dev/null
+++ b/scripts.log
@@ -0,0 +1,119 @@
1ERROR: Repo /srv/hg/repos/private/vim-site does not exist
2INFO: Writing hgrc for 'clients/cameo_tickets_php'
3INFO: Writing hgrc for 'forks/hg'
4INFO: Writing hgrc for 'private/slate_machine'
5INFO: Writing hgrc for 'clients/maxwellness-rewards'
6INFO: Writing hgrc for 'mongo_madness'
7INFO: Writing hgrc for 'greenbox'
8INFO: Writing hgrc for 'codemash_taste_of_cocoa'
9INFO: Writing hgrc for 'build_scripts'
10INFO: Writing hgrc for 'private/vim-site'
11INFO: Writing hgrc for 'clients/finelli_site'
12INFO: Writing hgrc for 'private/mcrute_blog_drafts'
13INFO: Writing hgrc for 'clients/cameo_tickets_py'
14INFO: Writing hgrc for 'milkman'
15INFO: Writing hgrc for 'hudson_sc'
16INFO: Writing hgrc for 'code_manager'
17INFO: Writing hgrc for 'dev_urandom'
18INFO: Writing hgrc for 'designer_site'
19INFO: Writing hgrc for 'pyrobots'
20INFO: Writing hgrc for 'svn_repolist'
21INFO: Writing hgrc for 'code_golf'
22INFO: Writing hgrc for 'firewall'
23INFO: Writing hgrc for 'screen_hooks'
24INFO: Writing hgrc for 'avolites_show_cleanup'
25INFO: Writing hgrc for 'clients/chapman_site'
26INFO: Writing hgrc for 'python_koans'
27INFO: Writing hgrc for 'pydepgraph'
28INFO: Writing hgrc for 'ventriloquist'
29INFO: Writing hgrc for 'myturl_clients'
30INFO: Writing hgrc for 'zine-patches'
31INFO: Writing hgrc for 'kronos'
32INFO: Writing hgrc for 'css_compressor'
33INFO: Writing hgrc for 'clients/affordable_site'
34INFO: Writing hgrc for 'mcrute_dotfiles'
35INFO: Writing hgrc for 'codemash_iphone'
36INFO: Writing hgrc for 'clients/brewbuster'
37INFO: Writing hgrc for 'clients/cris_site'
38INFO: Writing hgrc for 'cardigan'
39INFO: Writing hgrc for 'ventriloquy'
40INFO: Writing hgrc for 'conductor'
41INFO: Writing hgrc for 'pyapache'
42INFO: Writing hgrc for 'private/personal_wiki'
43INFO: Writing hgrc for 'clients/cameo_website'
44INFO: Writing hgrc for 'calendar_proxy'
45INFO: Writing hgrc for 'pompom_configs'
46INFO: Writing hgrc for 'opendmx_experiments'
47INFO: Writing hgrc for 'wordpress_plugins'
48INFO: Writing hgrc for 'nose_machineout'
49INFO: Writing hgrc for 'awesomebar'
50INFO: Writing hgrc for 'hg_sshsign'
51INFO: Writing hgrc for 'code_mining'
52INFO: Writing hgrc for 'snakeplan'
53INFO: Writing hgrc for 'clepy_no_frameworks'
54INFO: Writing hgrc for 'full_armor_site'
55INFO: Writing hgrc for 'clients/sweet_dreams'
56INFO: Writing hgrc for 'mike_crute_org'
57INFO: Writing '/srv/hg/.ssh/authorized_keys'
58INFO: Writing user 'Mike Crute <mcrute@gmail.com>'
59INFO: Writing user 'Marc Weber <marco-oweber@gmx.de>'
60INFO: Writing user 'Max Cantor <max@maxcantor.net>'
61INFO: Writing hgrc for 'clients/cameo_tickets_php'
62INFO: Writing hgrc for 'forks/hg'
63INFO: Writing hgrc for 'private/slate_machine'
64INFO: Writing hgrc for 'clients/maxwellness-rewards'
65INFO: Writing hgrc for 'mongo_madness'
66INFO: Writing hgrc for 'greenbox'
67INFO: Writing hgrc for 'codemash_taste_of_cocoa'
68INFO: Writing hgrc for 'build_scripts'
69INFO: Writing hgrc for 'private/vim-site'
70INFO: Writing hgrc for 'clients/finelli_site'
71INFO: Writing hgrc for 'private/mcrute_blog_drafts'
72INFO: Writing hgrc for 'clients/cameo_tickets_py'
73INFO: Writing hgrc for 'milkman'
74INFO: Writing hgrc for 'hudson_sc'
75INFO: Writing hgrc for 'code_manager'
76INFO: Writing hgrc for 'dev_urandom'
77INFO: Writing hgrc for 'designer_site'
78INFO: Writing hgrc for 'pyrobots'
79INFO: Writing hgrc for 'svn_repolist'
80INFO: Writing hgrc for 'code_golf'
81INFO: Writing hgrc for 'firewall'
82INFO: Writing hgrc for 'screen_hooks'
83INFO: Writing hgrc for 'avolites_show_cleanup'
84INFO: Writing hgrc for 'clients/chapman_site'
85INFO: Writing hgrc for 'python_koans'
86INFO: Writing hgrc for 'pydepgraph'
87INFO: Writing hgrc for 'ventriloquist'
88INFO: Writing hgrc for 'myturl_clients'
89INFO: Writing hgrc for 'zine-patches'
90INFO: Writing hgrc for 'kronos'
91INFO: Writing hgrc for 'css_compressor'
92INFO: Writing hgrc for 'clients/affordable_site'
93INFO: Writing hgrc for 'mcrute_dotfiles'
94INFO: Writing hgrc for 'codemash_iphone'
95INFO: Writing hgrc for 'clients/brewbuster'
96INFO: Writing hgrc for 'clients/cris_site'
97INFO: Writing hgrc for 'cardigan'
98INFO: Writing hgrc for 'ventriloquy'
99INFO: Writing hgrc for 'conductor'
100INFO: Writing hgrc for 'pyapache'
101INFO: Writing hgrc for 'private/personal_wiki'
102INFO: Writing hgrc for 'clients/cameo_website'
103INFO: Writing hgrc for 'calendar_proxy'
104INFO: Writing hgrc for 'pompom_configs'
105INFO: Writing hgrc for 'opendmx_experiments'
106INFO: Writing hgrc for 'wordpress_plugins'
107INFO: Writing hgrc for 'nose_machineout'
108INFO: Writing hgrc for 'awesomebar'
109INFO: Writing hgrc for 'hg_sshsign'
110INFO: Writing hgrc for 'code_mining'
111INFO: Writing hgrc for 'snakeplan'
112INFO: Writing hgrc for 'clepy_no_frameworks'
113INFO: Writing hgrc for 'full_armor_site'
114INFO: Writing hgrc for 'clients/sweet_dreams'
115INFO: Writing hgrc for 'mike_crute_org'
116INFO: Writing '/srv/hg/.ssh/authorized_keys'
117INFO: Writing user 'Mike Crute <mcrute@gmail.com>'
118INFO: Writing user 'Marc Weber <marco-oweber@gmx.de>'
119INFO: Writing user 'Max Cantor <max@maxcantor.net>'
diff --git a/server.fcgi b/server.fcgi
new file mode 100755
index 0000000..d8fc53a
--- /dev/null
+++ b/server.fcgi
@@ -0,0 +1,11 @@
1#!/usr/bin/env python
2
3from flup.server.fcgi_fork import WSGIServer
4from mercurial import demandimport; demandimport.enable()
5from mercurial.hgweb.hgwebdir_mod import hgwebdir
6
7
8WSGIServer(hgwebdir('/etc/hgweb.cfg'),
9 bindAddress='/tmp/hgserver.sock',
10 maxRequests=10, minSpare=1, maxSpare=3,
11 maxChildren=3).run()
diff --git a/sync-repo-config.py b/sync-repo-config.py
new file mode 100755
index 0000000..b706e02
--- /dev/null
+++ b/sync-repo-config.py
@@ -0,0 +1,55 @@
1#!/usr/bin/env python
2# vim: set filencoding=utf8
3"""
4Mercurial Shared SSH Repository Metadat Sync
5
6@author: Mike Crute (mcrute@gmail.com)
7@organization: SoftGroup Hosting
8@date: February 23, 2011
9"""
10
11import repolib
12
13
14def sync_repository_config(repos, users, log):
15 for repo in repos:
16 if repo.exists:
17 log.info("Writing hgrc for %r", repo.path)
18 repo.write_hgrc(users)
19 else:
20 log.warn("Non-existent repo %r", repo.path)
21
22
23def sync_ssh_config(auth_keys_filename, users, log):
24 with open(auth_keys_filename, 'w') as auth_keys:
25 log.info("Writing %r", auth_keys_filename)
26
27 for user in users:
28 log.info("Writing user '%s'", user)
29 auth_keys.write(user.ssh_line)
30
31
32def main(argv):
33 log = repolib.get_logger('sync-repo-config')
34
35 try:
36 cfg_file = argv[-1] if argv else "/etc/hgssh.cfg"
37 cfg = repolib.ConfigLoader(cfg_file)
38 except IOError:
39 log.error("Config file %r doesn't exist", cfg_file)
40 return 1
41
42 sync_repository_config(cfg.repos, cfg.user_dict, log)
43
44 try:
45 sync_ssh_config(cfg.repo_user_authorized_keys, cfg.users, log)
46 except ValueError, exc:
47 log.error("%s", exc)
48 return 1
49
50 return 0
51
52
53if __name__ == "__main__":
54 import sys
55 sys.exit(main(sys.argv[1:]))
diff --git a/validate-login.py b/validate-login.py
new file mode 100755
index 0000000..bd87fff
--- /dev/null
+++ b/validate-login.py
@@ -0,0 +1,63 @@
1#!/usr/bin/env python
2# vim: set filencoding=utf8
3"""
4Mercurial Shared SSH Login Validator
5
6@author: Mike Crute (mcrute@gmail.com)
7@organization: SoftGroup Hosting
8@date: February 23, 2011
9"""
10
11import re
12import os
13import repolib
14
15from mercurial import demandimport; demandimport.enable()
16from mercurial import dispatch
17
18
19def parse_path():
20 cmd = os.environ.get('SSH_ORIGINAL_COMMAND', '')
21 path = re.match("hg -R (\S+) serve --stdio", cmd)
22
23 if path:
24 return path.groups()[0]
25
26 return None
27
28
29def main(argv):
30 log = repolib.get_logger('validate-login')
31
32 path = parse_path()
33 if path:
34 repo = repolib.Repository(path)
35 repo.repo_path = os.getcwd()
36 else:
37 log.error("Invalid command")
38 return 1
39
40 if not repo.exists:
41 log.error("Repo %s does not exist", repo.full_path)
42 return 1
43
44 try:
45 repo.load_from_hgrc()
46 except IOError:
47 log.error("Could not read repo config")
48 return 1
49
50 user = argv[-1]
51 if not repo.can_be_read_by(user):
52 log.error("You can not read this repository")
53 return 1
54
55 os.environ['SSH_HG_USER'] = user
56 os.environ['SSH_HG_REPO'] = repo.full_path
57
58 dispatch.dispatch(['-R', path, 'serve', '--stdio'])
59
60
61if __name__ == "__main__":
62 import sys
63 sys.exit(main(sys.argv[1:]))