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.
- checksums.yaml +4 -4
- data/.gitignore +3 -22
- data/.gitmodules +3 -0
- data/.travis.yml +14 -0
- data/CHANGELOG +2 -0
- data/Gemfile +7 -2
- data/Gemfile.lock +17 -8
- data/README.md +95 -292
- data/Rakefile +27 -0
- data/examples/dispatch.fcgi +9 -0
- data/grack.gemspec +20 -0
- data/install.txt +60 -0
- data/lib/grack.rb +5 -0
- data/lib/grack/auth.rb +37 -0
- data/lib/grack/bundle.rb +20 -0
- data/lib/grack/server.rb +346 -0
- data/lib/grack/version.rb +3 -0
- data/tests/main_test.rb +255 -0
- metadata +18 -22
- data/Makefile +0 -153
- data/Procfile.example +0 -3
- data/Vagrantfile +0 -41
- data/database.yml.example +0 -13
- data/gitlab-openldap/.gitignore +0 -1
- data/gitlab-openldap/Makefile +0 -40
- data/gitlab-openldap/README.md +0 -60
- data/gitlab-openldap/bootstrap.ldif +0 -36
- data/gitlab-openldap/frontend.alt.example.com.ldif +0 -109
- data/gitlab-openldap/frontend.example.com.ldif +0 -109
- data/gitlab-openldap/run-slapd +0 -3
- data/gitlab-openldap/run-slapd-alt +0 -3
- data/redis/redis.conf.example +0 -2
- data/redis/resque.yml.example +0 -2
- data/support/edit-gitlab.yml +0 -11
data/Rakefile
ADDED
@@ -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))
|
data/grack.gemspec
ADDED
@@ -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
|
data/install.txt
ADDED
@@ -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
|
+
|
data/lib/grack/auth.rb
ADDED
@@ -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
|
data/lib/grack/bundle.rb
ADDED
@@ -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
|
data/lib/grack/server.rb
ADDED
@@ -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
|