diff --git a/.travis.yml b/.travis.yml index 05d84ef14..1bd9a17d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,15 +16,17 @@ matrix: - rvm: rbx-19mode - rvm: rbx-20mode -# We need the config so the tests don't fail -script: - - git config --global user.name 'The rugged tests are fragile' - - bundle exec rake +install: + - bundle install --deployment + - sudo apt-get -qq update + - sudo apt-get -qq install cmake libssh2-1-dev openssh-client openssh-server + +script: script/cibuild # Notify development list when needed notifications: - recipients: - - vicent@github.com - email: - on_success: change - on_failure: always + recipients: + - vicent@github.com + email: + on_success: change + on_failure: always diff --git a/ext/rugged/rugged.c b/ext/rugged/rugged.c index 289711116..fe24a0562 100644 --- a/ext/rugged/rugged.c +++ b/ext/rugged/rugged.c @@ -408,6 +408,7 @@ void Init_rugged(void) Init_rugged_diff_hunk(); Init_rugged_diff_line(); Init_rugged_blame(); + Init_rugged_cred(); /* * Sort the repository contents in no particular ordering; diff --git a/ext/rugged/rugged.h b/ext/rugged/rugged.h index 322f9b7fb..1a75d252f 100644 --- a/ext/rugged/rugged.h +++ b/ext/rugged/rugged.h @@ -67,6 +67,7 @@ void Init_rugged_diff_delta(void); void Init_rugged_diff_hunk(void); void Init_rugged_diff_line(void); void Init_rugged_blame(void); +void Init_rugged_cred(void); VALUE rb_git_object_init(git_otype type, int argc, VALUE *argv, VALUE self); diff --git a/ext/rugged/rugged_cred.c b/ext/rugged/rugged_cred.c new file mode 100644 index 000000000..727bd287c --- /dev/null +++ b/ext/rugged/rugged_cred.c @@ -0,0 +1,40 @@ +/* + * The MIT License + * + * Copyright (c) 2012 GitHub, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "rugged.h" + +extern VALUE rb_mRugged; +VALUE rb_mRuggedCred; +VALUE rb_cRuggedCredPlaintext; +VALUE rb_cRuggedCredSshKey; +VALUE rb_cRuggedCredDefault; + +void Init_rugged_cred(void) +{ + rb_mRuggedCred = rb_define_module_under(rb_mRugged, "Credentials"); + + rb_cRuggedCredPlaintext = rb_define_class_under(rb_mRuggedCred, "Plaintext", rb_cObject); + rb_cRuggedCredSshKey = rb_define_class_under(rb_mRuggedCred, "SshKey", rb_cObject); + rb_cRuggedCredDefault = rb_define_class_under(rb_mRuggedCred, "Default", rb_cObject); +} diff --git a/ext/rugged/rugged_repo.c b/ext/rugged/rugged_repo.c index 7654695c8..1c17e3afc 100644 --- a/ext/rugged/rugged_repo.c +++ b/ext/rugged/rugged_repo.c @@ -36,6 +36,10 @@ extern VALUE rb_cRuggedTag; extern VALUE rb_cRuggedTree; extern VALUE rb_cRuggedReference; +extern VALUE rb_cRuggedCredPlaintext; +extern VALUE rb_cRuggedCredSshKey; +extern VALUE rb_cRuggedCredDefault; + VALUE rb_cRuggedRepo; VALUE rb_cRuggedOdbObject; @@ -309,6 +313,7 @@ struct rugged_remote_cb_payload VALUE completion; VALUE transfer_progress; VALUE update_tips; + VALUE credentials; int exception; }; @@ -326,35 +331,169 @@ static int rugged__remote_transfer_progress_cb(const git_transfer_progress *stat return remote_payload->exception ? GIT_ERROR : GIT_OK; } +struct extract_cred_payload +{ + VALUE rb_cred; + git_cred **cred; + unsigned int allowed_types; +}; + +static VALUE rugged__extract_cred(VALUE payload) { + struct extract_cred_payload *cred_payload = (struct extract_cred_payload*)payload; + git_cred **cred = cred_payload->cred; + VALUE rb_cred = cred_payload->rb_cred; + + if (rb_obj_is_kind_of(rb_cred, rb_cRuggedCredPlaintext)) { + if (!(cred_payload->allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT)) { + rb_raise(rb_eArgError, "Invalid credential type"); + } else { + VALUE rb_username = rb_iv_get(rb_cred, "@username"); + VALUE rb_password = rb_iv_get(rb_cred, "@password"); + + Check_Type(rb_username, T_STRING); + Check_Type(rb_password, T_STRING); + + + rugged_exception_check( + git_cred_userpass_plaintext_new(cred, + StringValueCStr(rb_username), StringValueCStr(rb_password))); + } + } else if (rb_obj_is_kind_of(rb_cred, rb_cRuggedCredSshKey)) { + if (!(cred_payload->allowed_types & GIT_CREDTYPE_SSH_KEY)) { + rb_raise(rb_eArgError, "Invalid credential type"); + } else { + VALUE rb_username = rb_iv_get(rb_cred, "@username"); + VALUE rb_publickey = rb_iv_get(rb_cred, "@publickey"); + VALUE rb_privatekey = rb_iv_get(rb_cred, "@privatekey"); + VALUE rb_passphrase = rb_iv_get(rb_cred, "@passphrase"); + + Check_Type(rb_privatekey, T_STRING); + + if (!NIL_P(rb_username)) + Check_Type(rb_username, T_STRING); + if (!NIL_P(rb_publickey)) + Check_Type(rb_publickey, T_STRING); + if (!NIL_P(rb_passphrase)) + Check_Type(rb_passphrase, T_STRING); + + rugged_exception_check( + git_cred_ssh_key_new(cred, + NIL_P(rb_username) ? NULL : StringValueCStr(rb_username), + NIL_P(rb_publickey) ? NULL : StringValueCStr(rb_publickey), + StringValueCStr(rb_privatekey), + NIL_P(rb_passphrase) ? NULL : StringValueCStr(rb_passphrase))); + } + } else if (rb_obj_is_kind_of(rb_cred, rb_cRuggedCredDefault)) { + if (!(cred_payload->allowed_types & GIT_CREDTYPE_SSH_KEY)) { + rb_raise(rb_eArgError, "Invalid credential type"); + } else { + rugged_exception_check(git_cred_default_new(cred)); + } + } + + return Qnil; +} + +static int rugged__remote_credentials_cb( + git_cred **cred, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) +{ + struct rugged_remote_cb_payload *remote_payload = payload; + struct extract_cred_payload cred_payload; + VALUE args = rb_ary_new2(4), rb_allowed_types = rb_ary_new(); + + if (allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT) + rb_ary_push(rb_allowed_types, CSTR2SYM("plaintext")); + + if (allowed_types & GIT_CREDTYPE_SSH_KEY) + rb_ary_push(rb_allowed_types, CSTR2SYM("ssh_key")); + + if (allowed_types & GIT_CREDTYPE_DEFAULT) + rb_ary_push(rb_allowed_types, CSTR2SYM("default")); + + rb_ary_push(args, remote_payload->credentials); + rb_ary_push(args, url ? rb_str_new2(url) : Qnil); + rb_ary_push(args, username_from_url ? rb_str_new2(username_from_url) : Qnil); + rb_ary_push(args, rb_allowed_types); + + cred_payload.cred = cred; + cred_payload.rb_cred = rb_protect(rugged__block_yield_splat, args, &remote_payload->exception); + cred_payload.allowed_types = allowed_types; + + if (!remote_payload->exception) + rb_protect(rugged__extract_cred, (VALUE)&cred_payload, &remote_payload->exception); + + return remote_payload->exception ? GIT_ERROR : GIT_OK; +} + +static int rugged__default_remote_credentials_cb( + git_cred **cred, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) +{ + struct rugged_remote_cb_payload *remote_payload = payload; + struct extract_cred_payload cred_payload; + + cred_payload.cred = cred; + cred_payload.rb_cred = remote_payload->credentials; + cred_payload.allowed_types = allowed_types; + + rb_protect(rugged__extract_cred, (VALUE)&cred_payload, &remote_payload->exception); + + return remote_payload->exception ? GIT_ERROR : GIT_OK; +} + static void parse_clone_options(git_clone_options *ret, VALUE rb_options_hash, struct rugged_remote_cb_payload *remote_payload) { - if (!NIL_P(rb_options_hash)) { - VALUE val; - - val = rb_hash_aref(rb_options_hash, CSTR2SYM("bare")); - if (RTEST(val)) - ret->bare = 1; - - val = rb_hash_aref(rb_options_hash, CSTR2SYM("callbacks")); - if (RTEST(val)) { - git_remote_callbacks remote_callbacks = GIT_REMOTE_CALLBACKS_INIT; - VALUE cb; - - cb = rb_hash_aref(val, CSTR2SYM("transfer_progress")); - if (RTEST(cb)) { - if (!rb_respond_to(cb, rb_intern("call"))) { - rb_raise(rb_eArgError, "Expected a Proc or an object that responds to call (:transfer_progress)."); - } - - remote_payload->transfer_progress = cb; - remote_callbacks.transfer_progress = rugged__remote_transfer_progress_cb; - } + git_remote_callbacks remote_callbacks = GIT_REMOTE_CALLBACKS_INIT; + VALUE val; - remote_callbacks.payload = remote_payload; + if (NIL_P(rb_options_hash)) + return; - ret->remote_callbacks = remote_callbacks; + val = rb_hash_aref(rb_options_hash, CSTR2SYM("bare")); + if (RTEST(val)) + ret->bare = 1; + + val = rb_hash_aref(rb_options_hash, CSTR2SYM("credentials")); + if (RTEST(val)) { + if (rb_obj_is_kind_of(val, rb_cRuggedCredPlaintext) || + rb_obj_is_kind_of(val, rb_cRuggedCredSshKey) || + rb_obj_is_kind_of(val, rb_cRuggedCredDefault)) + { + remote_callbacks.credentials = rugged__default_remote_credentials_cb; + remote_payload->credentials = val; + } else if (rb_respond_to(val, rb_intern("call"))) { + remote_callbacks.credentials = rugged__remote_credentials_cb; + remote_payload->credentials = val; + } else { + rb_raise(rb_eArgError, + "Expected a Rugged::Credentials type, a Proc or an object that responds to call (:credentials)."); } } + + val = rb_hash_aref(rb_options_hash, CSTR2SYM("callbacks")); + if (RTEST(val)) { + VALUE cb; + + cb = rb_hash_aref(val, CSTR2SYM("transfer_progress")); + if (RTEST(cb)) { + if (!rb_respond_to(cb, rb_intern("call"))) { + rb_raise(rb_eArgError, "Expected a Proc or an object that responds to call (:transfer_progress)."); + } + + remote_payload->transfer_progress = cb; + remote_callbacks.transfer_progress = rugged__remote_transfer_progress_cb; + } + } + + remote_callbacks.payload = remote_payload; + ret->remote_callbacks = remote_callbacks; } /* @@ -378,6 +517,12 @@ static void parse_clone_options(git_clone_options *ret, VALUE rb_options_hash, s * :ignore_cert_errors :: * If set to +true+, errors while validating the remote's host certificate will be ignored. * + * :credentials :: + * The credentials to use for the clone operation. Can be either an instance of one + * of the Rugged::Credentials types, or a proc returning one of the former. + * The proc will be called with the +url+, the +username+ from the url (if applicable) and + * a list of applicable credential types. + * * :callbacks :: * A Hash containing name-value pairs that define different callbacks to run during * the clone operation. Possible callbacks are: @@ -388,9 +533,6 @@ static void parse_clone_options(git_clone_options *ret, VALUE rb_options_hash, s * :completion :: * Not yet implemented. * - * :credentials :: - * Not yet implemented. - * * :transfer_progress :: * A callback that will be executed to report clone progress information. It will be passed * the amount of +total_objects+, +indexed_objects+, +received_objects+ and +received_bytes+. diff --git a/lib/rugged.rb b/lib/rugged.rb index 1efb52c1a..b47093578 100644 --- a/lib/rugged.rb +++ b/lib/rugged.rb @@ -17,3 +17,4 @@ require 'rugged/diff' require 'rugged/patch' require 'rugged/remote' +require 'rugged/credentials' diff --git a/lib/rugged/credentials.rb b/lib/rugged/credentials.rb new file mode 100644 index 000000000..3707fad3d --- /dev/null +++ b/lib/rugged/credentials.rb @@ -0,0 +1,22 @@ +module Rugged + module Credentials + # A plain-text username and password credential object. + class Plaintext + def initialize(options) + @username, @password = options[:username], options[:password] + end + end + + # A ssh key credential object that can optionally be passphrase-protected + class SshKey + def initialize(options) + @username, @publickey, @privatekey, @passphrase = options[:username], options[:publickey], options[:privatekey], options[:passphrase] + end + end + + # A "default" credential usable for Negotiate mechanisms like NTLM or + # Kerberos authentication + class Default + end + end +end diff --git a/script/cibuild b/script/cibuild new file mode 100755 index 000000000..dca39e518 --- /dev/null +++ b/script/cibuild @@ -0,0 +1,23 @@ +#!/bin/sh + +# Create a test repo which we can use for the online::push tests +mkdir $HOME/_temp +git init --bare $HOME/_temp/test.git +git daemon --listen=localhost --export-all --enable=receive-pack --base-path=$HOME/_temp $HOME/_temp 2>/dev/null & + +sudo start ssh + +ssh-keygen -t rsa -f ~/.ssh/id_rsa -N "" -q +cat ~/.ssh/id_rsa.pub >>~/.ssh/authorized_keys +ssh-keyscan -t rsa localhost >>~/.ssh/known_hosts + +export GITTEST_REMOTE_GIT_URL="git://localhost/test.git" +export GITTEST_REMOTE_SSH_URL="ssh://localhost/$HOME/_temp/test.git" +export GITTEST_REMOTE_SSH_USER=$USER +export GITTEST_REMOTE_SSH_KEY="$HOME/.ssh/id_rsa" +export GITTEST_REMOTE_SSH_PUBKEY="$HOME/.ssh/id_rsa.pub" +export GITTEST_REMOTE_SSH_PASSPHRASE="" + +# We need the config so the tests don't fail +git config --global user.name 'The rugged tests are fragile' +bundle exec rake || exit $? diff --git a/test/online/clone_test.rb b/test/online/clone_test.rb new file mode 100644 index 000000000..322ce0941 --- /dev/null +++ b/test/online/clone_test.rb @@ -0,0 +1,94 @@ +require 'test_helper' + +class OnlineCloneTest < Rugged::TestCase + def ssh? + %w{URL USER KEY PUBKEY PASSPHRASE}.all? { |key| ENV["GITTEST_REMOTE_SSH_#{key}"] } + end + + def git? + ENV['GITTEST_REMOTE_GIT_URL'] + end + + def ssh_key_credential + Rugged::Credentials::SshKey.new({ + username: ENV["GITTEST_REMOTE_SSH_USER"], + publickey: ENV["GITTEST_REMOTE_SSH_PUBKEY"], + privatekey: ENV["GITTEST_REMOTE_SSH_KEY"], + passphrase: ENV["GITTEST_REMOTE_SSH_PASSPHASE"], + }) + end + + def test_clone_over_git + skip unless git? + + Dir.mktmpdir do |dir| + repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_GIT_URL'], dir) + + assert_instance_of Rugged::Repository, repo + end + end + + def test_clone_over_ssh_with_credentials + skip unless ssh? + + Dir.mktmpdir do |dir| + repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_SSH_URL'], dir, { + credentials: ssh_key_credential + }) + + assert_instance_of Rugged::Repository, repo + end + end + + def test_clone_over_ssh_with_credentials_callback + skip unless ssh? + + Dir.mktmpdir do |dir| + repo = Rugged::Repository.clone_at(ENV['GITTEST_REMOTE_SSH_URL'], dir, { + credentials: lambda { |url, username, allowed_types| + return ssh_key_credential + } + }) + + assert_instance_of Rugged::Repository, repo + end + end + + def test_clone_callback_args_without_username + Dir.mktmpdir do |dir| + url, username, allowed_types = nil, nil, nil + + assert_raises Rugged::SshError do + Rugged::Repository.clone_at("github.com:libgit2/TestGitRepository", dir, { + credentials: lambda { |*args| + url, username, allowed_types = *args + return nil + } + }) + end + + assert_equal "github.com:libgit2/TestGitRepository", url + assert_nil username + assert_equal [:plaintext, :ssh_key].sort, allowed_types.sort + end + end + + def test_clone_callback_args_with_username + Dir.mktmpdir do |dir| + url, username, allowed_types = nil, nil, nil + + assert_raises Rugged::SshError do + Rugged::Repository.clone_at("git@github.com:libgit2/TestGitRepository", dir, { + credentials: lambda { |*args| + url, username, allowed_types = *args + return nil + } + }) + end + + assert_equal "git@github.com:libgit2/TestGitRepository", url + assert_equal "git", username + assert_equal [:plaintext, :ssh_key].sort, allowed_types.sort + end + end +end