gitlab-grack 2.0.0.rc1 → 2.0.0.rc2

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.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => :test
5
+
6
+ desc "Run the tests."
7
+ task :test do
8
+ Dir.glob("tests/*_test.rb").each do |f|
9
+ system "ruby #{f}"
10
+ end
11
+ end
12
+
13
+ desc "Run test coverage."
14
+ task :rcov do
15
+ system "rcov tests/*_test.rb -i lib/git_http.rb -x rack -x Library -x tests"
16
+ system "open coverage/index.html"
17
+ end
18
+
19
+ namespace :grack do
20
+ desc "Start Grack"
21
+ task :start do
22
+ system "rackup config.ru -p 8080"
23
+ end
24
+ end
25
+
26
+ desc "Start everything."
27
+ multitask :start => [ 'grack:start' ]
@@ -0,0 +1,9 @@
1
+ #! /usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/lib')
3
+ require 'lib/git_http'
4
+ config = {
5
+ :project_root => "/opt",
6
+ :upload_pack => true,
7
+ :receive_pack => false,
8
+ }
9
+ Rack::Handler::FastCGI.run(GitHttp::App.new(config))
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/grack/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Scott Chacon"]
6
+ gem.email = ["schacon@gmail.com"]
7
+ gem.description = %q{Ruby/Rack Git Smart-HTTP Server Handler}
8
+ gem.summary = %q{Ruby/Rack Git Smart-HTTP Server Handler}
9
+ gem.homepage = "https://github.com/gitlabhq/grack"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- tests/*`.split("\n")
14
+ gem.name = "gitlab-grack"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Grack::VERSION
17
+
18
+ gem.add_dependency("rack", "~> 1.5.1")
19
+ gem.add_development_dependency("mocha", "~> 0.11")
20
+ end
@@ -0,0 +1,60 @@
1
+ Installation
2
+ ========================
3
+
4
+ ** This documentation is not finished yet. I haven't tested all of
5
+ these and it's obviously incomplete - these are currently just notes.
6
+
7
+ FastCGI
8
+ ---------------------------------------
9
+ Here is an example config from lighttpd server:
10
+ ----
11
+ # main fastcgi entry
12
+ $HTTP["url"] =~ "^/myapp/.+$" {
13
+ fastcgi.server = ( "/myapp" =>
14
+ ( "localhost" =>
15
+ ( "bin-path" => "/var/www/localhost/cgi-bin/dispatch.fcgi",
16
+ "docroot" => "/var/www/localhost/htdocs/myapp",
17
+ "host" => "127.0.0.1",
18
+ "port" => 1026,
19
+ "check-local" => "disable"
20
+ )
21
+ )
22
+ )
23
+ } # HTTP[url]
24
+ ----
25
+ You can use the examples/dispatch.fcgi file as your dispatcher.
26
+
27
+ (Example Apache setup?)
28
+
29
+ Installing in a Java application server
30
+ ---------------------------------------
31
+ # install Warbler
32
+ $ sudo gem install warbler
33
+ $ cd gitsmart
34
+ $ (edit config.ru)
35
+ $ warble
36
+ $ cp gitsmart.war /path/to/java/autodeploy/dir
37
+
38
+ Unicorn
39
+ ---------------------------------------
40
+ With Unicorn (http://unicorn.bogomips.org/) you can just run 'unicorn'
41
+ in the directory with the config.ru file.
42
+
43
+ Thin
44
+ ---------------------------------------
45
+ thin.yml
46
+ ---
47
+ pid: /home/deploy/myapp/server/thin.pid
48
+ log: /home/deploy/myapp/logs/thin.log
49
+ timeout: 30
50
+ port: 7654
51
+ max_conns: 1024
52
+ chdir: /home/deploy/myapp/site_files
53
+ rackup: /home/deploy/myapp/server/config.ru
54
+ max_persistent_conns: 512
55
+ environment: production
56
+ address: 127.0.0.1
57
+ servers: 1
58
+ daemonize: true
59
+
60
+
@@ -0,0 +1,5 @@
1
+ require "grack/bundle"
2
+
3
+ module Grack
4
+
5
+ end
@@ -0,0 +1,37 @@
1
+ require 'rack/auth/basic'
2
+ require 'rack/auth/abstract/handler'
3
+ require 'rack/auth/abstract/request'
4
+
5
+ module Grack
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# module Grack
@@ -0,0 +1,20 @@
1
+ require 'rack/builder'
2
+ require 'grack/auth'
3
+ require 'grack/server'
4
+
5
+ module Grack
6
+ module Bundle
7
+ extend self
8
+
9
+ def new(config)
10
+ Rack::Builder.new do
11
+ use Grack::Auth do |username, password|
12
+ false
13
+ end
14
+
15
+ run Grack::Server.new(config)
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,346 @@
1
+ require 'zlib'
2
+ require 'rack/request'
3
+ require 'rack/response'
4
+ require 'rack/utils'
5
+ require 'time'
6
+
7
+ module Grack
8
+ class Server
9
+
10
+ SERVICES = [
11
+ ["POST", 'service_rpc', "(.*?)/git-upload-pack$", 'upload-pack'],
12
+ ["POST", 'service_rpc', "(.*?)/git-receive-pack$", 'receive-pack'],
13
+
14
+ ["GET", 'get_info_refs', "(.*?)/info/refs$"],
15
+ ["GET", 'get_text_file', "(.*?)/HEAD$"],
16
+ ["GET", 'get_text_file', "(.*?)/objects/info/alternates$"],
17
+ ["GET", 'get_text_file', "(.*?)/objects/info/http-alternates$"],
18
+ ["GET", 'get_info_packs', "(.*?)/objects/info/packs$"],
19
+ ["GET", 'get_text_file', "(.*?)/objects/info/[^/]*$"],
20
+ ["GET", 'get_loose_object', "(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"],
21
+ ["GET", 'get_pack_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"],
22
+ ["GET", 'get_idx_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"],
23
+ ]
24
+
25
+ def initialize(config = false)
26
+ set_config(config)
27
+ end
28
+
29
+ def set_config(config)
30
+ @config = config || {}
31
+ end
32
+
33
+ def set_config_setting(key, value)
34
+ @config[key] = value
35
+ end
36
+
37
+ def call(env)
38
+ dup._call(env)
39
+ end
40
+
41
+ def _call(env)
42
+ @env = env
43
+ @req = Rack::Request.new(env)
44
+
45
+ cmd, path, @reqfile, @rpc = match_routing
46
+
47
+ return render_method_not_allowed if cmd == 'not_allowed'
48
+ return render_not_found if !cmd
49
+
50
+ @dir = get_git_dir(path)
51
+ return render_not_found if !@dir
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 if !has_access(@rpc, true)
68
+ input = read_body
69
+
70
+ @res = Rack::Response.new
71
+ @res.status = 200
72
+ @res["Content-Type"] = "application/x-git-%s-result" % @rpc
73
+ @res["Transfer-Encoding"] = "chunked"
74
+ @res["Cache-Control"] = "no-cache"
75
+
76
+ @res.finish do
77
+ command = git_command(%W(#{@rpc} --stateless-rpc #{@dir}))
78
+ IO.popen(popen_env, command, File::RDWR, popen_options) do |pipe|
79
+ pipe.write(input)
80
+ pipe.close_write
81
+ while !pipe.eof?
82
+ block = pipe.read(8192) # 8KB at a time
83
+ @res.write encode_chunk(block) # stream it to the client
84
+ end
85
+ @res.write terminating_chunk
86
+ end
87
+ end
88
+ end
89
+
90
+ def encode_chunk(chunk)
91
+ size_in_hex = chunk.size.to_s(16)
92
+ [ size_in_hex, CRLF, chunk, CRLF ].join
93
+ end
94
+
95
+ def terminating_chunk
96
+ [ 0, CRLF, CRLF ].join
97
+ end
98
+
99
+ def get_info_refs
100
+ service_name = get_service_type
101
+
102
+ if has_access(service_name)
103
+ cmd = git_command(%W(#{service_name} --stateless-rpc --advertise-refs #{@dir}))
104
+ refs = capture(cmd)
105
+
106
+ @res = Rack::Response.new
107
+ @res.status = 200
108
+ @res["Content-Type"] = "application/x-git-%s-advertisement" % service_name
109
+ hdr_nocache
110
+ @res.write(pkt_write("# service=git-#{service_name}\n"))
111
+ @res.write(pkt_flush)
112
+ @res.write(refs)
113
+ @res.finish
114
+ else
115
+ dumb_info_refs
116
+ end
117
+ end
118
+
119
+ def dumb_info_refs
120
+ update_server_info
121
+ send_file(@reqfile, "text/plain; charset=utf-8") do
122
+ hdr_nocache
123
+ end
124
+ end
125
+
126
+ def get_info_packs
127
+ # objects/info/packs
128
+ send_file(@reqfile, "text/plain; charset=utf-8") do
129
+ hdr_nocache
130
+ end
131
+ end
132
+
133
+ def get_loose_object
134
+ send_file(@reqfile, "application/x-git-loose-object") do
135
+ hdr_cache_forever
136
+ end
137
+ end
138
+
139
+ def get_pack_file
140
+ send_file(@reqfile, "application/x-git-packed-objects") do
141
+ hdr_cache_forever
142
+ end
143
+ end
144
+
145
+ def get_idx_file
146
+ send_file(@reqfile, "application/x-git-packed-objects-toc") do
147
+ hdr_cache_forever
148
+ end
149
+ end
150
+
151
+ def get_text_file
152
+ send_file(@reqfile, "text/plain") do
153
+ hdr_nocache
154
+ end
155
+ end
156
+
157
+ # ------------------------
158
+ # logic helping functions
159
+ # ------------------------
160
+
161
+ F = ::File
162
+
163
+ # some of this borrowed from the Rack::File implementation
164
+ def send_file(reqfile, content_type)
165
+ reqfile = File.join(@dir, reqfile)
166
+ return render_not_found if !F.exists?(reqfile)
167
+
168
+ if reqfile == File.realpath(reqfile)
169
+ # reqfile looks legit: no path traversal, no leading '|'
170
+ else
171
+ # reqfile does not look trustworthy; abort
172
+ return render_not_found
173
+ end
174
+
175
+ @res = Rack::Response.new
176
+ @res.status = 200
177
+ @res["Content-Type"] = content_type
178
+ @res["Last-Modified"] = F.mtime(reqfile).httpdate
179
+
180
+ yield
181
+
182
+ if size = F.size?(reqfile)
183
+ @res["Content-Length"] = size.to_s
184
+ @res.finish do
185
+ F.open(reqfile, "rb") do |file|
186
+ while part = file.read(8192)
187
+ @res.write part
188
+ end
189
+ end
190
+ end
191
+ else
192
+ body = [F.read(reqfile)]
193
+ size = Rack::Utils.bytesize(body.first)
194
+ @res["Content-Length"] = size
195
+ @res.write body
196
+ @res.finish
197
+ end
198
+ end
199
+
200
+ def get_git_dir(path)
201
+ root = @config[:project_root] || Dir.pwd
202
+ path = File.join(root, path)
203
+ if !File.exists?(path)
204
+ false
205
+ elsif File.realpath(path) != path # looks like path traversal
206
+ false
207
+ else
208
+ path # TODO: check is a valid git directory
209
+ end
210
+ end
211
+
212
+ def get_service_type
213
+ service_type = @req.params['service']
214
+ return false if !service_type
215
+ return false if service_type[0, 4] != 'git-'
216
+ service_type.gsub('git-', '')
217
+ end
218
+
219
+ def match_routing
220
+ cmd = nil
221
+ path = nil
222
+ SERVICES.each do |method, handler, match, rpc|
223
+ if m = Regexp.new(match).match(@req.path_info)
224
+ return ['not_allowed'] if method != @req.request_method
225
+ cmd = handler
226
+ path = m[1]
227
+ file = @req.path_info.sub(path + '/', '')
228
+ return [cmd, path, file, rpc]
229
+ end
230
+ end
231
+ return nil
232
+ end
233
+
234
+ def has_access(rpc, check_content_type = false)
235
+ if check_content_type
236
+ return false if @req.content_type != "application/x-git-%s-request" % rpc
237
+ end
238
+ return false if !['upload-pack', 'receive-pack'].include? rpc
239
+ if rpc == 'receive-pack'
240
+ return @config[:receive_pack] if @config.include? :receive_pack
241
+ end
242
+ if rpc == 'upload-pack'
243
+ return @config[:upload_pack] if @config.include? :upload_pack
244
+ end
245
+ return get_config_setting(rpc)
246
+ end
247
+
248
+ def get_config_setting(service_name)
249
+ service_name = service_name.gsub('-', '')
250
+ setting = get_git_config("http.#{service_name}")
251
+ if service_name == 'uploadpack'
252
+ return setting != 'false'
253
+ else
254
+ return setting == 'true'
255
+ end
256
+ end
257
+
258
+ def get_git_config(config_name)
259
+ cmd = git_command(%W(config #{config_name}))
260
+ capture(cmd).chomp
261
+ end
262
+
263
+ def read_body
264
+ if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
265
+ input = Zlib::GzipReader.new(@req.body).read
266
+ else
267
+ input = @req.body.read
268
+ end
269
+ end
270
+
271
+ def update_server_info
272
+ cmd = git_command(%W(update-server-info))
273
+ capture(cmd)
274
+ end
275
+
276
+ def git_command(command)
277
+ [@config[:git_path] || 'git'] + command
278
+ end
279
+
280
+ def capture(command)
281
+ IO.popen(popen_env, command, popen_options) { |p| p.read }
282
+ end
283
+
284
+ def popen_options
285
+ {chdir: @dir, unsetenv_others: true}
286
+ end
287
+
288
+ def popen_env
289
+ {'PATH' => ENV['PATH'], 'GL_ID' => ENV['GL_ID']}
290
+ end
291
+
292
+ # --------------------------------------
293
+ # HTTP error response handling functions
294
+ # --------------------------------------
295
+
296
+ PLAIN_TYPE = {"Content-Type" => "text/plain"}
297
+
298
+ def render_method_not_allowed
299
+ if @env['SERVER_PROTOCOL'] == "HTTP/1.1"
300
+ [405, PLAIN_TYPE, ["Method Not Allowed"]]
301
+ else
302
+ [400, PLAIN_TYPE, ["Bad Request"]]
303
+ end
304
+ end
305
+
306
+ def render_not_found
307
+ [404, PLAIN_TYPE, ["Not Found"]]
308
+ end
309
+
310
+ def render_no_access
311
+ [403, PLAIN_TYPE, ["Forbidden"]]
312
+ end
313
+
314
+
315
+ # ------------------------------
316
+ # packet-line handling functions
317
+ # ------------------------------
318
+
319
+ def pkt_flush
320
+ '0000'
321
+ end
322
+
323
+ def pkt_write(str)
324
+ (str.size + 4).to_s(base=16).rjust(4, '0') + str
325
+ end
326
+
327
+
328
+ # ------------------------
329
+ # header writing functions
330
+ # ------------------------
331
+
332
+ def hdr_nocache
333
+ @res["Expires"] = "Fri, 01 Jan 1980 00:00:00 GMT"
334
+ @res["Pragma"] = "no-cache"
335
+ @res["Cache-Control"] = "no-cache, max-age=0, must-revalidate"
336
+ end
337
+
338
+ def hdr_cache_forever
339
+ now = Time.now().to_i
340
+ @res["Date"] = now.to_s
341
+ @res["Expires"] = (now + 31536000).to_s;
342
+ @res["Cache-Control"] = "public, max-age=31536000";
343
+ end
344
+
345
+ end
346
+ end