gitlab-grack 2.0.0.rc1 → 2.0.0.rc2

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