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.

Parameters:

  • opts (Hash) (defaults to: {})

    a hash of supported options.

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.

Parameters:

  • env (Hash)

    a Rack request hash.

Returns:

  • a Rack response object.



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.

Returns:

  • (Boolean)

    true if fetches are allowed, false otherwise.



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.

Returns:

  • (Boolean)

    true if pushes are allowed, false otherwise.



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)

Returns true if the request is authorized; otherwise, false.

Returns:

  • (Boolean)

    true if the request is authorized; otherwise, false.



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)

Returns a Rack response for generally bad requests.

Returns:

  • a Rack response for generally bad requests.



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.

Parameters:

  • path (String)

    the path part of the request URI.

Returns:

  • (Boolean)

    true if the requested path is considered invalid; otherwise, false.



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.

Parameters:

  • env (Hash)

    a Rack request hash.

Returns:

  • a Rack response object.



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.

Parameters:

  • headers (Hash)

    headers to provide in the Rack response.

  • io_in (#read)

    a readable, IO-like object providing client input data.

  • opts (Hash) (defaults to: {})

    options to pass to the Git adapter’s #handle_pack method.

Returns:

  • a Rack response object.



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.

Parameters:

  • pack_type (String)

    the type of pack exchange to perform per the request.

Returns:

  • a Rack response object.



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)

Returns a hash of headers that should trigger caches permanent caching.

Returns:

  • a hash of headers that should trigger caches permanent caching.



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.

Returns:

  • a hash of headers that should prevent caching of a Rack response.



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.

Parameters:

  • path (String)

    the path to a pack index file within a Git repository, such as pack/pack-62c9f443d8405cd6da92dcbb4f849cc01a339c06.idx.

Returns:

  • a Rack response object.



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.

Parameters:

  • path (String)

    the path to an info pack file within a Git repository.

Returns:

  • a Rack response object.



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.

Returns:

  • a Rack response object.



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.

Parameters:

  • path (String)

    the path to a loose object file within a Git repository, such as objects/31/d73eb4914a8ddb6cb0e4adf250777161118f90.

Returns:

  • a Rack response object.



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.

Returns:

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



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)

Returns true if read permissions are needed; otherwise, false.

Returns:

  • (Boolean)

    true if read permissions are needed; otherwise, false.



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)

Returns a Rack response for forbidden resources.

Returns:

  • a Rack response for forbidden resources.



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

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

#not_foundObject (private)

Returns a Rack response for unlocatable resources.

Returns:

  • a Rack response for unlocatable resources.



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.

Parameters:

  • path (String)

    the path to a pack file within a Git repository such as pack/pack-62c9f443d8405cd6da92dcbb4f849cc01a339c06.pack.

Returns:

  • a Rack response object.



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.

Returns:

  • (#read)

    a read-able object that yields uncompressed data from the request body.



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.

Returns:

  • a Rack response object.



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.

Parameters:

  • streamer (FileStreamer, IOStreamer)

    a provider of content for the response body.

  • content_type (String)

    the MIME type of the content.

  • headers (Hash) (defaults to: {})

    additional headers to include in the response.

Returns:

  • a Rack response object.



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.

Parameters:

  • path (String)

    the path to a file within a Git repository, such as HEAD.

Returns:

  • a Rack response object.



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.

Returns:

  • (Boolean)

    true if the pack type is valid; otherwise, false.



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

def valid_pack_type?
  VALID_SERVICE_TYPES.include?(pack_type)
end