blaggard 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +15 -0
  5. data/Gemfile.lock +66 -0
  6. data/README.md +84 -0
  7. data/Rakefile +20 -0
  8. data/bin/blaggard +55 -0
  9. data/blaggard.gemspec +18 -0
  10. data/config.yml.example +9 -0
  11. data/install.txt +60 -0
  12. data/lib/blaggard.rb +33 -0
  13. data/lib/blaggard/advertisement.rb +92 -0
  14. data/lib/blaggard/auth.rb +37 -0
  15. data/lib/blaggard/bundle.rb +20 -0
  16. data/lib/blaggard/console.rb +3 -0
  17. data/lib/blaggard/git.rb +82 -0
  18. data/lib/blaggard/group_config.rb +123 -0
  19. data/lib/blaggard/group_finder.rb +28 -0
  20. data/lib/blaggard/server.rb +311 -0
  21. data/script/console +7 -0
  22. data/spec/advertisement_spec.rb +81 -0
  23. data/spec/fixtures/spec_repo.git/FETCH_HEAD +2 -0
  24. data/spec/fixtures/spec_repo.git/HEAD +1 -0
  25. data/spec/fixtures/spec_repo.git/HEAD_TRACKER +1 -0
  26. data/spec/fixtures/spec_repo.git/config +53 -0
  27. data/spec/fixtures/spec_repo.git/index +0 -0
  28. data/spec/fixtures/spec_repo.git/logs/HEAD +7 -0
  29. data/spec/fixtures/spec_repo.git/logs/refs/heads/br2 +2 -0
  30. data/spec/fixtures/spec_repo.git/logs/refs/heads/master +2 -0
  31. data/spec/fixtures/spec_repo.git/logs/refs/heads/not-good +1 -0
  32. data/spec/fixtures/spec_repo.git/logs/refs/remotes/origin/HEAD +1 -0
  33. data/spec/fixtures/spec_repo.git/logs/refs/remotes/test/master +2 -0
  34. data/spec/fixtures/spec_repo.git/objects/08/b041783f40edfe12bb406c9c9a8a040177c125 +0 -0
  35. data/spec/fixtures/spec_repo.git/objects/13/85f264afb75a56a5bec74243be9b367ba4ca08 +0 -0
  36. data/spec/fixtures/spec_repo.git/objects/18/1037049a54a1eb5fab404658a3a250b44335d7 +0 -0
  37. data/spec/fixtures/spec_repo.git/objects/18/10dff58d8a660512d4832e740f692884338ccd +0 -0
  38. data/spec/fixtures/spec_repo.git/objects/1a/443023183e3f2bfbef8ac923cd81c1018a18fd +0 -0
  39. data/spec/fixtures/spec_repo.git/objects/1b/8cbad43e867676df601306689fe7c3def5e689 +0 -0
  40. data/spec/fixtures/spec_repo.git/objects/1f/67fc4386b2d171e0d21be1c447e12660561f9b +0 -0
  41. data/spec/fixtures/spec_repo.git/objects/25/8f0e2a959a364e40ed6603d5d44fbb24765b10 +0 -0
  42. data/spec/fixtures/spec_repo.git/objects/27/0b8ea76056d5cad83af921837702d3e3c2924d +0 -0
  43. data/spec/fixtures/spec_repo.git/objects/2d/59075e0681f540482d4f6223a68e0fef790bc7 +0 -0
  44. data/spec/fixtures/spec_repo.git/objects/32/59a6bd5b57fb9c1281bb7ed3167b50f224cb54 +0 -0
  45. data/spec/fixtures/spec_repo.git/objects/36/97d64be941a53d4ae8f6a271e4e3fa56b022cc +0 -0
  46. data/spec/fixtures/spec_repo.git/objects/45/b983be36b73c0788dc9cbcb76cbb80fc7bb057 +0 -0
  47. data/spec/fixtures/spec_repo.git/objects/4a/202b346bb0fb0db7eff3cffeb3c70babbd2045 +2 -0
  48. data/spec/fixtures/spec_repo.git/objects/4a/23e2e65ad4e31c4c9db7dc746650bfad082679 +0 -0
  49. data/spec/fixtures/spec_repo.git/objects/4b/22b35d44b5a4f589edf3dc89196399771796ea +0 -0
  50. data/spec/fixtures/spec_repo.git/objects/52/1d87c1ec3aef9824daf6d96cc0ae3710766d91 +0 -0
  51. data/spec/fixtures/spec_repo.git/objects/5b/5b025afb0b4c913b4c338a42934a3863bf3644 +2 -0
  52. data/spec/fixtures/spec_repo.git/objects/75/057dd4114e74cca1d750d0aee1647c903cb60a +0 -0
  53. data/spec/fixtures/spec_repo.git/objects/76/3d71aadf09a7951596c9746c024e7eece7c7af +1 -0
  54. data/spec/fixtures/spec_repo.git/objects/7b/4384978d2493e851f9cca7858815fac9b10980 +0 -0
  55. data/spec/fixtures/spec_repo.git/objects/81/4889a078c031f61ed08ab5fa863aea9314344d +0 -0
  56. data/spec/fixtures/spec_repo.git/objects/84/96071c1b46c854b31185ea97743be6a8774479 +0 -0
  57. data/spec/fixtures/spec_repo.git/objects/84/9a5e34a26815e821f865b8479f5815a47af0fe +2 -0
  58. data/spec/fixtures/spec_repo.git/objects/94/4c0f6e4dfa41595e6eb3ceecdb14f50fe18162 +1 -0
  59. data/spec/fixtures/spec_repo.git/objects/9a/03079b8a8ee85a0bee58bf9be3da8b62414ed4 +0 -0
  60. data/spec/fixtures/spec_repo.git/objects/9f/13f7d0a9402c681f91dc590cf7b5470e6a77d2 +2 -0
  61. data/spec/fixtures/spec_repo.git/objects/9f/d738e8f7967c078dceed8190330fc8648ee56a +3 -0
  62. data/spec/fixtures/spec_repo.git/objects/a4/a7dce85cf63874e984719f4fdd239f5145052f +2 -0
  63. data/spec/fixtures/spec_repo.git/objects/a6/5fedf39aefe402d3bb6e24df4d4f5fe4547750 +3 -0
  64. data/spec/fixtures/spec_repo.git/objects/a7/1586c1dfe8a71c6cbf6c129f404c5642ff31bd +0 -0
  65. data/spec/fixtures/spec_repo.git/objects/a8/233120f6ad708f843d861ce2b7228ec4e3dec6 +0 -0
  66. data/spec/fixtures/spec_repo.git/objects/ae/90f12eea699729ed24555e40b9fd669da12a12 +0 -0
  67. data/spec/fixtures/spec_repo.git/objects/b2/5fa35b38051e4ae45d4222e795f9df2e43f1d1 +2 -0
  68. data/spec/fixtures/spec_repo.git/objects/b6/361fc6a97178d8fc8639fdeed71c775ab52593 +0 -0
  69. data/spec/fixtures/spec_repo.git/objects/be/3563ae3f795b2b4353bcce3a527ad0a4f7f644 +3 -0
  70. data/spec/fixtures/spec_repo.git/objects/c4/7800c7266a2be04c571c04d5a6614691ea99bd +3 -0
  71. data/spec/fixtures/spec_repo.git/objects/d0/7b0f9a8c89f1d9e74dc4fce6421dec5ef8a659 +0 -0
  72. data/spec/fixtures/spec_repo.git/objects/d6/c93164c249c8000205dd4ec5cbca1b516d487f +0 -0
  73. data/spec/fixtures/spec_repo.git/objects/d7/1aab4f9b04b45ce09bcaa636a9be6231474759 +0 -0
  74. data/spec/fixtures/spec_repo.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
  75. data/spec/fixtures/spec_repo.git/objects/e7/b4ad382349ff96dd8199000580b9b1e2042eb0 +0 -0
  76. data/spec/fixtures/spec_repo.git/objects/f1/425cef211cc08caa31e7b545ffb232acb098c3 +0 -0
  77. data/spec/fixtures/spec_repo.git/objects/f6/0079018b664e4e79329a7ef9559c8d9e0378d1 +0 -0
  78. data/spec/fixtures/spec_repo.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 +0 -0
  79. data/spec/fixtures/spec_repo.git/objects/fd/093bff70906175335656e6ce6ae05783708765 +0 -0
  80. data/spec/fixtures/spec_repo.git/objects/fd/4959ce7510db09d4d8217fa2d1780413e05a09 +0 -0
  81. data/spec/fixtures/spec_repo.git/objects/pack/pack-a81e489679b7d3418f9ab594bda8ceb37dd4c695.idx +0 -0
  82. data/spec/fixtures/spec_repo.git/objects/pack/pack-a81e489679b7d3418f9ab594bda8ceb37dd4c695.pack +0 -0
  83. data/spec/fixtures/spec_repo.git/objects/pack/pack-d7c6adf9f61318f041845b01440d09aa7a91e1b5.idx +0 -0
  84. data/spec/fixtures/spec_repo.git/objects/pack/pack-d7c6adf9f61318f041845b01440d09aa7a91e1b5.pack +0 -0
  85. data/spec/fixtures/spec_repo.git/objects/pack/pack-d85f5d483273108c9d8dd0e4728ccf0b2982423a.idx +0 -0
  86. data/spec/fixtures/spec_repo.git/objects/pack/pack-d85f5d483273108c9d8dd0e4728ccf0b2982423a.pack +0 -0
  87. data/spec/fixtures/spec_repo.git/packed-refs +3 -0
  88. data/spec/fixtures/spec_repo.git/refs/heads/br2 +1 -0
  89. data/spec/fixtures/spec_repo.git/refs/heads/cannot-fetch +1 -0
  90. data/spec/fixtures/spec_repo.git/refs/heads/chomped +1 -0
  91. data/spec/fixtures/spec_repo.git/refs/heads/haacked +1 -0
  92. data/spec/fixtures/spec_repo.git/refs/heads/master +1 -0
  93. data/spec/fixtures/spec_repo.git/refs/heads/not-good +1 -0
  94. data/spec/fixtures/spec_repo.git/refs/heads/packed-test +1 -0
  95. data/spec/fixtures/spec_repo.git/refs/heads/subtrees +1 -0
  96. data/spec/fixtures/spec_repo.git/refs/heads/test +1 -0
  97. data/spec/fixtures/spec_repo.git/refs/heads/track-local +1 -0
  98. data/spec/fixtures/spec_repo.git/refs/heads/trailing +1 -0
  99. data/spec/fixtures/spec_repo.git/refs/notes/fanout +1 -0
  100. data/spec/fixtures/spec_repo.git/refs/remotes/test/master +1 -0
  101. data/spec/fixtures/spec_repo.git/refs/tags/master-r1 +1 -0
  102. data/spec/fixtures/spec_repo.git/refs/tags/master-r2 +1 -0
  103. data/spec/fixtures/spec_repo.git/refs/tags/test-r1 +1 -0
  104. data/spec/fixtures/spec_repo.git/refs/tags/test-r2 +1 -0
  105. data/spec/git_spec.rb +23 -0
  106. data/spec/group_config_spec.rb +64 -0
  107. data/spec/spec_helper.rb +42 -0
  108. data/spec/support/test_env.rb +17 -0
  109. 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
@@ -0,0 +1,3 @@
1
+ Pry.config.prompt = lambda do |context, nesting, pry|
2
+ "[blaggard] #{context}> "
3
+ end
@@ -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