diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/action.rb | 4 | ||||
-rw-r--r-- | lib/action/custom.rb | 146 | ||||
-rw-r--r-- | lib/console_helper.rb | 17 | ||||
-rw-r--r-- | lib/gitlab_access_status.rb | 51 | ||||
-rw-r--r-- | lib/gitlab_config.rb | 56 | ||||
-rw-r--r-- | lib/gitlab_init.rb | 7 | ||||
-rw-r--r-- | lib/gitlab_keys.rb | 39 | ||||
-rw-r--r-- | lib/gitlab_lfs_authentication.rb | 43 | ||||
-rw-r--r-- | lib/gitlab_logger.rb | 120 | ||||
-rw-r--r-- | lib/gitlab_metrics.rb | 59 | ||||
-rw-r--r-- | lib/gitlab_net.rb | 165 | ||||
-rw-r--r-- | lib/gitlab_net/errors.rb | 4 | ||||
-rw-r--r-- | lib/gitlab_shell.rb | 277 | ||||
-rw-r--r-- | lib/hooks_utils.rb | 15 | ||||
-rw-r--r-- | lib/http_helper.rb | 125 | ||||
-rw-r--r-- | lib/httpunix.rb | 54 | ||||
-rw-r--r-- | lib/object_dirs_helper.rb | 39 |
17 files changed, 0 insertions, 1221 deletions
diff --git a/lib/action.rb b/lib/action.rb deleted file mode 100644 index 28c1c14..0000000 --- a/lib/action.rb +++ /dev/null @@ -1,4 +0,0 @@ -require_relative 'action/custom' - -module Action -end diff --git a/lib/action/custom.rb b/lib/action/custom.rb deleted file mode 100644 index 4efb0a8..0000000 --- a/lib/action/custom.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'base64' - -require_relative '../http_helper' -require_relative '../console_helper' - -module Action - class Custom - include HTTPHelper - include ConsoleHelper - - class BaseError < StandardError; end - class MissingPayloadError < BaseError; end - class MissingAPIEndpointsError < BaseError; end - class MissingDataError < BaseError; end - class UnsuccessfulError < BaseError; end - - NO_MESSAGE_TEXT = 'No message'.freeze - DEFAULT_HEADERS = { 'Content-Type' => CONTENT_TYPE_JSON }.freeze - - def initialize(gl_id, payload) - @gl_id = gl_id - @payload = payload - end - - def execute - validate! - inform_client(info_message) if info_message - process_api_endpoints! - end - - private - - attr_reader :gl_id, :payload - - def process_api_endpoints! - output = '' - resp = nil - - data_with_gl_id = data.merge('gl_id' => gl_id) - - api_endpoints.each do |endpoint| - url = "#{base_url}#{endpoint}" - json = { 'data' => data_with_gl_id, 'output' => output } - - resp = post(url, {}, headers: DEFAULT_HEADERS, options: { json: json }) - - # Net::HTTPSuccess is the parent of Net::HTTPOK, Net::HTTPCreated etc. - case resp - when Net::HTTPSuccess, Net::HTTPMultipleChoices - true - else - raise_unsuccessful!(resp) - end - - begin - body = JSON.parse(resp.body) - rescue JSON::ParserError - raise UnsuccessfulError, 'Response was not valid JSON' - end - - print_flush(body['result']) - - # In the context of the git push sequence of events, it's necessary to read - # stdin in order to capture output to pass onto subsequent commands - output = read_stdin - end - - resp - end - - def base_url - config.gitlab_url - end - - def data - @data ||= payload['data'] - end - - def api_endpoints - data['api_endpoints'] - end - - def info_message - data['info_message'] - end - - def config - @config ||= GitlabConfig.new - end - - def api - @api ||= GitlabNet.new - end - - def read_stdin - Base64.encode64($stdin.read) - end - - def print_flush(str) - return false unless str - $stdout.print(Base64.decode64(str)) - $stdout.flush - end - - def inform_client(str) - $stderr.puts(format_gitlab_output(str)) - end - - def format_gitlab_output(str) - format_for_stderr(str.split("\n")).join("\n") - end - - def validate! - validate_payload! - validate_data! - validate_api_endpoints! - end - - def validate_payload! - raise MissingPayloadError if !payload.is_a?(Hash) || payload.empty? - end - - def validate_data! - raise MissingDataError unless data.is_a?(Hash) - end - - def validate_api_endpoints! - raise MissingAPIEndpointsError if !api_endpoints.is_a?(Array) || - api_endpoints.empty? - end - - def raise_unsuccessful!(result) - message = "#{exception_message_for(result.body)} (#{result.code})" - raise UnsuccessfulError, format_gitlab_output(message) - end - - def exception_message_for(body) - body = JSON.parse(body) - return body['message'] unless body['message'].to_s.empty? - - body['result'].to_s.empty? ? NO_MESSAGE_TEXT : Base64.decode64(body['result']) - rescue JSON::ParserError - NO_MESSAGE_TEXT - end - end -end diff --git a/lib/console_helper.rb b/lib/console_helper.rb deleted file mode 100644 index 5c8bb83..0000000 --- a/lib/console_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module ConsoleHelper - LINE_PREFACE = '> GitLab:' - - def write_stderr(messages) - format_for_stderr(messages).each do |message| - $stderr.puts(message) - end - end - - def format_for_stderr(messages) - Array(messages).each_with_object([]) do |message, all| - all << "#{LINE_PREFACE} #{message}" unless message.empty? - end - end -end diff --git a/lib/gitlab_access_status.rb b/lib/gitlab_access_status.rb deleted file mode 100644 index 22e11a2..0000000 --- a/lib/gitlab_access_status.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'json' - -class GitAccessStatus - HTTP_MULTIPLE_CHOICES = '300'.freeze - - attr_reader :message, :gl_repository, :gl_project_path, :gl_id, :gl_username, - :gitaly, :git_protocol, :git_config_options, :payload, - :gl_console_messages - - def initialize(status, status_code, message, gl_repository: nil, - gl_project_path: nil, gl_id: nil, - gl_username: nil, gitaly: nil, git_protocol: nil, - git_config_options: nil, payload: nil, gl_console_messages: []) - @status = status - @status_code = status_code - @message = message - @gl_repository = gl_repository - @gl_project_path = gl_project_path - @gl_id = gl_id - @gl_username = gl_username - @git_config_options = git_config_options - @gitaly = gitaly - @git_protocol = git_protocol - @payload = payload - @gl_console_messages = gl_console_messages - end - - def self.create_from_json(json, status_code) - values = JSON.parse(json) - new(values["status"], - status_code, - values["message"], - gl_repository: values["gl_repository"], - gl_project_path: values["gl_project_path"], - gl_id: values["gl_id"], - gl_username: values["gl_username"], - git_config_options: values["git_config_options"], - gitaly: values["gitaly"], - git_protocol: values["git_protocol"], - payload: values["payload"], - gl_console_messages: values["gl_console_messages"]) - end - - def allowed? - @status - end - - def custom_action? - @status_code == HTTP_MULTIPLE_CHOICES - end -end diff --git a/lib/gitlab_config.rb b/lib/gitlab_config.rb deleted file mode 100644 index 85aa889..0000000 --- a/lib/gitlab_config.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'yaml' - -class GitlabConfig - attr_reader :config - - def initialize - @config = YAML.load_file(File.join(ROOT_PATH, 'config.yml')) - end - - def home - ENV['HOME'] - end - - def auth_file - @config['auth_file'] ||= File.join(home, ".ssh/authorized_keys") - end - - def secret_file - @config['secret_file'] ||= File.join(ROOT_PATH, '.gitlab_shell_secret') - end - - # Pass a default value because this is called from a repo's context; in which - # case, the repo's hooks directory should be the default. - # - def custom_hooks_dir(default: nil) - @config['custom_hooks_dir'] || default - end - - def gitlab_url - (@config['gitlab_url'] ||= "http://localhost:8080").sub(%r{/*$}, '') - end - - def http_settings - @config['http_settings'] ||= {} - end - - def log_file - @config['log_file'] ||= File.join(ROOT_PATH, 'gitlab-shell.log') - end - - def log_level - @config['log_level'] ||= 'INFO' - end - - def log_format - @config['log_format'] ||= 'text' - end - - def audit_usernames - @config['audit_usernames'] ||= false - end - - def metrics_log_file - @config['metrics_log_file'] ||= File.join(ROOT_PATH, 'gitlab-shell-metrics.log') - end -end diff --git a/lib/gitlab_init.rb b/lib/gitlab_init.rb deleted file mode 100644 index 9bd4e8f..0000000 --- a/lib/gitlab_init.rb +++ /dev/null @@ -1,7 +0,0 @@ -ROOT_PATH = ENV.fetch('GITLAB_SHELL_DIR', File.expand_path('..', __dir__)) - -# We are transitioning parts of gitlab-shell into the gitaly project. In -# gitaly, GITALY_EMBEDDED will be true. -GITALY_EMBEDDED = false - -require_relative 'gitlab_config' diff --git a/lib/gitlab_keys.rb b/lib/gitlab_keys.rb deleted file mode 100644 index 4742c45..0000000 --- a/lib/gitlab_keys.rb +++ /dev/null @@ -1,39 +0,0 @@ -module GitlabKeys - class KeyError < StandardError; end - - def self.command(whatever) - "#{ROOT_PATH}/bin/gitlab-shell #{whatever}" - end - - def self.command_key(key_id) - unless /\A[a-z0-9-]+\z/ =~ key_id - raise KeyError, "Invalid key_id: #{key_id.inspect}" - end - - command(key_id) - end - - def self.whatever_line(command, trailer) - "command=\"#{command}\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{trailer}" - end - - def self.key_line(key_id, public_key) - public_key.chomp! - - if public_key.include?("\n") - raise KeyError, "Invalid public_key: #{public_key.inspect}" - end - - whatever_line(command_key(key_id), public_key) - end - - def self.principal_line(username_key_id, principal) - principal.chomp! - - if principal.include?("\n") - raise KeyError, "Invalid principal: #{principal.inspect}" - end - - whatever_line(command_key(username_key_id), principal) - end -end diff --git a/lib/gitlab_lfs_authentication.rb b/lib/gitlab_lfs_authentication.rb deleted file mode 100644 index 574dc98..0000000 --- a/lib/gitlab_lfs_authentication.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'base64' -require 'json' - -class GitlabLfsAuthentication - # TODO: These don't need to be public - attr_accessor :username, :lfs_token, :repository_http_path - - def initialize(username, lfs_token, repository_http_path, expires_in = nil) - @username = username - @lfs_token = lfs_token - @repository_http_path = repository_http_path - @expires_in = expires_in - end - - def self.build_from_json(json) - values = JSON.parse(json) - new(values['username'], - values['lfs_token'], - values['repository_http_path'], - values['expires_in']) - rescue - nil - end - - # Source: https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md#ssh - # - def authentication_payload - payload = { header: { Authorization: authorization }, href: href } - payload[:expires_in] = @expires_in if @expires_in - - JSON.generate(payload) - end - - private - - def authorization - "Basic #{Base64.strict_encode64("#{username}:#{lfs_token}")}" - end - - def href - "#{repository_http_path}/info/lfs" - end -end diff --git a/lib/gitlab_logger.rb b/lib/gitlab_logger.rb deleted file mode 100644 index 67f6030..0000000 --- a/lib/gitlab_logger.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'json' -require 'logger' -require 'time' - -require_relative 'gitlab_config' - -def convert_log_level(log_level) - Logger.const_get(log_level.upcase) -rescue NameError - $stderr.puts "WARNING: Unrecognized log level #{log_level.inspect}." - $stderr.puts "WARNING: Falling back to INFO." - Logger::INFO -end - -class GitlabLogger - # Emulate the quoting logic of logrus - # https://github.com/sirupsen/logrus/blob/v1.0.5/text_formatter.go#L143-L156 - SHOULD_QUOTE = /[^a-zA-Z0-9\-._\/@^+]/ - - LEVELS = { - Logger::INFO => 'info'.freeze, - Logger::DEBUG => 'debug'.freeze, - Logger::WARN => 'warn'.freeze, - Logger::ERROR => 'error'.freeze - }.freeze - - def initialize(level, path, log_format) - @level = level - - @log_file = File.open(path, 'ab') - # By default Ruby will buffer writes. This is a problem when we exec - # into a new command before Ruby flushed its buffers. Setting 'sync' to - # true disables Ruby's buffering. - @log_file.sync = true - - @log_format = log_format - end - - def info(message, data = {}) - log_at(Logger::INFO, message, data) - end - - def debug(message, data = {}) - log_at(Logger::DEBUG, message, data) - end - - def warn(message, data = {}) - log_at(Logger::WARN, message, data) - end - - def error(message, data = {}) - log_at(Logger::ERROR, message, data) - end - - private - - attr_reader :log_file, :log_format - - def log_at(level, message, data) - return unless @level <= level - - data[:pid] = pid - data[:level] = LEVELS[level] - data[:msg] = message - - # Use RFC3339 to match logrus in the Go parts of gitlab-shell - data[:time] = time_now.to_datetime.rfc3339 - - case log_format - when 'json' - # Don't use IO#puts because of https://bugs.ruby-lang.org/issues/14042 - log_file.print("#{format_json(data)}\n") - else - log_file.print("#{format_text(data)}\n") - end - end - - def pid - Process.pid - end - - def time_now - Time.now - end - - def format_text(data) - # We start the line with these fields to match the behavior of logrus - result = [ - format_key_value(:time, data.delete(:time)), - format_key_value(:level, data.delete(:level)), - format_key_value(:msg, data.delete(:msg)) - ] - - data.sort.each { |k, v| result << format_key_value(k, v) } - result.join(' ') - end - - def format_key_value(key, value) - value_string = value.to_s - value_string = value_string.inspect if SHOULD_QUOTE =~ value_string - - "#{key}=#{value_string}" - end - - def format_json(data) - data.each do |key, value| - next unless value.is_a?(String) - - value = value.dup.force_encoding('utf-8') - value = value.inspect unless value.valid_encoding? - data[key] = value.freeze - end - - data.to_json - end -end - -config = GitlabConfig.new - -$logger = GitlabLogger.new(convert_log_level(config.log_level), config.log_file, config.log_format) diff --git a/lib/gitlab_metrics.rb b/lib/gitlab_metrics.rb deleted file mode 100644 index 917a489..0000000 --- a/lib/gitlab_metrics.rb +++ /dev/null @@ -1,59 +0,0 @@ -require_relative 'gitlab_config' -require_relative 'gitlab_logger' - -module GitlabMetrics - module System - # THREAD_CPUTIME is not supported on OS X - if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID) - def self.cpu_time - Process. - clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond) - end - else - def self.cpu_time - Process. - clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond) - end - end - - # Returns the current monotonic clock time in a given precision. - # - # Returns the time as a Fixnum. - def self.monotonic_time - if defined?(Process::CLOCK_MONOTONIC) - Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - else - Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) - end - end - end - - def self.logger - $logger - end - - # Measures the execution time of a block. - # - # Example: - # - # GitlabMetrics.measure(:find_by_username_duration) do - # User.find_by_username(some_username) - # end - # - # name - The name of the field to store the execution time in. - # - # Returns the value yielded by the supplied block. - def self.measure(name) - start_real = System.monotonic_time - start_cpu = System.cpu_time - - retval = yield - - real_time = System.monotonic_time - start_real - cpu_time = System.cpu_time - start_cpu - - logger.debug('metrics', name: name, wall_time: real_time, cpu_time: cpu_time) - - retval - end -end diff --git a/lib/gitlab_net.rb b/lib/gitlab_net.rb deleted file mode 100644 index 09767da..0000000 --- a/lib/gitlab_net.rb +++ /dev/null @@ -1,165 +0,0 @@ -require 'net/http' -require 'openssl' -require 'json' - -require_relative 'gitlab_config' -require_relative 'gitlab_access_status' -require_relative 'gitlab_lfs_authentication' -require_relative 'http_helper' - -class GitlabNet # rubocop:disable Metrics/ClassLength - include HTTPHelper - - CHECK_TIMEOUT = 5 - API_INACCESSIBLE_MESSAGE = 'API is not accessible'.freeze - - def check_access(cmd, gl_repository, repo, who, changes, protocol, env: {}) - changes = changes.join("\n") unless changes.is_a?(String) - - params = { - action: cmd, - changes: changes, - gl_repository: gl_repository, - project: sanitize_path(repo), - protocol: protocol, - env: env - } - - who_sym, _, who_v = self.class.parse_who(who) - params[who_sym] = who_v - - url = "#{internal_api_endpoint}/allowed" - resp = post(url, params) - - case resp - when Net::HTTPSuccess, Net::HTTPMultipleChoices, Net::HTTPUnauthorized, - Net::HTTPNotFound, Net::HTTPServiceUnavailable - if resp.content_type == CONTENT_TYPE_JSON - return GitAccessStatus.create_from_json(resp.body, resp.code) - end - end - - GitAccessStatus.new(false, resp.code, API_INACCESSIBLE_MESSAGE) - end - - def discover(who) - _, who_k, who_v = self.class.parse_who(who) - - resp = get("#{internal_api_endpoint}/discover?#{who_k}=#{who_v}") - - JSON.parse(resp.body) rescue nil - end - - def lfs_authenticate(gl_id, repo, operation) - id_sym, _, id = self.class.parse_who(gl_id) - params = { project: sanitize_path(repo), operation: operation } - - case id_sym - when :key_id - params[:key_id] = id - when :user_id - params[:user_id] = id - else - raise ArgumentError, "lfs_authenticate() got unsupported GL_ID='#{gl_id}'!" - end - - resp = post("#{internal_api_endpoint}/lfs_authenticate", params) - - GitlabLfsAuthentication.build_from_json(resp.body) if resp.code == '200' - end - - def broadcast_message - resp = get("#{internal_api_endpoint}/broadcast_message") - JSON.parse(resp.body) rescue {} - end - - def merge_request_urls(gl_repository, repo_path, changes) - changes = changes.join("\n") unless changes.is_a?(String) - changes = changes.encode('UTF-8', 'ASCII', invalid: :replace, replace: '') - url = "#{internal_api_endpoint}/merge_request_urls?project=#{URI.escape(repo_path)}&changes=#{URI.escape(changes)}" - url += "&gl_repository=#{URI.escape(gl_repository)}" if gl_repository - resp = get(url) - - if resp.code == '200' - JSON.parse(resp.body) - else - [] - end - rescue - [] - end - - def check - get("#{internal_api_endpoint}/check", options: { read_timeout: CHECK_TIMEOUT }) - end - - def authorized_key(key) - resp = get("#{internal_api_endpoint}/authorized_keys?key=#{URI.escape(key, '+/=')}") - JSON.parse(resp.body) if resp.code == "200" - rescue - nil - end - - def two_factor_recovery_codes(gl_id) - id_sym, _, id = self.class.parse_who(gl_id) - - resp = post("#{internal_api_endpoint}/two_factor_recovery_codes", id_sym => id) - - JSON.parse(resp.body) if resp.code == '200' - rescue - {} - end - - def notify_post_receive(gl_repository, repo_path) - params = { gl_repository: gl_repository, project: repo_path } - resp = post("#{internal_api_endpoint}/notify_post_receive", params) - - resp.code == '200' - rescue - false - end - - def post_receive(gl_repository, identifier, changes, push_options) - params = { - gl_repository: gl_repository, - identifier: identifier, - changes: changes, - :"push_options[]" => push_options, # rubocop:disable Style/HashSyntax - } - resp = post("#{internal_api_endpoint}/post_receive", params) - - raise NotFound if resp.code == '404' - - JSON.parse(resp.body) if resp.code == '200' - end - - def pre_receive(gl_repository) - resp = post("#{internal_api_endpoint}/pre_receive", gl_repository: gl_repository) - - raise NotFound if resp.code == '404' - - JSON.parse(resp.body) if resp.code == '200' - end - - def self.parse_who(who) - if who.start_with?("key-") - value = who.gsub("key-", "") - raise ArgumentError, "who='#{who}' is invalid!" unless value =~ /\A[0-9]+\z/ - [:key_id, 'key_id', value] - elsif who.start_with?("user-") - value = who.gsub("user-", "") - raise ArgumentError, "who='#{who}' is invalid!" unless value =~ /\A[0-9]+\z/ - [:user_id, 'user_id', value] - elsif who.start_with?("username-") - [:username, 'username', who.gsub("username-", "")] - else - raise ArgumentError, "who='#{who}' is invalid!" - end - end - - protected - - def sanitize_path(repo) - repo.delete("'") - end -end diff --git a/lib/gitlab_net/errors.rb b/lib/gitlab_net/errors.rb deleted file mode 100644 index f32b3d8..0000000 --- a/lib/gitlab_net/errors.rb +++ /dev/null @@ -1,4 +0,0 @@ -class GitlabNet - class ApiUnreachableError < StandardError; end - class NotFound < StandardError; end -end diff --git a/lib/gitlab_shell.rb b/lib/gitlab_shell.rb deleted file mode 100644 index 303f4d5..0000000 --- a/lib/gitlab_shell.rb +++ /dev/null @@ -1,277 +0,0 @@ -# frozen_string_literal: true - -require 'shellwords' -require 'pathname' - -require_relative 'gitlab_net' -require_relative 'gitlab_metrics' -require_relative 'action' -require_relative 'console_helper' - -class GitlabShell # rubocop:disable Metrics/ClassLength - include ConsoleHelper - - class AccessDeniedError < StandardError; end - class DisallowedCommandError < StandardError; end - class InvalidRepositoryPathError < StandardError; end - - GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack' - GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack' - GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive' - GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate' - - GITALY_COMMANDS = { - GIT_UPLOAD_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'), - GIT_UPLOAD_ARCHIVE_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'), - GIT_RECEIVE_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack') - }.freeze - - GIT_COMMANDS = (GITALY_COMMANDS.keys + [GIT_LFS_AUTHENTICATE_COMMAND]).freeze - TWO_FACTOR_RECOVERY_COMMAND = '2fa_recovery_codes' - GL_PROTOCOL = 'ssh' - - attr_accessor :gl_id, :gl_repository, :gl_project_path, :repo_name, :command, :git_access, :git_protocol - - def initialize(who) - who_sym, = GitlabNet.parse_who(who) - if who_sym == :username - @who = who - else - @gl_id = who - end - @config = GitlabConfig.new - end - - # The origin_cmd variable contains UNTRUSTED input. If the user ran - # ssh git@gitlab.example.com 'evil command', then origin_cmd contains - # 'evil command'. - def exec(origin_cmd) - unless origin_cmd - puts "Welcome to GitLab, #{username}!" - return true - end - - args = Shellwords.shellwords(origin_cmd) - args = parse_cmd(args) - - access_status = nil - - if GIT_COMMANDS.include?(args.first) - access_status = GitlabMetrics.measure('verify-access') { verify_access } - - @gl_repository = access_status.gl_repository - @git_protocol = ENV['GIT_PROTOCOL'] - @gl_project_path = access_status.gl_project_path - @gitaly = access_status.gitaly - @username = access_status.gl_username - @git_config_options = access_status.git_config_options - @gl_id = access_status.gl_id if defined?(@who) - - write_stderr(access_status.gl_console_messages) - elsif !defined?(@gl_id) - # We're processing an API command like 2fa_recovery_codes, but - # don't have a @gl_id yet, that means we're in the "username" - # mode and need to materialize it, calling the "user" method - # will do that and call the /discover method. - user - end - - if @command == GIT_RECEIVE_PACK_COMMAND && access_status.custom_action? - # If the response from /api/v4/allowed is a HTTP 300, we need to perform - # a Custom Action and therefore should return and not call process_cmd() - # - return process_custom_action(access_status) - end - - process_cmd(args) - - true - rescue GitlabNet::ApiUnreachableError - write_stderr('Failed to authorize your Git request: internal API unreachable') - false - rescue AccessDeniedError => ex - $logger.warn('Access denied', command: origin_cmd, user: log_username) - write_stderr(ex.message) - false - rescue DisallowedCommandError - $logger.warn('Denied disallowed command', command: origin_cmd, user: log_username) - write_stderr('Disallowed command') - false - rescue InvalidRepositoryPathError - write_stderr('Invalid repository path') - false - rescue Action::Custom::BaseError => ex - $logger.warn('Custom action error', exception: ex.class, message: ex.message, - command: origin_cmd, user: log_username) - $stderr.puts ex.message - false - end - - protected - - def parse_cmd(args) - # Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack - if args.length == 3 && args.first == 'git' - @command = "git-#{args[1]}" - args = [@command, args.last] - else - @command = args.first - end - - @git_access = @command - - return args if TWO_FACTOR_RECOVERY_COMMAND == @command - - raise DisallowedCommandError unless GIT_COMMANDS.include?(@command) - - case @command - when GIT_LFS_AUTHENTICATE_COMMAND - raise DisallowedCommandError unless args.count >= 2 - @repo_name = args[1] - case args[2] - when 'download' - @git_access = GIT_UPLOAD_PACK_COMMAND - when 'upload' - @git_access = GIT_RECEIVE_PACK_COMMAND - else - raise DisallowedCommandError - end - else - raise DisallowedCommandError unless args.count == 2 - @repo_name = args.last - end - - args - end - - def verify_access - status = api.check_access(@git_access, nil, @repo_name, @who || @gl_id, '_any', GL_PROTOCOL) - - raise AccessDeniedError, status.message unless status.allowed? - - status - end - - def process_custom_action(access_status) - Action::Custom.new(@gl_id, access_status.payload).execute - end - - def process_cmd(args) - return api_2fa_recovery_codes if TWO_FACTOR_RECOVERY_COMMAND == @command - - if @command == GIT_LFS_AUTHENTICATE_COMMAND - GitlabMetrics.measure('lfs-authenticate') do - operation = args[2] - $logger.info('Processing LFS authentication', operation: operation, user: log_username) - lfs_authenticate(operation) - end - return - end - - # TODO: instead of building from pieces here in gitlab-shell, build the - # entire gitaly_request in gitlab-ce and pass on as-is here. - args = JSON.dump( - 'repository' => @gitaly['repository'], - 'gl_repository' => @gl_repository, - 'gl_project_path' => @gl_project_path, - 'gl_id' => @gl_id, - 'gl_username' => @username, - 'git_config_options' => @git_config_options, - 'git_protocol' => @git_protocol - ) - - gitaly_address = @gitaly['address'] - executable = GITALY_COMMANDS.fetch(@command) - gitaly_bin = File.basename(executable) - args_string = [gitaly_bin, gitaly_address, args].join(' ') - $logger.info('executing git command', command: args_string, user: log_username) - - exec_cmd(executable, gitaly_address: gitaly_address, token: @gitaly['token'], json_args: args) - end - - # This method is not covered by Rspec because it ends the current Ruby process. - def exec_cmd(executable, gitaly_address:, token:, json_args:) - env = { 'GITALY_TOKEN' => token } - - args = [executable, gitaly_address, json_args] - # We use 'chdir: ROOT_PATH' to let the next executable know where config.yml is. - Kernel.exec(env, *args, unsetenv_others: true, chdir: ROOT_PATH) - end - - def api - GitlabNet.new - end - - def user - return @user if defined?(@user) - - begin - if defined?(@who) - @user = api.discover(@who) - @gl_id = "user-#{@user['id']}" if @user && @user.key?('id') - else - @user = api.discover(@gl_id) - end - rescue GitlabNet::ApiUnreachableError - @user = nil - end - end - - def username_from_discover - return nil unless user && user['username'] - - "@#{user['username']}" - end - - def username - @username ||= username_from_discover || 'Anonymous' - end - - # User identifier to be used in log messages. - def log_username - @config.audit_usernames ? username : "user with id #{@gl_id}" - end - - def lfs_authenticate(operation) - lfs_access = api.lfs_authenticate(@gl_id, @repo_name, operation) - - return unless lfs_access - - puts lfs_access.authentication_payload - end - - private - - def continue?(question) - puts "#{question} (yes/no)" - STDOUT.flush # Make sure the question gets output before we wait for input - continue = STDIN.gets.chomp - puts '' # Add a buffer in the output - continue == 'yes' - end - - def api_2fa_recovery_codes - continue = continue?( - "Are you sure you want to generate new two-factor recovery codes?\n" \ - "Any existing recovery codes you saved will be invalidated." - ) - - unless continue - puts 'New recovery codes have *not* been generated. Existing codes will remain valid.' - return - end - - resp = api.two_factor_recovery_codes(@gl_id) - if resp['success'] - codes = resp['recovery_codes'].join("\n") - puts "Your two-factor authentication recovery codes are:\n\n" \ - "#{codes}\n\n" \ - "During sign in, use one of the codes above when prompted for\n" \ - "your two-factor code. Then, visit your Profile Settings and add\n" \ - "a new device so you do not lose access to your account again." - else - puts "An error occurred while trying to generate new recovery codes.\n" \ - "#{resp['message']}" - end - end -end diff --git a/lib/hooks_utils.rb b/lib/hooks_utils.rb deleted file mode 100644 index b11317c..0000000 --- a/lib/hooks_utils.rb +++ /dev/null @@ -1,15 +0,0 @@ -module HooksUtils - module_function - - # Gets an array of Git push options from the environment - def get_push_options - count = ENV['GIT_PUSH_OPTION_COUNT'].to_i - result = [] - - count.times do |i| - result.push(ENV["GIT_PUSH_OPTION_#{i}"]) - end - - result - end -end diff --git a/lib/http_helper.rb b/lib/http_helper.rb deleted file mode 100644 index c6a4bb8..0000000 --- a/lib/http_helper.rb +++ /dev/null @@ -1,125 +0,0 @@ -require_relative 'httpunix' -require_relative 'gitlab_logger' -require_relative 'gitlab_net/errors' - -module HTTPHelper - READ_TIMEOUT = 300 - CONTENT_TYPE_JSON = 'application/json'.freeze - - protected - - def config - @config ||= GitlabConfig.new - end - - def base_api_endpoint - "#{config.gitlab_url}/api/v4" - end - - def internal_api_endpoint - "#{base_api_endpoint}/internal" - end - - def http_client_for(uri, options = {}) - http = if uri.is_a?(URI::HTTPUNIX) - Net::HTTPUNIX.new(uri.hostname) - else - Net::HTTP.new(uri.host, uri.port) - end - - http.read_timeout = options[:read_timeout] || read_timeout - - if uri.is_a?(URI::HTTPS) - http.use_ssl = true - http.cert_store = cert_store - http.verify_mode = OpenSSL::SSL::VERIFY_NONE if config.http_settings['self_signed_cert'] - end - - http - end - - def http_request_for(method, uri, params: {}, headers: {}, options: {}) - request_klass = method == :get ? Net::HTTP::Get : Net::HTTP::Post - request = request_klass.new(uri.request_uri, headers) - - user = config.http_settings['user'] - password = config.http_settings['password'] - request.basic_auth(user, password) if user && password - - if options[:json] - request.body = options[:json].merge(secret_token: secret_token).to_json - else - request.set_form_data(params.merge(secret_token: secret_token)) - end - - if uri.is_a?(URI::HTTPUNIX) - # The HTTPUNIX HTTP client does not set a correct Host header. This can - # lead to 400 Bad Request responses. - request['Host'] = 'localhost' - end - - request - end - - def request(method, url, params: {}, headers: {}, options: {}) - $logger.debug('Performing request', method: method.to_s.upcase, url: url) - - uri = URI.parse(url) - http = http_client_for(uri, options) - request = http_request_for(method, uri, - params: params, - headers: headers, - options: options) - - begin - start_time = Time.new - response = http.start { http.request(request) } - rescue => e - $logger.warn('Failed to connect', method: method.to_s.upcase, url: url, error: e) - raise GitlabNet::ApiUnreachableError - ensure - fields = { method: method.to_s.upcase, url: url, duration: Time.new - start_time, gitaly_embedded: GITALY_EMBEDDED } - $logger.info('finished HTTP request', fields) - end - - case response - when Net::HTTPSuccess, Net::HTTPMultipleChoices - $logger.debug('Received response', code: response.code, body: response.body) - else - $logger.error('Call failed', method: method.to_s.upcase, url: url, code: response.code, body: response.body) - end - - response - end - - def get(url, headers: {}, options: {}) - request(:get, url, headers: headers, options: options) - end - - def post(url, params, headers: {}, options: {}) - request(:post, url, params: params, headers: headers, options: options) - end - - def cert_store - @cert_store ||= begin - store = OpenSSL::X509::Store.new - store.set_default_paths - - ca_file = config.http_settings['ca_file'] - store.add_file(ca_file) if ca_file - - ca_path = config.http_settings['ca_path'] - store.add_path(ca_path) if ca_path - - store - end - end - - def secret_token - @secret_token ||= File.read config.secret_file - end - - def read_timeout - config.http_settings['read_timeout'] || READ_TIMEOUT - end -end diff --git a/lib/httpunix.rb b/lib/httpunix.rb deleted file mode 100644 index 7d00f71..0000000 --- a/lib/httpunix.rb +++ /dev/null @@ -1,54 +0,0 @@ -# support for http+unix://... connection scheme -# -# The URI scheme has the same structure as the similar one for python requests. See: -# http://fixall.online/theres-no-need-to-reinvent-the-wheelhttpsgithubcommsabramorequests-unixsocketurl/241810/ -# https://github.com/msabramo/requests-unixsocket - -require 'uri' -require 'net/http' - -module URI - class HTTPUNIX < HTTP - def hostname - # decode %XX from path to file - v = host - URI.decode(v) - end - - # port is not allowed in URI - DEFAULT_PORT = nil - def set_port(v) - return v unless v - raise InvalidURIError, "http+unix:// cannot contain port" - end - end - @@schemes['HTTP+UNIX'] = HTTPUNIX -end - -# Based on: -# - http://stackoverflow.com/questions/15637226/ruby-1-9-3-simple-get-request-to-unicorn-through-socket -# - Net::HTTP::connect -module Net - class HTTPUNIX < HTTP - def initialize(socketpath, port = nil) - super(socketpath, port) - @port = nil # HTTP will set it to default - override back -> set DEFAULT_PORT - end - - # override to prevent ":<port>" being appended to HTTP_HOST - def addr_port - address - end - - def connect - D "opening connection to #{address} ..." - s = UNIXSocket.new(address) - D "opened" - @socket = BufferedIO.new(s) - @socket.read_timeout = @read_timeout - @socket.continue_timeout = @continue_timeout - @socket.debug_output = @debug_output - on_connect - end - end -end diff --git a/lib/object_dirs_helper.rb b/lib/object_dirs_helper.rb deleted file mode 100644 index e175a03..0000000 --- a/lib/object_dirs_helper.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'pathname' - -class ObjectDirsHelper - class << self - def all_attributes - { - "GIT_ALTERNATE_OBJECT_DIRECTORIES" => absolute_alt_object_dirs, - "GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE" => relative_alt_object_dirs, - "GIT_OBJECT_DIRECTORY" => absolute_object_dir, - "GIT_OBJECT_DIRECTORY_RELATIVE" => relative_object_dir - } - end - - def absolute_object_dir - ENV['GIT_OBJECT_DIRECTORY'] - end - - def relative_object_dir - relative_path(absolute_object_dir) - end - - def absolute_alt_object_dirs - ENV['GIT_ALTERNATE_OBJECT_DIRECTORIES'].to_s.split(File::PATH_SEPARATOR) - end - - def relative_alt_object_dirs - absolute_alt_object_dirs.map { |dir| relative_path(dir) }.compact - end - - private - - def relative_path(absolute_path) - return if absolute_path.nil? - - repo_dir = Dir.pwd - Pathname.new(absolute_path).relative_path_from(Pathname.new(repo_dir)).to_s - end - end -end |