class Grack::App

A Rack application for serving Git repositories over HTTP.

Constants

PLAIN_TYPE

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

ROUTES

Route mappings from URIs to valid verbs and handler functions.

VALID_SERVICE_TYPES

A list of supported pack service types.

Attributes

env[R]

The Rack request hash.

git[R]

The Git adapter instance for the requested repository.

pack_type[R]

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

repository_uri[R]

The path to the repository.

request[R]

The request object built from the request hash.

request_verb[R]

The HTTP verb of the request.

root[R]

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

Public Class Methods

new(opts = {}) click to toggle source

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

@param [Hash] opts a hash of supported options. @option opts [String] :root (Dir.pwd) a directory path containing 1 or

more Git repositories.

@option opts [Boolean, nil] :allow_push (nil) determines whether or not to

allow pushes into the repositories.  +nil+ means to defer to the
requested repository.

@option opts [Boolean, nil] :allow_pull (nil) determines whether or not to

allow fetches/pulls from the repositories.  +nil+ means to defer to the
requested repository.

@option opts [#call] :git_adapter_factory (->{ GitAdapter.new }) a

call-able object that creates Git adapter instances per request.
# 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

Public Instance Methods

call(env) click to toggle source

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.

@param [Hash] env a Rack request hash.

@return a Rack response object.

# File lib/grack/app.rb, line 65
def call(env)
  dup._call(env)
end

Protected Instance Methods

_call(env) click to toggle source

The real request handler.

@param [Hash] env a Rack request hash.

@return a Rack response object.

# File lib/grack/app.rb, line 77
def _call(env)
  @git = @git_adapter_factory.call
  @env = env
  @request = Rack::Request.new(env)
  route
end

Private Instance Methods

allow_pull?() click to toggle source

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

@return [Boolean] true if fetches are allowed, false otherwise.

# File lib/grack/app.rb, line 144
def allow_pull?
  @allow_pull || (@allow_pull.nil? && git.allow_pull?)
end
allow_push?() click to toggle source

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

@return [Boolean] true if pushes are allowed, false otherwise.

# File lib/grack/app.rb, line 135
def allow_push?
  @allow_push || (@allow_push.nil? && git.allow_push?)
end
authorized?() click to toggle source

@return [Boolean] true if the request is authorized; otherwise, false.

# File lib/grack/app.rb, line 117
def authorized?
  return allow_pull? if need_read?
  return allow_push?
end
bad_request() click to toggle source

@return a Rack response for generally bad requests.

# File lib/grack/app.rb, line 394
def bad_request
  [400, PLAIN_TYPE, ['Bad Request']]
end
bad_uri?(path) click to toggle source

Determines whether or not path is an acceptable URI.

@param [String] path the path part of the request URI.

@return [Boolean] true if the requested path is considered invalid;

otherwise, +false+.
# File lib/grack/app.rb, line 362
def bad_uri?(path)
  invalid_segments = %w{. ..}
  path.split('/').any? { |segment| invalid_segments.include?(segment) }
end
exchange_pack(headers, io_in, opts = {}) click to toggle source

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

@param [Hash] headers headers to provide in the Rack response. @param [#read] io_in a readable, IO-like object providing client input

data.

@param [Hash] opts options to pass to the Git adapter's handle_pack

method.

@return a Rack response object.

# 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) click to toggle source

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

@param [String] pack_type the type of pack exchange to perform per the

request.

@return a Rack response object.

# 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_forever() click to toggle source

@return a hash of headers that should trigger caches permanent caching.

# 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_nocache() click to toggle source

NOTE: This should probably be converted to a constant.

@return a hash of headers that should prevent caching of a Rack response.

# 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) click to toggle source

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.

@param [String] path the path to a pack index file within a Git

repository, such as
+pack/pack-62c9f443d8405cd6da92dcbb4f849cc01a339c06.idx+.

@return a Rack response object.

# 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) click to toggle source

Processes requests for info packs for the requested repository.

@param [String] path the path to an info pack file within a Git

repository.

@return a Rack response object.

# 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_refs() click to toggle source

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.

@return a Rack response object.

# 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) click to toggle source

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.

@param [String] path the path to a loose object file within a Git

repository, such as +objects/31/d73eb4914a8ddb6cb0e4adf250777161118f90+.

@return a Rack response object.

# 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_allowed() click to toggle source

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.

@return a Rack response appropriate for requests that use invalid verbs

for the requested resources.
# 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?() click to toggle source

@return [Boolean] true if read permissions are needed; otherwise,

+false+.
# 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_access() click to toggle source

@return a Rack response for forbidden resources.

# File lib/grack/app.rb, line 406
def no_access
  [403, PLAIN_TYPE, ['Forbidden']]
end
not_found() click to toggle source

@return a Rack response for unlocatable resources.

# File lib/grack/app.rb, line 400
def not_found
  [404, PLAIN_TYPE, ['Not Found']]
end
pack_file(path) click to toggle source

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.

@param [String] path the path to a pack file within a Git repository such

as +pack/pack-62c9f443d8405cd6da92dcbb4f849cc01a339c06.pack+.

@return a Rack response object.

# 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() click to toggle source

Transparently ensures that the request body is not compressed.

@return [#read] a read-able object that yields uncompressed data from

the request body.
# 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
route() click to toggle source

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

@return a Rack response object.

# 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 = {}) click to toggle source

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.

@param [FileStreamer,IOStreamer] streamer a provider of content for the

response body.

@param [String] content_type the MIME type of the content. @param [Hash] headers additional headers to include in the response.

@return a Rack response object.

# 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) click to toggle source

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.

@param [String] path the path to a file within a Git repository, such as

+HEAD+.

@return a Rack response object.

# 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?() click to toggle source

Determines whether or not the requested pack type is valid.

@return [Boolean] true if the pack type is valid; otherwise, false.

# File lib/grack/app.rb, line 351
def valid_pack_type?
  VALID_SERVICE_TYPES.include?(pack_type)
end