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
The Rack request hash.
The Git adapter instance for the requested repository.
The requested pack type. Will be nil
for requests that do no involve pack RPCs.
The path to the repository.
The request object built from the request hash.
The HTTP verb of the request.
The path containing 1 or more Git repositories which may be requested.
Public Class Methods
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
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
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
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
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
@return a Rack response for generally bad requests.
# File lib/grack/app.rb, line 394 def bad_request [400, PLAIN_TYPE, ['Bad Request']] end
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
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
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
@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
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
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
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
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
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
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
@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
@return a Rack response for forbidden resources.
# File lib/grack/app.rb, line 406 def no_access [403, PLAIN_TYPE, ['Forbidden']] end
@return a Rack response for unlocatable resources.
# File lib/grack/app.rb, line 400 def not_found [404, PLAIN_TYPE, ['Not Found']] end
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
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
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
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
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
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