Class: Grack::App
- Inherits:
-
Object
- Object
- Grack::App
- 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
-
#env ⇒ Object
readonly
private
The Rack request hash.
-
#git ⇒ Object
readonly
private
The Git adapter instance for the requested repository.
-
#pack_type ⇒ Object
readonly
private
The requested pack type.
-
#repository_uri ⇒ Object
readonly
private
The path to the repository.
-
#request ⇒ Object
readonly
private
The request object built from the request hash.
-
#request_verb ⇒ Object
readonly
private
The HTTP verb of the request.
-
#root ⇒ Object
readonly
private
The path containing 1 or more Git repositories which may be requested.
Instance Method Summary collapse
-
#_call(env) ⇒ Object
protected
The real request handler.
-
#allow_pull? ⇒ Boolean
private
Determines whether or not fetches/pulls from the requested repository are allowed.
-
#allow_push? ⇒ Boolean
private
Determines whether or not pushes into the requested repository are allowed.
-
#authorized? ⇒ Boolean
private
true
if the request is authorized; otherwise,false
. -
#bad_request ⇒ Object
private
A Rack response for generally bad requests.
-
#bad_uri?(path) ⇒ Boolean
private
Determines whether or not path is an acceptable URI.
-
#call(env) ⇒ Object
The Rack handler entry point for this application.
-
#exchange_pack(headers, io_in, opts = {}) ⇒ Object
private
Opens a tunnel for the pack file exchange protocol between the client and the Git adapter.
-
#handle_pack(pack_type) ⇒ Object
private
Processes pack file exchange requests for both push and pull.
-
#hdr_cache_forever ⇒ Object
private
A hash of headers that should trigger caches permanent caching.
-
#hdr_nocache ⇒ Object
private
NOTE: This should probably be converted to a constant.
-
#idx_file(path) ⇒ Object
private
Process a request for a pack index file located at path for the selected repository.
-
#info_packs(path) ⇒ Object
private
Processes requests for info packs for the requested repository.
-
#info_refs ⇒ Object
private
Processes requests for the list of refs for the requested repository.
-
#initialize(opts = {}) ⇒ App
constructor
Creates a new instance of this application with the configuration provided by opts.
-
#loose_object(path) ⇒ Object
private
Processes a request for a loose object at path for the selected repository.
-
#method_not_allowed ⇒ Object
private
Returns a Rack response appropriate for requests that use invalid verbs for the requested resources.
-
#need_read? ⇒ Boolean
private
true
if read permissions are needed; otherwise,false
. -
#no_access ⇒ Object
private
A Rack response for forbidden resources.
-
#not_found ⇒ Object
private
A Rack response for unlocatable resources.
-
#pack_file(path) ⇒ Object
private
Process a request for a pack file located at path for the selected repository.
-
#request_io_in ⇒ #read
private
Transparently ensures that the request body is not compressed.
-
#route ⇒ Object
private
Routes requests to appropriate handlers.
-
#send_file(streamer, content_type, headers = {}) ⇒ Object
private
Produces a Rack response that wraps the output from the Git adapter.
-
#text_file(path) ⇒ Object
private
Process a request for a generic file located at path for the selected repository.
-
#valid_pack_type? ⇒ Boolean
private
Determines whether or not the requested pack type is valid.
Constructor Details
#initialize(opts = {}) ⇒ App
Creates a new instance of this application with the configuration provided by opts.
49 50 51 52 53 54 55 |
# File 'lib/grack/app.rb', line 49 def initialize(opts = {}) @root = Pathname.new(opts.fetch(:root, '.')). @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
#env ⇒ Object (readonly, private)
The Rack request hash.
88 89 90 |
# File 'lib/grack/app.rb', line 88 def env @env end |
#git ⇒ Object (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_type ⇒ Object (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_uri ⇒ Object (readonly, private)
The path to the repository.
104 105 106 |
# File 'lib/grack/app.rb', line 104 def repository_uri @repository_uri end |
#request ⇒ Object (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_verb ⇒ Object (readonly, private)
The HTTP verb of the request.
108 109 110 |
# File 'lib/grack/app.rb', line 108 def request_verb @request_verb end |
#root ⇒ Object (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)
Returns true
if the request is authorized; otherwise, false
.
117 118 119 120 |
# File 'lib/grack/app.rb', line 117 def return allow_pull? if need_read? return allow_push? end |
#bad_request ⇒ Object (private)
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.
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? && return no_access end headers = {'Content-Type' => "application/x-#{@pack_type}-result"} exchange_pack(headers, request_io_in) end |
#hdr_cache_forever ⇒ Object (private)
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_nocache ⇒ Object (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 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 send_file(git.file(path), 'text/plain; charset=utf-8', hdr_nocache) end |
#info_refs ⇒ Object (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 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 send_file( git.file(path), 'application/x-git-loose-object', hdr_cache_forever ) end |
#method_not_allowed ⇒ Object (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)
Returns 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_access ⇒ Object (private)
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_found ⇒ Object (private)
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.
259 260 261 262 263 264 |
# File 'lib/grack/app.rb', line 259 def pack_file(path) return no_access unless 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 |
#route ⇒ Object (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 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 |