summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/action.rb4
-rw-r--r--lib/action/custom.rb146
-rw-r--r--lib/console_helper.rb17
-rw-r--r--lib/gitlab_access_status.rb51
-rw-r--r--lib/gitlab_config.rb56
-rw-r--r--lib/gitlab_init.rb7
-rw-r--r--lib/gitlab_keys.rb39
-rw-r--r--lib/gitlab_lfs_authentication.rb43
-rw-r--r--lib/gitlab_logger.rb120
-rw-r--r--lib/gitlab_metrics.rb59
-rw-r--r--lib/gitlab_net.rb165
-rw-r--r--lib/gitlab_net/errors.rb4
-rw-r--r--lib/gitlab_shell.rb277
-rw-r--r--lib/hooks_utils.rb15
-rw-r--r--lib/http_helper.rb125
-rw-r--r--lib/httpunix.rb54
-rw-r--r--lib/object_dirs_helper.rb39
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