diff options
-rw-r--r-- | .gitlab-ci.yml | 7 | ||||
-rw-r--r-- | CHANGELOG | 20 | ||||
-rw-r--r-- | VERSION | 2 | ||||
-rwxr-xr-x | bin/gitlab-shell | 2 | ||||
-rw-r--r-- | config.yml.example | 10 | ||||
-rw-r--r-- | lib/gitlab_config.rb | 2 | ||||
-rw-r--r-- | lib/gitlab_net.rb | 7 | ||||
-rw-r--r-- | lib/gitlab_projects.rb | 43 | ||||
-rw-r--r-- | lib/gitlab_shell.rb | 35 | ||||
-rw-r--r-- | lib/httpunix.rb | 54 | ||||
-rw-r--r-- | spec/gitlab_config_spec.rb | 6 | ||||
-rw-r--r-- | spec/gitlab_shell_spec.rb | 57 | ||||
-rw-r--r-- | spec/httpunix_spec.rb | 55 |
13 files changed, 239 insertions, 61 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fa4f2d4..784e8d5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,8 @@ before_script: - - export PATH=~/bin:/usr/local/bin:/usr/bin:/bin - - gem install bundler + - export PATH=~/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + - apt-get update + - apt-get install -y git-annex + - gem install --bindir /usr/local/bin bundler - cp config.yml.example config.yml - bundle install @@ -8,7 +10,6 @@ rspec: script: - bundle exec rspec spec tags: - - git-annex - ruby except: - tags @@ -1,4 +1,14 @@ -v2.6.6 (unreleased) +v2.6.9 + - Remove trailing slashes from gitlab_url + +v2.6.8 + - Revert git-lfs-authenticate command from white list + +v2.6.7 + - Exit with non-zero status when import-repository fails + - Add fetch-remote command + +v2.6.6 - Do not clean LANG environment variable for the git hooks when working through the SSH-protocol - Add git-lfs-authenticate command to white list (this command is used by git-lfs for SSO authentication through SSH-protocol) - Handle git-annex and gcryptsetup @@ -59,13 +69,13 @@ v2.1.0 - Use secret token with GitLab internal API. Requires GitLab 7.5 or higher v2.0.1 - - Send post-receive changes to redis as a string instead of array + - Send post-receive changes to redis as a string instead of array v2.0.0 - Works with GitLab v7.3+ - Replace raise with abort when checking path to prevent path exposure - Handle invalid number of arguments on remote commands - - Replace update hook with pre-receive and post-receive hooks. + - Replace update hook with pre-receive and post-receive hooks. - Symlink the whole hooks directory - Ignore missing repositories in create-hooks - Connect to Redis via sockets by default @@ -89,10 +99,10 @@ v1.9.3 - Ignore force push detection for new branch or branch remove push v1.9.2 - - Add support for force push detection + - Add support for force push detection v1.9.1 - - Update hook sends branch and tag name + - Update hook sends branch and tag name v1.9.0 - Call api in update hook for both ssdh and http push. Requires GitLab 6.7+ @@ -1 +1 @@ -2.6.5 +2.6.9 diff --git a/bin/gitlab-shell b/bin/gitlab-shell index 084f0c9..f145a1b 100755 --- a/bin/gitlab-shell +++ b/bin/gitlab-shell @@ -17,7 +17,7 @@ require_relative '../lib/gitlab_init' # require File.join(ROOT_PATH, 'lib', 'gitlab_shell') -if GitlabShell.new(key_id, original_cmd).exec +if GitlabShell.new(key_id).exec(original_cmd) exit 0 else exit 1 diff --git a/config.yml.example b/config.yml.example index 43d6e85..a7e8d8a 100644 --- a/config.yml.example +++ b/config.yml.example @@ -6,12 +6,14 @@ # GitLab user. git by default user: git -# Url to gitlab instance. Used for api calls. Should end with a slash. -# Default: http://localhost:8080/ +# Url to gitlab instance. Used for api calls. +# Default: http://localhost:8080 # You only have to change the default if you have configured Unicorn # to listen on a custom port, or if you have configured Unicorn to -# only listen on a Unix domain socket. -gitlab_url: "http://localhost:8080/" +# only listen on a Unix domain socket. For Unix domain sockets use +# "http+unix://<urlquoted-path-to-socket>", e.g. +# "http+unix://%2Fpath%2Fto%2Fsocket" +gitlab_url: "http://localhost:8080" # See installation.md#using-https for additional HTTPS configuration details. http_settings: diff --git a/lib/gitlab_config.rb b/lib/gitlab_config.rb index caca176..831f0e3 100644 --- a/lib/gitlab_config.rb +++ b/lib/gitlab_config.rb @@ -24,7 +24,7 @@ class GitlabConfig end def gitlab_url - @config['gitlab_url'] ||= "http://localhost/" + (@config['gitlab_url'] ||= "http://localhost:8080").sub(%r{/*$}, '') end def http_settings diff --git a/lib/gitlab_net.rb b/lib/gitlab_net.rb index 8eb63ae..6f47938 100644 --- a/lib/gitlab_net.rb +++ b/lib/gitlab_net.rb @@ -5,6 +5,7 @@ require 'json' require_relative 'gitlab_config' require_relative 'gitlab_logger' require_relative 'gitlab_access' +require_relative 'httpunix' class GitlabNet class ApiUnreachableError < StandardError; end @@ -63,7 +64,11 @@ class GitlabNet end def http_client_for(uri) - http = Net::HTTP.new(uri.host, uri.port) + if uri.is_a?(URI::HTTPUNIX) + http = Net::HTTPUNIX.new(uri.hostname) + else + http = Net::HTTP.new(uri.host, uri.port) + end if uri.is_a?(URI::HTTPS) http.use_ssl = true diff --git a/lib/gitlab_projects.rb b/lib/gitlab_projects.rb index 461819a..c1d175a 100644 --- a/lib/gitlab_projects.rb +++ b/lib/gitlab_projects.rb @@ -59,7 +59,8 @@ class GitlabProjects when 'mv-project'; mv_project when 'import-project'; import_project when 'fork-project'; fork_project - when 'update-head'; update_head + when 'fetch-remote'; fetch_remote + when 'update-head'; update_head when 'gc'; gc else $logger.warn "Attempt to execute invalid gitlab-projects command #{@command.inspect}." @@ -129,6 +130,30 @@ class GitlabProjects url end + def fetch_remote + @name = ARGV.shift + + # timeout for fetch + timeout = (ARGV.shift || 120).to_i + $logger.info "Fetching remote #{@name} for project #{@project_name}." + cmd = %W(git --git-dir=#{full_path} fetch #{@name} --tags) + pid = Process.spawn(*cmd) + + begin + Timeout.timeout(timeout) do + Process.wait(pid) + end + + $?.exitstatus.zero? + rescue Timeout::Error + $logger.error "Fetching remote #{@name} for project #{@project_name} failed due to timeout." + + Process.kill('KILL', pid) + Process.wait + false + end + end + def remove_origin_in_repo cmd = %W(git --git-dir=#{full_path} remote rm origin) pid = Process.spawn(*cmd) @@ -155,19 +180,23 @@ class GitlabProjects Timeout.timeout(timeout) do Process.wait(pid) end + + return false unless $?.exitstatus.zero? rescue Timeout::Error $logger.error "Importing project #{@project_name} from <#{masked_source}> failed due to timeout." Process.kill('KILL', pid) Process.wait FileUtils.rm_rf(full_path) - false - else - self.class.create_hooks(full_path) - # The project was imported successfully. - # Remove the origin URL since it may contain password. - remove_origin_in_repo + return false end + + self.class.create_hooks(full_path) + # The project was imported successfully. + # Remove the origin URL since it may contain password. + remove_origin_in_repo + + true end # Move repository from one directory to another diff --git a/lib/gitlab_shell.rb b/lib/gitlab_shell.rb index 4bc1cd7..96ee1b7 100644 --- a/lib/gitlab_shell.rb +++ b/lib/gitlab_shell.rb @@ -11,37 +11,40 @@ class GitlabShell attr_accessor :key_id, :repo_name, :git_cmd, :repos_path, :repo_name - def initialize(key_id, origin_cmd) + def initialize(key_id) @key_id = key_id - @origin_cmd = origin_cmd @config = GitlabConfig.new @repos_path = @config.repos_path end - def exec - unless @origin_cmd + # 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 - parse_cmd + args = Shellwords.shellwords(origin_cmd) + parse_cmd(args) verify_access - process_cmd + process_cmd(args) true rescue GitlabNet::ApiUnreachableError => ex $stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable" false rescue AccessDeniedError => ex - message = "gitlab-shell: Access denied for git command <#{@origin_cmd}> by #{log_username}." + message = "gitlab-shell: Access denied for git command <#{origin_cmd}> by #{log_username}." $logger.warn message $stderr.puts "GitLab: #{ex.message}" false rescue DisallowedCommandError => ex - message = "gitlab-shell: Attempt to execute disallowed command <#{@origin_cmd}> by #{log_username}." + message = "gitlab-shell: Attempt to execute disallowed command <#{origin_cmd}> by #{log_username}." $logger.warn message $stderr.puts "GitLab: Disallowed command" @@ -53,8 +56,7 @@ class GitlabShell protected - def parse_cmd - args = Shellwords.shellwords(@origin_cmd) + def parse_cmd(args) @git_cmd = args.first @git_access = @git_cmd @@ -91,13 +93,12 @@ class GitlabShell raise AccessDeniedError, status.message unless status.allowed? end - def process_cmd + def process_cmd(args) repo_full_path = File.join(repos_path, repo_name) if @git_cmd == 'git-annex-shell' raise DisallowedCommandError unless @config.git_annex_enabled? - args = Shellwords.shellwords(@origin_cmd) parsed_args = args.map do |arg| # Convert /~/group/project.git to group/project.git @@ -111,8 +112,6 @@ class GitlabShell $logger.info "gitlab-shell: executing git-annex command <#{parsed_args.join(' ')}> for #{log_username}." exec_cmd(*parsed_args) - elsif @git_cmd == 'git-lfs-authenticate' - exec_cmd(@origin_cmd) else $logger.info "gitlab-shell: executing git command <#{@git_cmd} #{repo_full_path}> for #{log_username}." exec_cmd(@git_cmd, repo_full_path) @@ -121,7 +120,15 @@ class GitlabShell # This method is not covered by Rspec because it ends the current Ruby process. def exec_cmd(*args) + # If you want to call a command without arguments, use + # exec_cmd(['my_command', 'my_command']) . Otherwise use + # exec_cmd('my_command', 'my_argument', ...). + if args.count == 1 && !args.first.is_a?(Array) + raise DisallowedCommandError + end + env = { + 'HOME' => ENV['HOME'], 'PATH' => ENV['PATH'], 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'], 'LANG' => ENV['LANG'], diff --git a/lib/httpunix.rb b/lib/httpunix.rb new file mode 100644 index 0000000..12787ee --- /dev/null +++ b/lib/httpunix.rb @@ -0,0 +1,54 @@ +# 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 = self.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/spec/gitlab_config_spec.rb b/spec/gitlab_config_spec.rb index 52fb182..0ce641b 100644 --- a/spec/gitlab_config_spec.rb +++ b/spec/gitlab_config_spec.rb @@ -35,6 +35,12 @@ eos it { should_not be_empty } it { should eq(url) } + + context 'remove trailing slashes' do + before { config.send(:config)['gitlab_url'] = url + '//' } + + it { should eq(url) } + end end describe :audit_usernames do diff --git a/spec/gitlab_shell_spec.rb b/spec/gitlab_shell_spec.rb index 62e0d36..86d72f4 100644 --- a/spec/gitlab_shell_spec.rb +++ b/spec/gitlab_shell_spec.rb @@ -13,7 +13,7 @@ describe GitlabShell do subject do ARGV[0] = key_id - GitlabShell.new(key_id, ssh_cmd).tap do |shell| + GitlabShell.new(key_id).tap do |shell| shell.stub(exec_cmd: :exec_called) shell.stub(api: api) end @@ -44,10 +44,10 @@ describe GitlabShell do describe :parse_cmd do describe 'git' do context 'w/o namespace' do - let(:ssh_cmd) { 'git-upload-pack gitlab-ci.git' } + let(:ssh_args) { %W(git-upload-pack gitlab-ci.git) } before do - subject.send :parse_cmd + subject.send :parse_cmd, ssh_args end its(:repo_name) { should == 'gitlab-ci.git' } @@ -55,10 +55,10 @@ describe GitlabShell do end context 'namespace' do - let(:ssh_cmd) { 'git-upload-pack dmitriy.zaporozhets/gitlab-ci.git' } + let(:ssh_args) { %W(git-upload-pack dmitriy.zaporozhets/gitlab-ci.git) } before do - subject.send :parse_cmd + subject.send :parse_cmd, ssh_args end its(:repo_name) { should == 'dmitriy.zaporozhets/gitlab-ci.git' } @@ -66,10 +66,10 @@ describe GitlabShell do end context 'with an invalid number of arguments' do - let(:ssh_cmd) { 'foobar' } + let(:ssh_args) { %W(foobar) } it "should raise an DisallowedCommandError" do - expect { subject.send :parse_cmd }.to raise_error(GitlabShell::DisallowedCommandError) + expect { subject.send :parse_cmd, ssh_args }.to raise_error(GitlabShell::DisallowedCommandError) end end end @@ -77,7 +77,7 @@ describe GitlabShell do describe 'git-annex' do let(:repo_path) { File.join(tmp_repos_path, 'dzaporozhets/gitlab.git') } - let(:ssh_cmd) { 'git-annex-shell inannex /~/dzaporozhets/gitlab.git SHA256E' } + let(:ssh_args) { %W(git-annex-shell inannex /~/dzaporozhets/gitlab.git SHA256E) } before do GitlabConfig.any_instance.stub(git_annex_enabled?: true) @@ -87,7 +87,7 @@ describe GitlabShell do cmd = %W(git --git-dir=#{repo_path} init --bare) system(*cmd) - subject.send :parse_cmd + subject.send :parse_cmd, ssh_args end its(:repo_name) { should == 'dzaporozhets/gitlab.git' } @@ -98,7 +98,7 @@ describe GitlabShell do end context 'with git-annex-shell gcryptsetup' do - let(:ssh_cmd) { 'git-annex-shell gcryptsetup /~/dzaporozhets/gitlab.git' } + let(:ssh_args) { %W(git-annex-shell gcryptsetup /~/dzaporozhets/gitlab.git) } it 'should not init git-annex' do File.exists?(File.join(tmp_repos_path, 'dzaporozhets/gitlab.git/annex')).should be_false @@ -110,10 +110,10 @@ describe GitlabShell do describe :exec do context 'git-upload-pack' do let(:ssh_cmd) { 'git-upload-pack gitlab-ci.git' } - after { subject.exec } + after { subject.exec(ssh_cmd) } it "should process the command" do - subject.should_receive(:process_cmd).with() + subject.should_receive(:process_cmd).with(%W(git-upload-pack gitlab-ci.git)) end it "should execute the command" do @@ -135,10 +135,10 @@ describe GitlabShell do context 'git-receive-pack' do let(:ssh_cmd) { 'git-receive-pack gitlab-ci.git' } - after { subject.exec } + after { subject.exec(ssh_cmd) } it "should process the command" do - subject.should_receive(:process_cmd).with() + subject.should_receive(:process_cmd).with(%W(git-receive-pack gitlab-ci.git)) end it "should execute the command" do @@ -155,7 +155,7 @@ describe GitlabShell do context 'arbitrary command' do let(:ssh_cmd) { 'arbitrary command' } - after { subject.exec } + after { subject.exec(ssh_cmd) } it "should not process the command" do subject.should_not_receive(:process_cmd) @@ -172,7 +172,7 @@ describe GitlabShell do end context 'no command' do - after { subject.exec } + after { subject.exec(nil) } it "should call api.discover" do api.should_receive(:discover).with(key_id) @@ -185,7 +185,7 @@ describe GitlabShell do before { api.stub(:check_access).and_raise(GitlabNet::ApiUnreachableError) } - after { subject.exec } + after { subject.exec(ssh_cmd) } it "should not process the command" do subject.should_not_receive(:process_cmd) @@ -203,7 +203,7 @@ describe GitlabShell do GitlabConfig.any_instance.stub(git_annex_enabled?: true) end - after { subject.exec } + after { subject.exec(ssh_cmd) } it "should execute the command" do subject.should_receive(:exec_cmd).with("git-annex-shell", "commit", File.join(tmp_repos_path, 'gitlab-ci.git'), "SHA256") @@ -213,7 +213,7 @@ describe GitlabShell do describe :validate_access do let(:ssh_cmd) { 'git-upload-pack gitlab-ci.git' } - after { subject.exec } + after { subject.exec(ssh_cmd) } it "should call api.check_access" do api.should_receive(:check_access). @@ -229,24 +229,33 @@ describe GitlabShell do end describe :exec_cmd do - let(:shell) { GitlabShell.new(key_id, ssh_cmd) } + let(:shell) { GitlabShell.new(key_id) } before { Kernel.stub!(:exec) } it "uses Kernel::exec method" do - Kernel.should_receive(:exec).with(kind_of(Hash), 1, unsetenv_others: true).once - shell.send :exec_cmd, 1 + Kernel.should_receive(:exec).with(kind_of(Hash), 1, 2, unsetenv_others: true).once + shell.send :exec_cmd, 1, 2 + end + + it "refuses to execute a lone non-array argument" do + expect { shell.send :exec_cmd, 1 }.to raise_error(GitlabShell::DisallowedCommandError) + end + + it "allows one argument if it is an array" do + Kernel.should_receive(:exec).with(kind_of(Hash), [1, 2], unsetenv_others: true).once + shell.send :exec_cmd, [1, 2] end end describe :api do - let(:shell) { GitlabShell.new(key_id, ssh_cmd) } + let(:shell) { GitlabShell.new(key_id) } subject { shell.send :api } it { should be_a(GitlabNet) } end describe :escape_path do - let(:shell) { GitlabShell.new(key_id, ssh_cmd) } + let(:shell) { GitlabShell.new(key_id) } before { File.stub(:absolute_path) { 'y' } } subject { -> { shell.send(:escape_path, 'z') } } diff --git a/spec/httpunix_spec.rb b/spec/httpunix_spec.rb new file mode 100644 index 0000000..cd2ede9 --- /dev/null +++ b/spec/httpunix_spec.rb @@ -0,0 +1,55 @@ +require_relative 'spec_helper' +require_relative '../lib/httpunix' +require 'webrick' + +describe URI::HTTPUNIX do + describe :parse do + uri = URI::parse('http+unix://%2Fpath%2Fto%2Fsocket/img.jpg') + subject { uri } + + it { should be_an_instance_of(URI::HTTPUNIX) } + its(:scheme) { should eq('http+unix') } + its(:hostname) { should eq('/path/to/socket') } + its(:path) { should eq('/img.jpg') } + end +end + + +# like WEBrick::HTTPServer, but listens on UNIX socket +class HTTPUNIXServer < WEBrick::HTTPServer + def listen(address, port) + socket = Socket.unix_server_socket(address) + socket.autoclose = false + server = UNIXServer.for_fd(socket.fileno) + socket.close + @listeners << server + end +end + +def tmp_socket_path + File.join(ROOT_PATH, 'tmp', 'socket') +end + +describe Net::HTTPUNIX do + # "hello world" over unix socket server in background thread + FileUtils.mkdir_p(File.dirname(tmp_socket_path)) + server = HTTPUNIXServer.new(:BindAddress => tmp_socket_path) + server.mount_proc '/' do |req, resp| + resp.body = "Hello World (at #{req.path})" + end + Thread.start { server.start } + + it "talks via HTTP ok" do + VCR.turned_off do + begin + WebMock.allow_net_connect! + http = Net::HTTPUNIX.new(tmp_socket_path) + expect(http.get('/').body).to eq('Hello World (at /)') + expect(http.get('/path').body).to eq('Hello World (at /path)') + + ensure + WebMock.disable_net_connect! + end + end + end +end |