lib/git/lighttp.rb
# Standard requirements
require 'yaml'
# 3rd part requirements
require 'sinatra/base'
require 'json'
# Internal requirements
require 'git/lighttp/extensions'
require 'git/lighttp/version'
# See *Git::Lighttp* for documentation.
module Git
# Module wich implements the following features:
#
# - Smart-HTTP, based on +git-http-backend+.
# - Authentication flexible based on database or configuration file like
# +.htpasswd+.
# - API to get information about repository.
#
# This class configure the needed variables used by application.
#
# Basically, the +default+ attribute set the values that will be necessary
# by all applications.
#
# The HTTP-Backend application is configured by +http_backend+ attribute to
# set the Git RCP CLI. More details about this feature, see
# +git-http-backend+ {official
# page}[http://www.kernel.org/pub/software/scm/git/docs/git-http-backend.html].
#
# For tree view (JSON API) just use the attribute +treeish+.
#
# [*default*]
# Default configuration. All attributes will be used by all modular
# applications.
#
# *default.project_root* ::
# Sets the root directory where repositories have been
# placed.
# *default.git_path* ::
# Path to the git command line.
#
# [*treeish*]
# Configuration for Treeish JSON API.
#
# *treeish.authenticate* ::
# Sets if the tree view server requires authentication.
#
# [*http_backend*]
# HTTP-Backend configuration.
#
# *http_backend.authenticate* ::
# Sets if authentication is required.
#
# *http_backend.get_any_file* ::
# Like +http.getanyfile+.
#
# *http_backend.upload_pack* ::
# Like +http.uploadpack+.
#
# *http_backend.receive_pack* ::
# Like +http.receivepack+.
module Lighttp
class ProjectHandler #:nodoc:
# Path to git comamnd
attr_reader :path
attr_reader :project_root
attr_reader :repository
def initialize(project_root, path = '/usr/bin/git')
@repository = nil
@path = check_path(File.expand_path(path))
@project_root = check_path(File.expand_path(project_root))
end
def path_to(*args)
File.join(@repository || @project_root, *args.compact.map(&:to_s))
end
def repository=(name)
@repository = check_path(path_to(name))
end
def cli(command, *args)
"#{@path} #{args.unshift(command.to_s.tr('_', '-')).compact.join(' ')}"
end
def run(command, *args)
chdir { %x(#{cli command, *args}) }
end
def read_file(*file)
File.read(path_to(*file))
end
def loose_object_path(*hash)
path_to(:objects, *hash)
end
def pack_idx_path(pack)
path_to(:objects, :pack, pack)
end
def info_packs_path
path_to(:objects, :info, :packs)
end
def tree(ref = 'HEAD', path = '')
list = run("ls-tree --abbrev=6 --full-tree --long #{ref}:#{path}")
if list
tree = []
pattern = /^
(?<ty>\d{3})
(?<pu>\d)
(?<pg>\d)
(?<po>\d)[ ]
(?<ot>\w.*?)[ ]
(?<oh>.{6})[ \t]{0,}
(?<sz>.*?)\t
(?<nm>.*?)\n
/mux
list.scan pattern do |ty, pu, pg, po, ot, oh, sz, nm|
object = {
ftype: ftype[ty],
fperm: "#{fperm[pu.to_i]}#{fperm[pg.to_i]}#{fperm[po.to_i]}",
otype: ot.to_sym,
ohash: oh,
fsize: fsize(sz, 2),
fname: nm
}
object[:objects] = nil if object[:otype] == :tree
tree << object
end
tree
else
false
end
end
private
def repository_path(name)
bare = name =~ /\w\.git/ ? name : %W(#{name} .git)
check_path(path_to(*bare))
end
def check_path(path)
path && !path.empty? && File.ftype(path) && path
end
def chdir(&block)
Dir.chdir(@repository || @project_root, &block)
end
def ftype
{ '120' => 'l', '100' => '-', '040' => 'd' }
end
def fperm
['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']
end
def fsize(str, scale = 1)
units = [:b, :kb, :mb, :gb, :tb]
value = str.to_f
size = 0.0
units.each_index do |i|
size = value / 1024**i
return [
format("%.#{scale}f", size).to_f,
units[i].to_s.upcase
] if size <= 10
end
end
end
class Htpasswd #:nodoc:
def initialize(file)
require 'webrick/httpauth/htpasswd'
@handler = WEBrick::HTTPAuth::Htpasswd.new(file)
yield self if block_given?
end
def find(username) #:yield: password, salt
password = @handler.get_passwd(nil, username, false)
if block_given?
yield password ? [password, password[0, 2]] : [nil, nil]
else
password
end
end
def authenticated?(username, password)
find username do |crypted, salt|
crypted && salt && crypted == password.crypt(salt)
end
end
def create(username, password)
@handler.set_passwd(nil, username, password)
end
alias update create
def destroy(username)
@handler.delete_passwd(nil, username)
end
def include?(username)
users.include? username
end
def size
users.size
end
def write!
@handler.flush
end
private
def users
@handler.each { |username, _password| username }
end
end
class Htgroup #:nodoc:
def initialize(file)
require 'webrick/httpauth/htgroup'
WEBrick::HTTPAuth::Htgroup.class_eval do
attr_reader :group
end
@handler = WEBrick::HTTPAuth::Htgroup.new(file)
yield self if block_given?
end
def members(group)
@handler.members(group)
end
def groups(username)
@handler.group.select do |_group, members|
members.include? username
end.keys
end
end
module GitHelpers #:nodoc:
def git
@git ||= ProjectHandler.new(settings.project_root, settings.git_path)
end
def repository
git.repository ||= (params[:repository] || params[:captures].first)
git
end
def content_type_for_git(name, *suffixes)
content_type("application/x-git-#{name}-#{suffixes.compact.join('-')}")
end
end
module AuthenticationHelpers #:nodoc:
def htpasswd
@htpasswd ||= Htpasswd.new(git.path_to('htpasswd'))
end
def authentication
@authentication ||= Rack::Auth::Basic::Request.new request.env
end
def authenticated?
request.env['REMOTE_USER'] && request.env['git.lighttp.authenticated']
end
def authenticate(username, password)
checked = [username, password] == authentication.credentials
validated = authentication.provided? && authentication.basic?
granted = htpasswd.authenticated? username, password
if checked && validated && granted
request.env['git.lighttp.authenticated'] = true
request.env['REMOTE_USER'] = authentication.username
else
false
end
end
def unauthorized!(realm = Git::Lighttp.info)
headers 'WWW-Authenticate' => %(Basic realm="#{realm}")
throw :halt, [401, 'Authorization Required']
end
def bad_request!
throw :halt, [400, 'Bad Request']
end
def authenticate!
return if authenticated?
unauthorized! unless authentication.provided?
bad_request! unless authentication.basic?
unauthorized! unless authenticate(*authentication.credentials)
request.env['REMOTE_USER'] = authentication.username
end
def access_granted?(username, password)
authenticated? || authenticate(username, password)
end
end # AuthenticationHelpers
# Servers
autoload :HttpBackend, 'git/lighttp/http_backend'
autoload :Treeish, 'git/lighttp/treeish'
class << self
DEFAULT_CONFIG = {
default: {
project_root: '/home/git',
git_path: '/usr/bin/git'
},
treeish: {
authenticate: false
},
http_backend: {
authenticate: true,
get_any_file: true,
upload_pack: true,
receive_pack: false
}
}.freeze
def config
@config ||= DEFAULT_CONFIG.to_struct
end
def config_reset!
@config = DEFAULT_CONFIG.to_struct
end
# Configure Git::Lighttp modules using keys. See Config for options.
def configure(*) # :yield: config
yield config
config
end
def load_config_file(file)
YAML.load_file(file).to_struct.each_pair do |app, options|
options.each_pair do |option, value|
config[app][option] = value
end
end
config
rescue IndexError
abort 'configuration option not found'
end
end
class Application < Sinatra::Base #:nodoc:
set :project_root, lambda { Git::Lighttp.config.default.project_root }
set :git_path, lambda { Git::Lighttp.config.default.git_path }
mime_type :json, 'application/json'
end
end
end