summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--CHANGELOG20
-rw-r--r--VERSION2
-rwxr-xr-xbin/gitlab-shell2
-rw-r--r--config.yml.example10
-rw-r--r--lib/gitlab_config.rb2
-rw-r--r--lib/gitlab_net.rb7
-rw-r--r--lib/gitlab_projects.rb43
-rw-r--r--lib/gitlab_shell.rb35
-rw-r--r--lib/httpunix.rb54
-rw-r--r--spec/gitlab_config_spec.rb6
-rw-r--r--spec/gitlab_shell_spec.rb57
-rw-r--r--spec/httpunix_spec.rb55
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
diff --git a/CHANGELOG b/CHANGELOG
index d82ccc6..75617dd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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+
diff --git a/VERSION b/VERSION
index 57cf282..d48d370 100644
--- a/VERSION
+++ b/VERSION
@@ -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