Class: Grack::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/grack/server.rb

Constant Summary collapse

SERVICES =
[
  ["POST", 'service_rpc',      "(.*?)/git-upload-pack$",  'upload-pack'],
  ["POST", 'service_rpc',      "(.*?)/git-receive-pack$", 'receive-pack'],

  ["GET",  'get_info_refs',    "(.*?)/info/refs$"],
  ["GET",  'get_text_file',    "(.*?)/HEAD$"],
  ["GET",  'get_text_file',    "(.*?)/objects/info/alternates$"],
  ["GET",  'get_text_file',    "(.*?)/objects/info/http-alternates$"],
  ["GET",  'get_info_packs',   "(.*?)/objects/info/packs$"],
  ["GET",  'get_text_file',    "(.*?)/objects/info/[^/]*$"],
  ["GET",  'get_loose_object', "(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"],
  ["GET",  'get_pack_file',    "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"],
  ["GET",  'get_idx_file',     "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"],
]
CRLF =

Uses chunked (streaming) transfer, otherwise response blocks to calculate Content-Length header en.wikipedia.org/wiki/Chunked_transfer_encoding

"\r\n"
F =

logic helping functions


::File
PLAIN_TYPE =

HTTP error response handling functions


{"Content-Type" => "text/plain"}

Instance Method Summary collapse

Constructor Details

#initialize(config = false) ⇒ Server

Returns a new instance of Server.



25
26
27
# File 'lib/grack/server.rb', line 25

def initialize(config = false)
  set_config(config)
end

Instance Method Details

#call(env) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/grack/server.rb', line 37

def call(env)
  @env = env
  @req = Rack::Request.new(env)

  cmd, path, @reqfile, @rpc = match_routing

  return render_method_not_allowed if cmd == 'not_allowed'
  return render_not_found if !cmd

  @dir = get_git_dir(path)
  return render_not_found if !@dir

  Dir.chdir(@dir) do
    self.method(cmd).call()
  end
end

#dumb_info_refsObject



117
118
119
120
121
122
# File 'lib/grack/server.rb', line 117

def dumb_info_refs
  update_server_info
  send_file(@reqfile, "text/plain; charset=utf-8") do
    hdr_nocache
  end
end

#encode_chunk(chunk) ⇒ Object



88
89
90
91
# File 'lib/grack/server.rb', line 88

def encode_chunk(chunk)
  size_in_hex = chunk.size.to_s(16)
  [ size_in_hex, CRLF, chunk, CRLF ].join
end

#get_config_setting(service_name) ⇒ Object



236
237
238
239
240
241
242
243
244
# File 'lib/grack/server.rb', line 236

def get_config_setting(service_name)
  service_name = service_name.gsub('-', '')
  setting = get_git_config("http.#{service_name}")
  if service_name == 'uploadpack'
    return setting != 'false'
  else
    return setting == 'true'
  end
end

#get_git_config(config_name) ⇒ Object



246
247
248
249
# File 'lib/grack/server.rb', line 246

def get_git_config(config_name)
  cmd = git_command("config #{config_name}")
  `#{cmd}`.chomp
end

#get_git_dir(path) ⇒ Object



191
192
193
194
195
196
197
198
# File 'lib/grack/server.rb', line 191

def get_git_dir(path)
  root = @config[:project_root] || `pwd`
  path = File.join(root, path)
  if File.exists?(path) # TODO: check is a valid git directory
    return path
  end
  false
end

#get_idx_fileObject



143
144
145
146
147
# File 'lib/grack/server.rb', line 143

def get_idx_file
  send_file(@reqfile, "application/x-git-packed-objects-toc") do
    hdr_cache_forever
  end
end

#get_info_packsObject



124
125
126
127
128
129
# File 'lib/grack/server.rb', line 124

def get_info_packs
  # objects/info/packs
  send_file(@reqfile, "text/plain; charset=utf-8") do
    hdr_nocache
  end
end

#get_info_refsObject



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/grack/server.rb', line 97

def get_info_refs
  service_name = get_service_type

  if has_access(service_name)
    cmd = git_command("#{service_name} --stateless-rpc --advertise-refs .")
    refs = `#{cmd}`

    @res = Rack::Response.new
    @res.status = 200
    @res["Content-Type"] = "application/x-git-%s-advertisement" % service_name
    hdr_nocache
    @res.write(pkt_write("# service=git-#{service_name}\n"))
    @res.write(pkt_flush)
    @res.write(refs)
    @res.finish
  else
    dumb_info_refs
  end
end

#get_loose_objectObject



131
132
133
134
135
# File 'lib/grack/server.rb', line 131

def get_loose_object
  send_file(@reqfile, "application/x-git-loose-object") do
    hdr_cache_forever
  end
end

#get_pack_fileObject



137
138
139
140
141
# File 'lib/grack/server.rb', line 137

def get_pack_file
  send_file(@reqfile, "application/x-git-packed-objects") do
    hdr_cache_forever
  end
end

#get_service_typeObject



200
201
202
203
204
205
# File 'lib/grack/server.rb', line 200

def get_service_type
  service_type = @req.params['service']
  return false if !service_type
  return false if service_type[0, 4] != 'git-'
  service_type.gsub('git-', '')
end

#get_text_fileObject



149
150
151
152
153
# File 'lib/grack/server.rb', line 149

def get_text_file
  send_file(@reqfile, "text/plain") do
    hdr_nocache
  end
end

#git_command(command) ⇒ Object



