Class: Grack::App

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

Overview

A Rack application for serving Git repositories over HTTP.

Constant Summary collapse

VALID_SERVICE_TYPES =

A list of supported pack service types.

%w{git-upload-pack git-receive-pack}
ROUTES =

Route mappings from URIs to valid verbs and handler functions.

[
  [%r'/(.*?)/(git-(?:upload|receive)-pack)$',        'POST', :handle_pack],
  [%r'/(.*?)/info/refs$',                            'GET',  :info_refs],
  [%r'/(.*?)/(HEAD)$',                               'GET',  :text_file],
  [%r'/(.*?)/(objects/info/alternates)$',            'GET',  :text_file],
  [%r'/(.*?)/(objects/info/http-alternates)$',       'GET',  :text_file],
  [%r'/(.*?)/(objects/info/packs)$',                 'GET',  :info_packs],
  [%r'/(.*?)/(objects/info/[^/]+)$',                 'GET',  :text_file],
  [%r'/(.*?)/(objects/[0-9a-f]{2}/[0-9a-f]{38})$',   'GET',  :loose_object],
  [%r'/(.*?)/(objects/pack/pack-[0-9a-f]{40}\.pack)$', 'GET', :pack_file],
  [%r'/(.*?)/(objects/pack/pack-[0-9a-f]{40}\.idx)$', 'GET', :idx_file],
]
PLAIN_TYPE =

A shorthand for specifying a text content type for the Rack response.

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ App

Creates a new instance of this application with the configuration provided by opts.

