blaggard 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|