# Standard requirements require 'json' require 'yaml' # 3rd part requirements require 'sinatra/base' # Internal requirements require 'git/webby/extensions' # See Git::Webby for documentation. module Git # The main goal of the Git::Webby is implement the following useful # 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. See # Config::DEFAULTS for the values will be initialized by default. # # 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 the # {git-http-backend official # page}[http://www.kernel.org/pub/software/scm/git/docs/git-http-backend.html] # # [*default*] # Default configuration. All attributes will be used by all modular # applications. # # *project_root* :: # Sets the root directory where repositories have been # placed. # *git_path* :: # Path to the git command line. # # [*http_backend*] # HTTP-Backend configuration. # # *authenticate* :: # Sets if authentication is required. # # *get_any_file* :: # Like +http.getanyfile+. # # *upload_pack* :: # Like +http.uploadpack+. # # *receive_pack* :: # Like +http.receivepack+. module Webby class ProjectHandler #:nodoc: # Path to git command 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) %Q[#{@path} #{args.unshift(command.to_s.gsub("_","-")).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 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 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) self.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 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.webby.authenticated"] end def authenticate(username, password) checked = [ username, password ] == authentication.credentials validated = authentication.provided? && authentication.basic? granted = htpasswd.authenticated? username, password if checked and validated and granted request.env["git.webby.authenticated"] = true request.env["REMOTE_USER"] = authentication.username else nil end end def unauthorized!(realm = Git::Webby::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/webby/http_backend" class << self def config @config ||= { :default => { :project_root => "/home/git", :git_path => "/usr/bin/git" }, :http_backend => { :authenticate => true, :get_any_file => true, :upload_pack => true, :receive_pack => false } }.to_struct end # Configure Git::Webby modules using keys. See Config for options. def configure(&block) 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::Webby.config.default.project_root } set :git_path, lambda { Git::Webby.config.default.git_path } mime_type :json, "application/json" end end end