blaggard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +66 -0
- data/README.md +84 -0
- data/Rakefile +20 -0
- data/bin/blaggard +55 -0
- data/blaggard.gemspec +18 -0
- data/config.yml.example +9 -0
- data/install.txt +60 -0
- data/lib/blaggard.rb +33 -0
- data/lib/blaggard/advertisement.rb +92 -0
- data/lib/blaggard/auth.rb +37 -0
- data/lib/blaggard/bundle.rb +20 -0
- data/lib/blaggard/console.rb +3 -0
- data/lib/blaggard/git.rb +82 -0
- data/lib/blaggard/group_config.rb +123 -0
- data/lib/blaggard/group_finder.rb +28 -0
- data/lib/blaggard/server.rb +311 -0
- data/script/console +7 -0
- data/spec/advertisement_spec.rb +81 -0
- data/spec/fixtures/spec_repo.git/FETCH_HEAD +2 -0
- data/spec/fixtures/spec_repo.git/HEAD +1 -0
- data/spec/fixtures/spec_repo.git/HEAD_TRACKER +1 -0
- data/spec/fixtures/spec_repo.git/config +53 -0
- data/spec/fixtures/spec_repo.git/index +0 -0
- data/spec/fixtures/spec_repo.git/logs/HEAD +7 -0
- data/spec/fixtures/spec_repo.git/logs/refs/heads/br2 +2 -0
- data/spec/fixtures/spec_repo.git/logs/refs/heads/master +2 -0
- data/spec/fixtures/spec_repo.git/logs/refs/heads/not-good +1 -0
- data/spec/fixtures/spec_repo.git/logs/refs/remotes/origin/HEAD +1 -0
- data/spec/fixtures/spec_repo.git/logs/refs/remotes/test/master +2 -0
- data/spec/fixtures/spec_repo.git/objects/08/b041783f40edfe12bb406c9c9a8a040177c125 +0 -0
- data/spec/fixtures/spec_repo.git/objects/13/85f264afb75a56a5bec74243be9b367ba4ca08 +0 -0
- data/spec/fixtures/spec_repo.git/objects/18/1037049a54a1eb5fab404658a3a250b44335d7 +0 -0
- data/spec/fixtures/spec_repo.git/objects/18/10dff58d8a660512d4832e740f692884338ccd +0 -0
- data/spec/fixtures/spec_repo.git/objects/1a/443023183e3f2bfbef8ac923cd81c1018a18fd +0 -0
- data/spec/fixtures/spec_repo.git/objects/1b/8cbad43e867676df601306689fe7c3def5e689 +0 -0
- data/spec/fixtures/spec_repo.git/objects/1f/67fc4386b2d171e0d21be1c447e12660561f9b +0 -0
- data/spec/fixtures/spec_repo.git/objects/25/8f0e2a959a364e40ed6603d5d44fbb24765b10 +0 -0
- data/spec/fixtures/spec_repo.git/objects/27/0b8ea76056d5cad83af921837702d3e3c2924d +0 -0
- data/spec/fixtures/spec_repo.git/objects/2d/59075e0681f540482d4f6223a68e0fef790bc7 +0 -0
- data/spec/fixtures/spec_repo.git/objects/32/59a6bd5b57fb9c1281bb7ed3167b50f224cb54 +0 -0
- data/spec/fixtures/spec_repo.git/objects/36/97d64be941a53d4ae8f6a271e4e3fa56b022cc +0 -0
- data/spec/fixtures/spec_repo.git/objects/45/b983be36b73c0788dc9cbcb76cbb80fc7bb057 +0 -0
- data/spec/fixtures/spec_repo.git/objects/4a/202b346bb0fb0db7eff3cffeb3c70babbd2045 +2 -0
- data/spec/fixtures/spec_repo.git/objects/4a/23e2e65ad4e31c4c9db7dc746650bfad082679 +0 -0
- data/spec/fixtures/spec_repo.git/objects/4b/22b35d44b5a4f589edf3dc89196399771796ea +0 -0
- data/spec/fixtures/spec_repo.git/objects/52/1d87c1ec3aef9824daf6d96cc0ae3710766d91 +0 -0
- data/spec/fixtures/spec_repo.git/objects/5b/5b025afb0b4c913b4c338a42934a3863bf3644 +2 -0
- data/spec/fixtures/spec_repo.git/objects/75/057dd4114e74cca1d750d0aee1647c903cb60a +0 -0
- data/spec/fixtures/spec_repo.git/objects/76/3d71aadf09a7951596c9746c024e7eece7c7af +1 -0
- data/spec/fixtures/spec_repo.git/objects/7b/4384978d2493e851f9cca7858815fac9b10980 +0 -0
- data/spec/fixtures/spec_repo.git/objects/81/4889a078c031f61ed08ab5fa863aea9314344d +0 -0
- data/spec/fixtures/spec_repo.git/objects/84/96071c1b46c854b31185ea97743be6a8774479 +0 -0
- data/spec/fixtures/spec_repo.git/objects/84/9a5e34a26815e821f865b8479f5815a47af0fe +2 -0
- data/spec/fixtures/spec_repo.git/objects/94/4c0f6e4dfa41595e6eb3ceecdb14f50fe18162 +1 -0
- data/spec/fixtures/spec_repo.git/objects/9a/03079b8a8ee85a0bee58bf9be3da8b62414ed4 +0 -0
- data/spec/fixtures/spec_repo.git/objects/9f/13f7d0a9402c681f91dc590cf7b5470e6a77d2 +2 -0
- data/spec/fixtures/spec_repo.git/objects/9f/d738e8f7967c078dceed8190330fc8648ee56a +3 -0
- data/spec/fixtures/spec_repo.git/objects/a4/a7dce85cf63874e984719f4fdd239f5145052f +2 -0
- data/spec/fixtures/spec_repo.git/objects/a6/5fedf39aefe402d3bb6e24df4d4f5fe4547750 +3 -0
- data/spec/fixtures/spec_repo.git/objects/a7/1586c1dfe8a71c6cbf6c129f404c5642ff31bd +0 -0
- data/spec/fixtures/spec_repo.git/objects/a8/233120f6ad708f843d861ce2b7228ec4e3dec6 +0 -0
- data/spec/fixtures/spec_repo.git/objects/ae/90f12eea699729ed24555e40b9fd669da12a12 +0 -0
- data/spec/fixtures/spec_repo.git/objects/b2/5fa35b38051e4ae45d4222e795f9df2e43f1d1 +2 -0
- data/spec/fixtures/spec_repo.git/objects/b6/361fc6a97178d8fc8639fdeed71c775ab52593 +0 -0
- data/spec/fixtures/spec_repo.git/objects/be/3563ae3f795b2b4353bcce3a527ad0a4f7f644 +3 -0
- data/spec/fixtures/spec_repo.git/objects/c4/7800c7266a2be04c571c04d5a6614691ea99bd +3 -0
- data/spec/fixtures/spec_repo.git/objects/d0/7b0f9a8c89f1d9e74dc4fce6421dec5ef8a659 +0 -0
- data/spec/fixtures/spec_repo.git/objects/d6/c93164c249c8000205dd4ec5cbca1b516d487f +0 -0
- data/spec/fixtures/spec_repo.git/objects/d7/1aab4f9b04b45ce09bcaa636a9be6231474759 +0 -0
- data/spec/fixtures/spec_repo.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
- data/spec/fixtures/spec_repo.git/objects/e7/b4ad382349ff96dd8199000580b9b1e2042eb0 +0 -0
- data/spec/fixtures/spec_repo.git/objects/f1/425cef211cc08caa31e7b545ffb232acb098c3 +0 -0
- data/spec/fixtures/spec_repo.git/objects/f6/0079018b664e4e79329a7ef9559c8d9e0378d1 +0 -0
- data/spec/fixtures/spec_repo.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 +0 -0
- data/spec/fixtures/spec_repo.git/objects/fd/093bff70906175335656e6ce6ae05783708765 +0 -0
- data/spec/fixtures/spec_repo.git/objects/fd/4959ce7510db09d4d8217fa2d1780413e05a09 +0 -0
- data/spec/fixtures/spec_repo.git/objects/pack/pack-a81e489679b7d3418f9ab594bda8ceb37dd4c695.idx +0 -0
- data/spec/fixtures/spec_repo.git/objects/pack/pack-a81e489679b7d3418f9ab594bda8ceb37dd4c695.pack +0 -0
- data/spec/fixtures/spec_repo.git/objects/pack/pack-d7c6adf9f61318f041845b01440d09aa7a91e1b5.idx +0 -0
- data/spec/fixtures/spec_repo.git/objects/pack/pack-d7c6adf9f61318f041845b01440d09aa7a91e1b5.pack +0 -0
- data/spec/fixtures/spec_repo.git/objects/pack/pack-d85f5d483273108c9d8dd0e4728ccf0b2982423a.idx +0 -0
- data/spec/fixtures/spec_repo.git/objects/pack/pack-d85f5d483273108c9d8dd0e4728ccf0b2982423a.pack +0 -0
- data/spec/fixtures/spec_repo.git/packed-refs +3 -0
- data/spec/fixtures/spec_repo.git/refs/heads/br2 +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/cannot-fetch +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/chomped +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/haacked +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/master +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/not-good +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/packed-test +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/subtrees +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/test +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/track-local +1 -0
- data/spec/fixtures/spec_repo.git/refs/heads/trailing +1 -0
- data/spec/fixtures/spec_repo.git/refs/notes/fanout +1 -0
- data/spec/fixtures/spec_repo.git/refs/remotes/test/master +1 -0
- data/spec/fixtures/spec_repo.git/refs/tags/master-r1 +1 -0
- data/spec/fixtures/spec_repo.git/refs/tags/master-r2 +1 -0
- data/spec/fixtures/spec_repo.git/refs/tags/test-r1 +1 -0
- data/spec/fixtures/spec_repo.git/refs/tags/test-r2 +1 -0
- data/spec/git_spec.rb +23 -0
- data/spec/group_config_spec.rb +64 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/support/test_env.rb +17 -0
- metadata +197 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'rack/auth/basic'
|
|
2
|
+
require 'rack/auth/abstract/handler'
|
|
3
|
+
require 'rack/auth/abstract/request'
|
|
4
|
+
|
|
5
|
+
module Blaggard
|
|
6
|
+
class Auth < Rack::Auth::Basic
|
|
7
|
+
def call(env)
|
|
8
|
+
@env = env
|
|
9
|
+
@request = Rack::Request.new(env)
|
|
10
|
+
@auth = Request.new(env)
|
|
11
|
+
|
|
12
|
+
if not @auth.provided?
|
|
13
|
+
unauthorized
|
|
14
|
+
elsif not @auth.basic?
|
|
15
|
+
bad_request
|
|
16
|
+
else
|
|
17
|
+
result = if (access = valid? and access == true)
|
|
18
|
+
@env['REMOTE_USER'] = @auth.username
|
|
19
|
+
@app.call(env)
|
|
20
|
+
else
|
|
21
|
+
if access == '404'
|
|
22
|
+
render_not_found
|
|
23
|
+
elsif access == '403'
|
|
24
|
+
render_no_access
|
|
25
|
+
else
|
|
26
|
+
unauthorized
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
end# method call
|
|
32
|
+
|
|
33
|
+
def valid?
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
end# class Auth
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'rack/builder'
|
|
2
|
+
require 'blaggard/auth'
|
|
3
|
+
require 'blaggard/server'
|
|
4
|
+
|
|
5
|
+
module Blaggard
|
|
6
|
+
module Bundle
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
def new(config)
|
|
10
|
+
Rack::Builder.new do
|
|
11
|
+
use Blaggard::Auth do |username, password|
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
run Blaggard::Server.new(config)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/blaggard/git.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module Blaggard
|
|
2
|
+
|
|
3
|
+
class Git
|
|
4
|
+
attr_accessor :git_path, :repo
|
|
5
|
+
|
|
6
|
+
def initialize(repo, path = nil)
|
|
7
|
+
@repo = repo
|
|
8
|
+
@git_path = path ? path : 'git'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute(cmd)
|
|
12
|
+
cmd = command(cmd)
|
|
13
|
+
if block_given?
|
|
14
|
+
IO.popen(popen_env, cmd, File::RDWR, popen_options) do |pipe|
|
|
15
|
+
yield(pipe)
|
|
16
|
+
end
|
|
17
|
+
else
|
|
18
|
+
capture(cmd).chomp
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def time_ordered_refs
|
|
23
|
+
opts = %W(--sort=-committerdate refs/heads/ --format=%(refname))
|
|
24
|
+
return execute(['for-each-ref', opts]).split("\n")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tags_on_branch(commitish)
|
|
28
|
+
opts = %W(--simplify-by-decoration --decorate --pretty=oneline)
|
|
29
|
+
return execute(['log', opts, commitish]).scan(/tag: (.*?)(\,|\))/).map{ |match| match.first}
|
|
30
|
+
rescue => e
|
|
31
|
+
[]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def command(cmd)
|
|
35
|
+
[git_path || 'git'] + cmd.flatten
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def capture(command)
|
|
39
|
+
IO.popen(popen_env, command, popen_options).read
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def popen_options
|
|
43
|
+
{chdir: repo, unsetenv_others: true}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def popen_env
|
|
47
|
+
{'PATH' => ENV['PATH'], 'GL_ID' => ENV['GL_ID']}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get_config_setting(service_name)
|
|
51
|
+
service_name = service_name.gsub('-', '')
|
|
52
|
+
setting = get_git_config("http.#{service_name}")
|
|
53
|
+
if service_name == 'uploadpack'
|
|
54
|
+
return setting != 'false'
|
|
55
|
+
else
|
|
56
|
+
return setting == 'true'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def get_git_config(config_name)
|
|
61
|
+
execute(%W(config #{config_name}))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def valid_repo?
|
|
65
|
+
return false unless File.exists?(repo) &&
|
|
66
|
+
File.realpath(repo) == repo
|
|
67
|
+
match = execute(%W(rev-parse --git-dir)).match(/\.$|\.git$/)
|
|
68
|
+
|
|
69
|
+
if match.to_s == '.git'
|
|
70
|
+
# Since the parent could be a git repo, we want to make sure the actual repo contains a git dir.
|
|
71
|
+
return false unless Dir.entries(repo).include?('.git')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
!!match
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def update_server_info
|
|
78
|
+
# TODO: Update this for use with ACL
|
|
79
|
+
execute(%W(update-server-info))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module Blaggard
|
|
2
|
+
class GroupConfigError < StandardError; end
|
|
3
|
+
class GroupConfig
|
|
4
|
+
# groups is an object like this:
|
|
5
|
+
# {
|
|
6
|
+
# # group: { 'read': [refs]
|
|
7
|
+
# # 'write': [refs]}
|
|
8
|
+
# "admin_users" => {
|
|
9
|
+
# 'read' => ['refs/heads/*'],
|
|
10
|
+
# 'write' => ['refs/heads/haacked', 'refs/heads/master']
|
|
11
|
+
# },
|
|
12
|
+
# "normal_users" => {
|
|
13
|
+
# 'read' => ['refs/heads/*'],
|
|
14
|
+
# 'write' => []
|
|
15
|
+
# }
|
|
16
|
+
# }
|
|
17
|
+
attr_accessor :groups, :git
|
|
18
|
+
def initialize( repo_path )
|
|
19
|
+
@repo_path = repo_path
|
|
20
|
+
@git = Blaggard::Git.new(repo_path)
|
|
21
|
+
@config_path = File.join(@repo_path, 'refs/meta')
|
|
22
|
+
unless File.directory?(@config_path)
|
|
23
|
+
FileUtils.mkdir_p(File.join(@repo_path, 'refs/meta'))
|
|
24
|
+
end
|
|
25
|
+
@config_file = File.join(@config_path, 'config')
|
|
26
|
+
read_from_git
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def write_to_git
|
|
30
|
+
tmp_file = File.absolute_path File.join(@config_path, 'tmp')
|
|
31
|
+
if File.exist? tmp_file
|
|
32
|
+
# tmp acts like a lock file to prevent multiple writes at the same time.
|
|
33
|
+
raise Blaggard::GroupConfigError, "Unable to write to meta config, someone else is currently using it. Try again in a few minutes."
|
|
34
|
+
end
|
|
35
|
+
File.open( tmp_file , 'w' ) do |f|
|
|
36
|
+
f.write(YAML.dump @groups)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
ref = git.execute(['hash-object', '-w', tmp_file])
|
|
40
|
+
File.open( @config_file , 'w') do |f|
|
|
41
|
+
f.write(ref)
|
|
42
|
+
end
|
|
43
|
+
FileUtils.rm(tmp_file)
|
|
44
|
+
return true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read_from_git
|
|
48
|
+
ref = File.open(@config_file, &:readline).chomp
|
|
49
|
+
yml = git.execute(['cat-file', '-p', ref])
|
|
50
|
+
@groups = YAML.load yml
|
|
51
|
+
return @groups
|
|
52
|
+
rescue
|
|
53
|
+
@groups = {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_branch(group, priv, branch)
|
|
57
|
+
@groups = read_from_git
|
|
58
|
+
@groups[group] = {:read =>[], :write => []} unless @groups[group]
|
|
59
|
+
validate_privilege priv
|
|
60
|
+
unless @groups[group][priv].include? branch
|
|
61
|
+
if valid_branch_name? branch
|
|
62
|
+
@groups[group][priv] << branch
|
|
63
|
+
write_to_git
|
|
64
|
+
else
|
|
65
|
+
raise Blaggard::GroupConfigError, "Branch name #{branch} invalid. Must be of format 'refs/heads/<branch_name>"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def delete_branch(group, priv, branch)
|
|
71
|
+
@groups = read_from_git
|
|
72
|
+
validate_group group
|
|
73
|
+
validate_privilege priv
|
|
74
|
+
if @groups[group][priv].delete(branch)
|
|
75
|
+
write_to_git
|
|
76
|
+
else
|
|
77
|
+
return false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def delete_group(group)
|
|
82
|
+
@groups = read_from_git
|
|
83
|
+
validate_group group
|
|
84
|
+
if @groups.delete(group)
|
|
85
|
+
write_to_git
|
|
86
|
+
else
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def can_access_branch?(group, priv, branch)
|
|
92
|
+
validate_group group rescue (return false)
|
|
93
|
+
validate_privilege priv
|
|
94
|
+
@groups[group][priv].include? branch
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def branches(user_groups, priv)
|
|
98
|
+
validate_privilege priv
|
|
99
|
+
user_groups.map{ |group|
|
|
100
|
+
validate_group group rescue next
|
|
101
|
+
@groups[group][priv]
|
|
102
|
+
}.uniq.flatten.compact
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def valid_branch_name?(branch)
|
|
106
|
+
valid_branches = Dir[File.join(@repo_path, 'refs/heads/*')].map{|b| b.split('/')[-3..-1].join('/')}
|
|
107
|
+
valid_branches << 'refs/heads/*'
|
|
108
|
+
return valid_branches.include?(branch)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Validation Methods
|
|
112
|
+
|
|
113
|
+
def validate_group(group)
|
|
114
|
+
unless @groups[group]
|
|
115
|
+
raise Blaggard::GroupConfigError, "Group #{group} does not exist. Use add_branch to create it."
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def validate_privilege(priv)
|
|
120
|
+
raise Blaggard::GroupConfigError, "Privilege must be either :read or :write" unless [:read, :write].include?(priv)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
module Blaggard
|
|
4
|
+
class GroupFinder
|
|
5
|
+
def initialize(config)
|
|
6
|
+
# Make your base url and resource something like:
|
|
7
|
+
#
|
|
8
|
+
# https://example.com/api/v1/users_groups/user1
|
|
9
|
+
#
|
|
10
|
+
# Ideally this should bring down a list of strings that
|
|
11
|
+
# will correspond to the group keys in the repo config.
|
|
12
|
+
# These will be the groups that user is a part of. The
|
|
13
|
+
# User is identified by their username over http auth
|
|
14
|
+
# ie. the REMOTE_USER header.
|
|
15
|
+
@url = "#{config[:base_url]}/#{config[:group_resource]}/:id"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def find(identifier)
|
|
19
|
+
uri = URI(@url.gsub(':id', identifier))
|
|
20
|
+
res = Net::HTTP.get_response(uri)
|
|
21
|
+
if res.code == "200"
|
|
22
|
+
return JSON.load(res.body)
|
|
23
|
+
else
|
|
24
|
+
[]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
module Blaggard
|
|
2
|
+
class Server
|
|
3
|
+
attr_reader :git, :config
|
|
4
|
+
SERVICES = [
|
|
5
|
+
["POST", 'service_rpc', "(.*?)/git-upload-pack$", 'upload-pack'],
|
|
6
|
+
["POST", 'service_rpc', "(.*?)/git-receive-pack$", 'receive-pack'],
|
|
7
|
+
|
|
8
|
+
["GET", 'get_info_refs', "(.*?)/info/refs$"],
|
|
9
|
+
["GET", 'get_text_file', "(.*?)/HEAD$"],
|
|
10
|
+
["GET", 'get_text_file', "(.*?)/objects/info/alternates$"],
|
|
11
|
+
["GET", 'get_text_file', "(.*?)/objects/info/http-alternates$"],
|
|
12
|
+
["GET", 'get_info_packs', "(.*?)/objects/info/packs$"],
|
|
13
|
+
["GET", 'get_text_file', "(.*?)/objects/info/[^/]*$"],
|
|
14
|
+
["GET", 'get_loose_object', "(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"],
|
|
15
|
+
["GET", 'get_pack_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"],
|
|
16
|
+
["GET", 'get_idx_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"],
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def initialize(conf = false)
|
|
20
|
+
if conf.instance_of? Hash
|
|
21
|
+
@config = conf
|
|
22
|
+
elsif (File.exist?(conf) rescue false)
|
|
23
|
+
@config = YAML.load( File.read conf )
|
|
24
|
+
else
|
|
25
|
+
@config = {}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_config_setting(key, value)
|
|
30
|
+
@config[key] = value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(env)
|
|
34
|
+
dup._call(env)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def _call(env)
|
|
38
|
+
@env = env
|
|
39
|
+
# puts env
|
|
40
|
+
@req = Rack::Request.new(env)
|
|
41
|
+
|
|
42
|
+
cmd, path, @reqfile, @rpc = match_routing
|
|
43
|
+
return render_method_not_allowed if cmd == 'not_allowed'
|
|
44
|
+
return render_not_found if !cmd
|
|
45
|
+
|
|
46
|
+
@git = get_git(path)
|
|
47
|
+
return render_not_found unless git.valid_repo?
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
identifier = env['REMOTE_USER']
|
|
51
|
+
@groups = Blaggard::GroupFinder.new(@config).find(identifier) if @config[:use_acl]
|
|
52
|
+
|
|
53
|
+
self.method(cmd).call()
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# ---------------------------------
|
|
57
|
+
# actual command handling functions
|
|
58
|
+
# ---------------------------------
|
|
59
|
+
|
|
60
|
+
# Uses chunked (streaming) transfer, otherwise response
|
|
61
|
+
# blocks to calculate Content-Length header
|
|
62
|
+
# http://en.wikipedia.org/wiki/Chunked_transfer_encoding
|
|
63
|
+
|
|
64
|
+
CRLF = "\r\n"
|
|
65
|
+
|
|
66
|
+
def service_rpc
|
|
67
|
+
return render_no_access unless has_access?(@rpc, true)
|
|
68
|
+
|
|
69
|
+
input = read_body
|
|
70
|
+
|
|
71
|
+
@res = Rack::Response.new
|
|
72
|
+
@res.status = 200
|
|
73
|
+
@res["Content-Type"] = "application/x-git-%s-result" % @rpc
|
|
74
|
+
@res["Transfer-Encoding"] = "chunked"
|
|
75
|
+
@res["Cache-Control"] = "no-cache"
|
|
76
|
+
|
|
77
|
+
@res.finish do
|
|
78
|
+
git.execute([@rpc, '--stateless-rpc', git.repo]) do |pipe|
|
|
79
|
+
pipe.write(input)
|
|
80
|
+
pipe.close_write
|
|
81
|
+
|
|
82
|
+
while !pipe.eof?
|
|
83
|
+
block = pipe.read(8192) # 8KB at a time
|
|
84
|
+
@res.write encode_chunk(block) # stream it to the client
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@res.write terminating_chunk
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def encode_chunk(chunk)
|
|
93
|
+
size_in_hex = chunk.size.to_s(16)
|
|
94
|
+
[ size_in_hex, CRLF, chunk, CRLF ].join
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def terminating_chunk
|
|
98
|
+
[ 0, CRLF, CRLF ].join
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get_info_refs
|
|
102
|
+
service_name = get_service_type
|
|
103
|
+
return render_no_access unless has_access?(service_name)
|
|
104
|
+
|
|
105
|
+
refs = begin
|
|
106
|
+
if @config[:use_acl]
|
|
107
|
+
Blaggard::Advertisement.new(git.repo, @groups, service_name).advertise
|
|
108
|
+
else
|
|
109
|
+
git.execute(%W(#{service_name} --stateless-rpc --advertise-refs #{git.repo}))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
@res = Rack::Response.new
|
|
114
|
+
@res.status = 200
|
|
115
|
+
@res["Content-Type"] = "application/x-git-%s-advertisement" % service_name
|
|
116
|
+
hdr_nocache
|
|
117
|
+
@res.write(pkt_write("# service=git-#{service_name}\n"))
|
|
118
|
+
@res.write(pkt_flush)
|
|
119
|
+
@res.write(refs)
|
|
120
|
+
@res.finish
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def dumb_info_refs
|
|
124
|
+
git.update_server_info
|
|
125
|
+
send_file(@reqfile, "text/plain; charset=utf-8") do
|
|
126
|
+
hdr_nocache
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def get_info_packs
|
|
131
|
+
# objects/info/packs
|
|
132
|
+
send_file(@reqfile, "text/plain; charset=utf-8") do
|
|
133
|
+
hdr_nocache
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def get_loose_object
|
|
138
|
+
send_file(@reqfile, "application/x-git-loose-object") do
|
|
139
|
+
hdr_cache_forever
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def get_pack_file
|
|
144
|
+
send_file(@reqfile, "application/x-git-packed-objects") do
|
|
145
|
+
hdr_cache_forever
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def get_idx_file
|
|
150
|
+
send_file(@reqfile, "application/x-git-packed-objects-toc") do
|
|
151
|
+
hdr_cache_forever
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def get_text_file
|
|
156
|
+
send_file(@reqfile, "text/plain") do
|
|
157
|
+
hdr_nocache
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# ------------------------
|
|
162
|
+
# logic helping functions
|
|
163
|
+
# ------------------------
|
|
164
|
+
|
|
165
|
+
# some of this borrowed from the Rack::File implementation
|
|
166
|
+
def send_file(reqfile, content_type)
|
|
167
|
+
reqfile = File.join(git.repo, reqfile)
|
|
168
|
+
return render_not_found unless File.exists?(reqfile)
|
|
169
|
+
|
|
170
|
+
return render_not_found unless reqfile == File.realpath(reqfile)
|
|
171
|
+
|
|
172
|
+
# reqfile looks legit: no path traversal, no leading '|'
|
|
173
|
+
|
|
174
|
+
@res = Rack::Response.new
|
|
175
|
+
@res.status = 200
|
|
176
|
+
@res["Content-Type"] = content_type
|
|
177
|
+
@res["Last-Modified"] = File.mtime(reqfile).httpdate
|
|
178
|
+
|
|
179
|
+
yield
|
|
180
|
+
|
|
181
|
+
if size = File.size?(reqfile)
|
|
182
|
+
@res["Content-Length"] = size.to_s
|
|
183
|
+
@res.finish do
|
|
184
|
+
File.open(reqfile, "rb") do |file|
|
|
185
|
+
while part = file.read(8192)
|
|
186
|
+
@res.write part
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
else
|
|
191
|
+
body = [File.read(reqfile)]
|
|
192
|
+
size = Rack::Utils.bytesize(body.first)
|
|
193
|
+
@res["Content-Length"] = size
|
|
194
|
+
@res.write body
|
|
195
|
+
@res.finish
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def get_git(path)
|
|
200
|
+
root = @config[:project_root] || Dir.pwd
|
|
201
|
+
path = File.join(root, path)
|
|
202
|
+
Blaggard::Git.new( path, @config[:git_path] )
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def get_service_type
|
|
206
|
+
service_type = @req.params['service']
|
|
207
|
+
return false if !service_type
|
|
208
|
+
return false if service_type[0, 4] != 'git-'
|
|
209
|
+
service_type.gsub('git-', '')
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def match_routing
|
|
213
|
+
SERVICES.each do |method, handler, match, rpc|
|
|
214
|
+
if m = Regexp.new(match).match(@req.path_info)
|
|
215
|
+
return ['not_allowed'] if method != @req.request_method
|
|
216
|
+
cmd = handler
|
|
217
|
+
path = m[1]
|
|
218
|
+
file = @req.path_info.sub(path + '/', '')
|
|
219
|
+
return [cmd, path, file, rpc]
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
return nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def smart_http?(rpc = @rpc)
|
|
226
|
+
@req.content_type == "application/x-git-#{rpc}-request"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def has_access?(rpc, check_content_type = false)
|
|
230
|
+
if check_content_type
|
|
231
|
+
return false unless smart_http?(rpc)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
return false unless ['upload-pack', 'receive-pack'].include?(rpc)
|
|
235
|
+
|
|
236
|
+
if rpc == 'receive-pack'
|
|
237
|
+
return @config[:receive_pack] if @config.include?(:receive_pack)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if rpc == 'upload-pack'
|
|
241
|
+
return @config[:upload_pack] if @config.include?(:upload_pack)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
git.config_setting(rpc)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def read_body
|
|
250
|
+
if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
|
|
251
|
+
input = Zlib::GzipReader.new(@req.body).read
|
|
252
|
+
else
|
|
253
|
+
input = @req.body.read
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# --------------------------------------
|
|
258
|
+
# HTTP error response handling functions
|
|
259
|
+
# --------------------------------------
|
|
260
|
+
|
|
261
|
+
PLAIN_TYPE = {"Content-Type" => "text/plain"}
|
|
262
|
+
|
|
263
|
+
def render_method_not_allowed
|
|
264
|
+
if @env['SERVER_PROTOCOL'] == "HTTP/1.1"
|
|
265
|
+
[405, PLAIN_TYPE, ["Method Not Allowed"]]
|
|
266
|
+
else
|
|
267
|
+
[400, PLAIN_TYPE, ["Bad Request"]]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def render_not_found
|
|
272
|
+
[404, PLAIN_TYPE, ["Not Found"]]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def render_no_access
|
|
276
|
+
[403, PLAIN_TYPE, ["Forbidden"]]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ------------------------------
|
|
281
|
+
# packet-line handling functions
|
|
282
|
+
# ------------------------------
|
|
283
|
+
|
|
284
|
+
def pkt_flush
|
|
285
|
+
'0000'
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def pkt_write(str)
|
|
289
|
+
(str.size + 4).to_s(base=16).rjust(4, '0') + str
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ------------------------
|
|
294
|
+
# header writing functions
|
|
295
|
+
# ------------------------
|
|
296
|
+
|
|
297
|
+
def hdr_nocache
|
|
298
|
+
@res["Expires"] = "Fri, 01 Jan 1980 00:00:00 GMT"
|
|
299
|
+
@res["Pragma"] = "no-cache"
|
|
300
|
+
@res["Cache-Control"] = "no-cache, max-age=0, must-revalidate"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def hdr_cache_forever
|
|
304
|
+
now = Time.now().to_i
|
|
305
|
+
@res["Date"] = now.to_s
|
|
306
|
+
@res["Expires"] = (now + 31536000).to_s;
|
|
307
|
+
@res["Cache-Control"] = "public, max-age=31536000";
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
end
|
|
311
|
+
end
|