Options Hash (opts):

  • :root (String) — default: Dir.pwd

    a directory path containing 1 or more Git repositories.

  • :allow_push (Boolean, nil) — default: nil

    determines whether or not to allow pushes into the repositories. nil means to defer to the requested repository.

  • :allow_pull (Boolean, nil) — default: nil

    determines whether or not to allow fetches/pulls from the repositories. nil means to defer to the requested repository.

  • :git_adapter_factory (#call) — default: ->{ GitAdapter.new }

    a call-able object that creates Git adapter instances per request.



49
50
51
52
53
54
55
# File 'lib/grack/app.rb', line 49

def initialize(opts = {})
  @root                = Pathname.new(opts.fetch(:root, '.')).expand_path
  @allow_push          = opts.fetch(:allow_push, nil)
  @allow_pull          = opts.fetch(:allow_pull, nil)
  @git_adapter_factory =
    opts.fetch(:git_adapter_factory, ->{ GitAdapter.new })
end

Instance Attribute Details

#envObject (readonly, private)

The Rack request hash.



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

def env
  @env
end

#gitObject (readonly, private)

The Git adapter instance for the requested repository.



96
97
98
# File 'lib/grack/app.rb', line 96

def git
  @git
end

#pack_typeObject (readonly, private)

The requested pack type. Will be nil for requests that do no involve pack RPCs.



113
114
115
# File 'lib/grack/app.rb', line 113

def pack_type
  @pack_type
end

#repository_uriObject (readonly, private)

The path to the repository.



104
105
106
# File 'lib/grack/app.rb', line 104

def repository_uri
  @repository_uri
end

#requestObject (readonly, private)

The request object built from the request hash.



92
93
94
# File 'lib/grack/app.rb', line 92

def request
  @request
end

#request_verbObject (readonly, private)

The HTTP verb of the request.



108
109
110
# File 'lib/grack/app.rb', line 108

def request_verb
  @request_verb
end

#rootObject (readonly, private)

The path containing 1 or more Git repositories which may be requested.



100
101
102
# File 'lib/grack/app.rb', line 100

def root
  @root
end

Instance Method Details

#_call(env) ⇒ Object (protected)

The real request handler.



77
78
79
80
81
82
# File 'lib/grack/app.rb', line 77

def _call(env)
  @git = @git_adapter_factory.call
  @env = env
  @request = Rack::Request.new(env)
  route
end

#allow_pull?Boolean (private)

Determines whether or not fetches/pulls from the requested repository are allowed.



144
145
146
# File 'lib/grack/app.rb', line 144

def allow_pull?
  @allow_pull || (@allow_pull.nil? && git.allow_pull?)
end

#allow_push?Boolean (private)

Determines whether or not pushes into the requested repository are allowed.



135
136
137
# File 'lib/grack/app.rb', line 135

def allow_push?
  @allow_push || (@allow_push.nil? && git.allow_push?)
end

#authorized?Boolean (private)



117
118
119
120
# File 'lib/grack/app.rb', line 117

def authorized?
  return allow_pull? if need_read?
  return allow_push?
end

#bad_requestObject (private)



394
395
396
# File 'lib/grack/app.rb', line 394

def bad_request
  [400, PLAIN_TYPE, ['Bad Request']]
end

#bad_uri?(path) ⇒ Boolean (private)

Determines whether or not path is an acceptable URI.



362
363
364
365
# File 'lib/grack/app.rb', line 362

def bad_uri?(path)
  invalid_segments = %w{. ..}
  path.split('/').any? { |segment| invalid_segments.include?(segment) }
end

#call(env) ⇒ Object

The Rack handler entry point for this application. This duplicates the object and uses the duplicate to perform the work in order to enable thread safe request handling.



65
66
67
# File 'lib/grack/app.rb', line 65

def call(env)
  dup._call(env)
end

#exchange_pack(headers, io_in, opts = {}) ⇒ Object (private)

Opens a tunnel for the pack file exchange protocol between the client and the Git adapter.



331
332
333
334
335
# File 'lib/grack/app.rb', line 331

def exchange_pack(headers, io_in, opts = {})
  Rack::Response.new([], 200, headers).finish do |response|
    git.handle_pack(pack_type, io_in, response, opts)
  end
end

#handle_pack(pack_type) ⇒ Object (private)

Processes pack file exchange requests for both push and pull. Ensures that the request is allowed and properly formatted.



184
185
186
187
188
189
190
191
192
193
# File 'lib/grack/app.rb', line 184

def handle_pack(pack_type)
  @pack_type = pack_type
  unless request.content_type == "application/x-#{@pack_type}-request" &&
         valid_pack_type? && authorized?
    return no_access
  end

  headers = {'Content-Type' => "application/x-#{@pack_type}-result"}
  exchange_pack(headers, request_io_in)
end

#hdr_cache_foreverObject (private)



429
430
431
432
433
434
435
436
# File 'lib/grack/app.rb', line 429

def hdr_cache_forever
  now = Time.now().to_i
  {
    'Date'          => now.to_s,
    'Expires'       => (now + 31536000).to_s,
    'Cache-Control' => 'public, max-age=31536000'
  }
end

#hdr_nocacheObject (private)

NOTE: This should probably be converted to a constant.



419
420
421
422
423
424
425
# File 'lib/grack/app.rb', line 419

def hdr_nocache
  {
    'Expires'       => 'Fri, 01 Jan 1980 00:00:00 GMT',
    'Pragma'        => 'no-cache',
    'Cache-Control' => 'no-cache, max-age=0, must-revalidate'
  }
end

#idx_file(path) ⇒ Object (private)

Process a request for a pack index file located at path for the selected repository. If the file is located, the content type is set to application/x-git-packed-objects-toc and permanent caching is enabled.



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

def idx_file(path)
  return no_access unless authorized?
  send_file(
    git.file(path),
    'application/x-git-packed-objects-toc',
    hdr_cache_forever
  )
end

#info_packs(path) ⇒ Object (private)

Processes requests for info packs for the requested repository.



229
230
231
232
# File 'lib/grack/app.rb', line 229

def info_packs(path)
  return no_access unless authorized?
  send_file(git.file(path), 'text/plain; charset=utf-8', hdr_nocache)
end

#info_refsObject (private)

Processes requests for the list of refs for the requested repository.

This works for both Smart HTTP clients and basic ones. For basic clients, the Git adapter is used to update the info/refs file which is then served to the clients. For Smart HTTP clients, the more efficient pack file exchange mechanism is used.



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

def info_refs
  @pack_type = request.params['service']
  return no_access unless authorized?

  if @pack_type.nil?
    git.update_server_info
    send_file(
      git.file('info/refs'), 'text/plain; charset=utf-8', hdr_nocache
    )
  elsif valid_pack_type?
    headers = hdr_nocache
    headers['Content-Type'] = "application/x-#{@pack_type}-advertisement"
    exchange_pack(headers, nil, {:advertise_refs => true})
  else
    not_found
  end
end

#loose_object(path) ⇒ Object (private)

Processes a request for a loose object at path for the selected repository. If the file is located, the content type is set to application/x-git-loose-object and permanent caching is enabled.



243
244
245
246
247
248
# File 'lib/grack/app.rb', line 243

def loose_object(path)
  return no_access unless authorized?
  send_file(
    git.file(path), 'application/x-git-loose-object', hdr_cache_forever
  )
end

#method_not_allowedObject (private)

Returns a Rack response appropriate for requests that use invalid verbs for the requested resources.

For HTTP 1.1 requests, a 405 code is returned. For other versions, the value from #bad_request is returned.



384
385
386
387
388
389
390
# File 'lib/grack/app.rb', line 384

def method_not_allowed
  if env['SERVER_PROTOCOL'] == 'HTTP/1.1'
    [405, PLAIN_TYPE, ['Method Not Allowed']]
  else
    bad_request
  end
end

#need_read?Boolean (private)



125
126
127
128
# File 'lib/grack/app.rb', line 125

def need_read?
  (request_verb == 'GET' && pack_type != 'git-receive-pack') ||
    request_verb == 'POST' && pack_type == 'git-upload-pack'
end

#no_accessObject (private)



406
407
408
# File 'lib/grack/app.rb', line 406

def no_access
  [403, PLAIN_TYPE, ['Forbidden']]
end

#not_foundObject (private)



400
401
402
# File 'lib/grack/app.rb', line 400

def not_found
  [404, PLAIN_TYPE, ['Not Found']]
end

#pack_file(path) ⇒ Object (private)

Process a request for a pack file located at path for the selected repository. If the file is located, the content type is set to application/x-git-packed-objects and permanent caching is enabled.



259
260
261
262
263
264
# File 'lib/grack/app.rb', line 259

def pack_file(path)
  return no_access unless authorized?
  send_file(
    git.file(path), 'application/x-git-packed-objects', hdr_cache_forever
  )
end

#request_io_in#read (private)

Transparently ensures that the request body is not compressed.



342
343
344
345
# File 'lib/grack/app.rb', line 342

def request_io_in
  return request.body unless env['HTTP_CONTENT_ENCODING'] =~ /gzip/
  Zlib::GzipReader.new(request.body)
end

#routeObject (private)

Routes requests to appropriate handlers. Performs request path cleanup and several sanity checks prior to attempting to handle the request.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/grack/app.rb', line 153

def route
  # Sanitize the URI:
  # * Unescape escaped characters
  # * Replace runs of / with a single /
  path_info = Rack::Utils.unescape(request.path_info).gsub(%r{/+}, '/')

  ROUTES.each do |path_matcher, verb, handler|
    path_info.match(path_matcher) do |match|
      @repository_uri = match[1]
      @request_verb = verb

      return method_not_allowed unless verb == request.request_method
      return bad_request if bad_uri?(@repository_uri)

      git.repository_path = root + @repository_uri
      return not_found unless git.exist?

      return send(handler, *match[2..-1])
    end
  end
  not_found
end

#send_file(streamer, content_type, headers = {}) ⇒ Object (private)

Produces a Rack response that wraps the output from the Git adapter.

A 404 response is produced if streamer is nil. Otherwise a 200 response is produced with streamer as the response body.



311
312
313
314
315
316
317
318
# File 'lib/grack/app.rb', line 311

def send_file(streamer, content_type, headers = {})
  return not_found if streamer.nil?

  headers['Content-Type'] = content_type
  headers['Last-Modified'] = streamer.mtime.httpdate

  [200, headers, streamer]
end

#text_file(path) ⇒ Object (private)

Process a request for a generic file located at path for the selected repository. If the file is located, the content type is set to text/plain and caching is disabled.



294
295
296
297
# File 'lib/grack/app.rb', line 294

def text_file(path)
  return no_access unless authorized?
  send_file(git.file(path), 'text/plain', hdr_nocache)
end

#valid_pack_type?Boolean (private)

Determines whether or not the requested pack type is valid.



351
352
353
# File 'lib/grack/app.rb', line 351

def valid_pack_type?
  VALID_SERVICE_TYPES.include?(pack_type)
end