264
265
266
267
268
# File 'lib/grack/server.rb', line 264

def git_command(command)
  git_bin = @config[:git_path] || 'git'
  command = "#{git_bin} #{command}"
  command
end

#has_access(rpc, check_content_type = false) ⇒ Object



222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/grack/server.rb', line 222

def has_access(rpc, check_content_type = false)
  if check_content_type
    return false if @req.content_type != "application/x-git-%s-request" % rpc
  end
  return false if !['upload-pack', 'receive-pack'].include? rpc
  if rpc == 'receive-pack'
    return @config[:receive_pack] if @config.include? :receive_pack
  end
  if rpc == 'upload-pack'
    return @config[:upload_pack] if @config.include? :upload_pack
  end
  return get_config_setting(rpc)
end

#hdr_cache_foreverObject



316
317
318
319
320
321
# File 'lib/grack/server.rb', line 316

def hdr_cache_forever
  now = Time.now().to_i
  @res["Date"] = now.to_s
  @res["Expires"] = (now + 31536000).to_s;
  @res["Cache-Control"] = "public, max-age=31536000";
end

#hdr_nocacheObject


header writing functions




310
311
312
313
314
# File 'lib/grack/server.rb', line 310

def hdr_nocache
  @res["Expires"] = "Fri, 01 Jan 1980 00:00:00 GMT"
  @res["Pragma"] = "no-cache"
  @res["Cache-Control"] = "no-cache, max-age=0, must-revalidate"
end

#match_routingObject



207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/grack/server.rb', line 207

def match_routing
  cmd = nil
  path = nil
  SERVICES.each do |method, handler, match, rpc|
    if m = Regexp.new(match).match(@req.path_info)
      return ['not_allowed'] if method != @req.request_method
      cmd = handler
      path = m[1]
      file = @req.path_info.sub(path + '/', '')
      return [cmd, path, file, rpc]
    end
  end
  return nil
end

#pkt_flushObject


packet-line handling functions




297
298
299
# File 'lib/grack/server.rb', line 297

def pkt_flush
  '0000'
end

#pkt_write(str) ⇒ Object



301
302
303
# File 'lib/grack/server.rb', line 301

def pkt_write(str)
  (str.size + 4).to_s(base=16).rjust(4, '0') + str
end

#read_bodyObject



251
252
253
254
255
256
257
# File 'lib/grack/server.rb', line 251

def read_body
  if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
    input = Zlib::GzipReader.new(@req.body).read
  else
    input = @req.body.read
  end
end

#render_method_not_allowedObject



276
277
278
279
280
281
282
# File 'lib/grack/server.rb', line 276

def render_method_not_allowed
  if @env['SERVER_PROTOCOL'] == "HTTP/1.1"
    [405, PLAIN_TYPE, ["Method Not Allowed"]]
  else
    [400, PLAIN_TYPE, ["Bad Request"]]
  end
end

#render_no_accessObject



288
289
290
# File 'lib/grack/server.rb', line 288

def render_no_access
  [403, PLAIN_TYPE, ["Forbidden"]]
end

#render_not_foundObject



284
285
286
# File 'lib/grack/server.rb', line 284

def render_not_found
  [404, PLAIN_TYPE, ["Not Found"]]
end

#send_file(reqfile, content_type) ⇒ Object

some of this borrowed from the Rack::File implementation



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/grack/server.rb', line 162

def send_file(reqfile, content_type)
  reqfile = File.join(@dir, reqfile)
  return render_not_found if !F.exists?(reqfile)

  @res = Rack::Response.new
  @res.status = 200
  @res["Content-Type"]  = content_type
  @res["Last-Modified"] = F.mtime(reqfile).httpdate

  yield

  if size = F.size?(reqfile)
    @res["Content-Length"] = size.to_s
    @res.finish do
      F.open(reqfile, "rb") do |file|
        while part = file.read(8192)
          @res.write part
        end
      end
    end
  else
    body = [F.read(reqfile)]
    size = Rack::Utils.bytesize(body.first)
    @res["Content-Length"] = size
    @res.write body
    @res.finish
  end
end

#service_rpcObject



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/grack/server.rb', line 64

def service_rpc
  return render_no_access if !has_access(@rpc, true)
  input = read_body

  @res = Rack::Response.new
  @res.status = 200
  @res["Content-Type"] = "application/x-git-%s-result" % @rpc
  @res["Transfer-Encoding"] = "chunked"
  @res["Cache-Control"] = "no-cache"

  @res.finish do
    command = git_command("#{@rpc} --stateless-rpc #{@dir}")
    IO.popen(command, File::RDWR) do |pipe|
      pipe.write(input)
      pipe.close_write
      while !pipe.eof?
        block = pipe.read(8192)           # 8KB at a time
        @res.write encode_chunk(block)    # stream it to the client
      end
      @res.write terminating_chunk
    end
  end
end

#set_config(config) ⇒ Object



29
30
31
# File 'lib/grack/server.rb', line 29

def set_config(config)
  @config = config || {}
end

#set_config_setting(key, value) ⇒ Object



33
34
35
# File 'lib/grack/server.rb', line 33

def set_config_setting(key, value)
  @config[key] = value
end

#terminating_chunkObject



93
94
95
# File 'lib/grack/server.rb', line 93

def terminating_chunk
  [ 0, CRLF, CRLF ].join
end

#update_server_infoObject



259
260
261
262
# File 'lib/grack/server.rb', line 259

def update_server_info
  cmd = git_command("update-server-info")
  `#{cmd}`
end