├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── ext └── libssh2_ruby_c │ ├── channel.c │ ├── error.c │ ├── extconf.rb │ ├── global.c │ ├── libssh2_ruby.h │ ├── libssh2_ruby_c.c │ └── session.c ├── lib ├── libssh2.rb └── libssh2 │ ├── channel.rb │ ├── error.rb │ ├── native.rb │ ├── native │ ├── error.rb │ └── error_codes.rb │ ├── session.rb │ └── version.rb ├── libssh2.gemspec ├── spec ├── acceptance │ ├── auth_spec.rb │ ├── channel_spec.rb │ ├── execute_spec.rb │ └── setup.rb ├── config.yml.example └── support │ └── acceptance_context.rb └── tasks ├── extension.rake ├── ports.rake └── test.rake /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle 2 | *.gem 3 | *.so 4 | .bundle 5 | .yardoc/ 6 | doc/ 7 | Gemfile.lock 8 | pkg/* 9 | ports/ 10 | test.rb 11 | spec/config.yml 12 | tmp/ 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in libssh2.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libssh2 Ruby Bindings 2 | 3 | This library provides bindings to [libssh2](http://www.libssh2.org). LibSSH2 4 | is a modern C-library implementing the SSH2 protocol and made by the same 5 | people who made [cURL](http://curl.haxx.se/). 6 | 7 | **Project status:** Alpha. Much of the library is still not yet done, 8 | and proper cross-platform testing has not been done. However, the library 9 | itself is functional, and everything in this README should work. 10 | 11 | ## Motivation 12 | 13 | The creation of `libssh2-ruby` was motivated primarily by edge-case issues 14 | experienced by [Vagrant](http://vagrantup.com) while using 15 | [Net-SSH](https://github.com/net-ssh/net-ssh). Net-SSH has been around 7 16 | years and is a stable library, but the Vagrant project found that around 1% 17 | of users were experiencing connectivity issues primarily revolving around 18 | slightly incompliant behavior by remote servers. 19 | 20 | `libssh2` is a heavily used library created by the same people as 21 | [cURL](http://curl.haxx.se/), simply one of the best command line applications 22 | around. It has been around for a few years and handles the SSH2 protocol 23 | very well. It is under active development and the motivation for libssh2 24 | to always work is high, since it is used by many high-profile open source 25 | projects. 26 | 27 | For this reason, `libssh2-ruby` was made to interface with this high 28 | quality library and provide a stable and robust SSH2 interface for Ruby. 29 | 30 | ## Usage 31 | 32 | The API for interacting with SSH is idiomatic Ruby and you should find 33 | it friendly and intuitive: 34 | 35 | ```ruby 36 | require "libssh2" 37 | 38 | # This API is not yet complete. More coming soon! 39 | 40 | session = LibSSH2::Session.new("127.0.0.1", "2222") 41 | session.auth_by_password("username", "password") 42 | session.execute "echo foo" do |channel| 43 | channel.on_data do |data| 44 | puts "stdout: #{data}" 45 | end 46 | end 47 | ``` 48 | 49 | However, if you require more fine-grained control, I don't want the 50 | API to limit you in any way. Therefore, it is my intention to expose 51 | all the native libssh2 functions on the `LibSSH2::Native` 52 | module as singleton methods, without the `libssh2_` prefix. So if you want 53 | to call `libssh2_init`, you actually call `LibSSH2::Native.init`. Here is 54 | an example that executes a basic `echo` via SSH: 55 | 56 | ```ruby 57 | require "libssh2" 58 | require "socket" 59 | include LibSSH2::Native 60 | 61 | # Remember, we're using the _native_ interface so below looks a lot 62 | # like C and some nasty Ruby code, but it is the direct interface 63 | # to libssh2. libssh2-ruby also provides a more idiomatic Ruby interface 64 | # that you can see above in the README. 65 | socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) 66 | socket.connect Socket.sockaddr_in("2222", "127.0.0.1") 67 | 68 | session = session_init 69 | session_set_blocking(session, true) 70 | session_handshake(session, socket.fileno) 71 | userauth_password(session, "username", "password") 72 | 73 | channel = channel_open_session(session) 74 | channel_exec(channel, "echo hello") 75 | data, _ = channel_read(channel) 76 | 77 | # Outputs "hello\n" 78 | puts data 79 | ``` 80 | 81 | ## Are there any downsides? When should I use Net::SSH? 82 | 83 | There are certainly some downsides. I've enumerated them below: 84 | 85 | * **libssh2-ruby requires libssh2**. This library requires [libssh2](http://www.libssh2.org/) 86 | to be installed. On most platforms this is very easy but it is still another 87 | step, whereas Net::SSH is a pure Ruby implementation of the SSH protocol. 88 | * **libssh2 can't do much for stdout/stderr ordering.** Due to the way the libssh2 89 | API is, the ordering of stdout/stderr is usually off. In practice this may 90 | or may not matter for you, and I'm working with the libssh2 team to try 91 | to address this issue in some way. 92 | * **libssh2 doesn't have access to every stream/request.** If you require advanced 93 | SSH usage, you can manually read from specific stream IDs, but the libssh2 94 | evented interface won't work with custom stream IDs or request types. For the 95 | 99% case this is not an issue, but I did want to note that this problem 96 | exists. 97 | 98 | That being said, libssh2 is wonderfully stable and fast, and if you're not 99 | negatively impacted by the above issues, then you should use it. 100 | 101 | ## Contributing 102 | 103 | ### Basic Steps 104 | 105 | 1. Fork it 106 | 2. Create your feature branch (`git checkout -b my-new-feature`) 107 | 3. Commit your changes (`git commit -am 'Added some feature'`) 108 | 4. Push to the branch (`git push origin my-new-feature`) 109 | 5. Create new Pull Request 110 | 111 | ### Using the Library from Source 112 | 113 | Since this library is a C extension based library, it can be tricky to work 114 | on and test. It is annoying to have to build the project every time to test 115 | some new code. Luckily, you don't have to! Just follow the steps below: 116 | 117 | 1. `rake compile` anytime you need to build the C extension. This should be 118 | done the first time you clone the project as well as any time you change 119 | any C code. On Linux, this will also build `libssh2` for you! 120 | 2. Make a test Ruby file that uses `libssh2` as if it were actually installed. 121 | For example, just `require "libssh2"` like normal. 122 | 3. Execute your test file using `bundle exec ruby test.rb`. This will use the 123 | source version instead of any gem installed version! 124 | 125 | You can use the above steps to iterate on the code and verify things are 126 | working okay while you do. However, prior to committing any new functionality, 127 | you should run the acceptance tests and potentially add to it, which is 128 | covered below. 129 | 130 | ### Running the Tests 131 | 132 | This library has an acceptance test suite to verify everything is working. 133 | Since it is an acceptance test library, it will make actually SSH connections 134 | to verify things are working properly. Running the suite is easy. First, 135 | create a `config.yml` based on the `config.yml.example` file in the `spec` 136 | directory. This must be configured to point to a real server that can an 137 | SSH connection can be established to with both password and key based auth. 138 | Once the `config.yml` is in place, run the tests: 139 | 140 | $ bundle exec rake 141 | 142 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | # The compile command should depend on installing libssh2 4 | task :compile => ["ports:libssh2"] 5 | 6 | # Load all the rak tasks from the "tasks" folder. This folder 7 | # allows us to nicely separate rake tasks into individual files 8 | # based on their role, which makes development and debugging easier 9 | # than one monolithic file. 10 | task_dir = File.expand_path("../tasks", __FILE__) 11 | Dir["#{task_dir}/**/*.rake"].each do |task_file| 12 | load task_file 13 | end 14 | 15 | # Setup the default task to test 16 | task :default => "spec" 17 | -------------------------------------------------------------------------------- /ext/libssh2_ruby_c/channel.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Prototypes 4 | static VALUE channel_read_ex(VALUE self, VALUE rb_stream_id, VALUE rb_buffer_size); 5 | 6 | /* 7 | * Helpers to return the LIBSSH2_CHANNEL for the given instance. 8 | * */ 9 | static inline LIBSSH2_CHANNEL * 10 | get_channel(VALUE self) { 11 | LibSSH2_Ruby_Channel *channel_data; 12 | Data_Get_Struct(self, LibSSH2_Ruby_Channel, channel_data); 13 | return channel_data->channel; 14 | } 15 | 16 | /* 17 | * Deallocates the memory associated with the channel data 18 | * structure. This also properly lowers the reference count 19 | * on the session structure. 20 | * */ 21 | static void 22 | channel_dealloc(LibSSH2_Ruby_Channel *channel_data) { 23 | if (channel_data->channel != NULL) { 24 | BLOCK(libssh2_channel_free(channel_data->channel)); 25 | } 26 | 27 | if (channel_data->session != NULL) { 28 | libssh2_ruby_session_release(channel_data->session); 29 | } 30 | 31 | free(channel_data); 32 | } 33 | 34 | /* 35 | * Called to allocate the memory associated with channels. We allocate 36 | * some space to store pointers to libssh2 structs in here. 37 | * */ 38 | static VALUE 39 | channel_allocate(VALUE self) { 40 | LibSSH2_Ruby_Channel *channel = malloc(sizeof(LibSSH2_Ruby_Channel)); 41 | channel->channel = NULL; 42 | channel->session = NULL; 43 | 44 | return Data_Wrap_Struct(self, 0, channel_dealloc, channel); 45 | } 46 | 47 | /* 48 | * call-seq: 49 | * Channel.new(session) 50 | * 51 | * Creates a new channel for the given session. This will open 52 | * a channel on the session so the session must be ready for that 53 | * or an exception will be raised. 54 | * 55 | * */ 56 | static VALUE 57 | channel_initialize(VALUE self, VALUE rb_session) { 58 | LIBSSH2_SESSION *session; 59 | LibSSH2_Ruby_Channel *channel_data; 60 | 61 | // Verify we have a valid session object 62 | CHECK_SESSION(rb_session); 63 | 64 | // Get the internal data from the instance. 65 | Data_Get_Struct(self, LibSSH2_Ruby_Channel, channel_data); 66 | 67 | // Read the interal data from the session 68 | Data_Get_Struct(rb_session, LibSSH2_Ruby_Session, channel_data->session); 69 | session = channel_data->session->session; 70 | 71 | // Create the channel, which we always do in a blocking 72 | // fashion since there is no other opportunity. 73 | do { 74 | channel_data->channel = libssh2_channel_open_session(session); 75 | 76 | // If we don't have a channel ant don't have a EAGAIN 77 | // error, then we raise an exception. 78 | if (channel_data->channel == NULL) { 79 | int error = libssh2_session_last_error(session, NULL, NULL, 0); 80 | if (error != LIBSSH2_ERROR_EAGAIN) { 81 | rb_exc_raise(libssh2_ruby_wrap_error(error)); 82 | return Qnil; 83 | } 84 | } 85 | } while(channel_data->channel == NULL); 86 | 87 | // Increase the refcount of the session data for us now that 88 | // we have a channel. 89 | libssh2_ruby_session_retain(channel_data->session); 90 | 91 | return self; 92 | } 93 | 94 | /* 95 | * call-seq: 96 | * channel.close -> true 97 | * 98 | * Sends a CLOSE command to the remote side. This typically has the effect 99 | * of the remote end stdin is closed. 100 | * */ 101 | static VALUE 102 | channel_close(VALUE self) { 103 | int result = libssh2_channel_close(get_channel(self)); 104 | HANDLE_LIBSSH2_RESULT(result); 105 | } 106 | 107 | /* 108 | * call-seq: 109 | * channel.exec("echo foo") -> Qtrue 110 | * 111 | * Executes a command line method on the channel. 112 | * 113 | * */ 114 | static VALUE 115 | channel_exec(VALUE self, VALUE command) { 116 | int result; 117 | 118 | rb_check_type(command, T_STRING); 119 | 120 | result = libssh2_channel_exec(get_channel(self), StringValuePtr(command)); 121 | HANDLE_LIBSSH2_RESULT(result); 122 | } 123 | 124 | /* 125 | * call-seq: 126 | * channel.eof -> true|false 127 | * 128 | * Returns a boolean of whether an EOF packet was sent by the remote end. 129 | * */ 130 | static VALUE 131 | channel_eof(VALUE self) { 132 | return libssh2_channel_eof(get_channel(self)) == 1 ? Qtrue : Qfalse; 133 | } 134 | 135 | /* 136 | * call-seq: 137 | * channel.get_exit_status -> int 138 | * 139 | * Returns the exit status of the program. 140 | * */ 141 | static VALUE 142 | channel_get_exit_status(VALUE self) { 143 | return INT2FIX(libssh2_channel_get_exit_status(get_channel(self))); 144 | } 145 | 146 | /* 147 | * call-seq: 148 | * channel.read -> string 149 | * 150 | * Reads from the channel. This will return the data as a string. This will 151 | * raise an ERROR_EAGAIN error if the socket would block. This will return 152 | * `nil` when an EOF is reached. 153 | * 154 | * */ 155 | static VALUE 156 | channel_read(VALUE self, VALUE buffer_size) { 157 | return channel_read_ex(self, INT2NUM(0), buffer_size); 158 | } 159 | 160 | /* 161 | * call-seq: 162 | * channel.read_ex(0, 1000) -> string 163 | * 164 | * Reads from the channel on the specified stream. This will return the data 165 | * as a string. This will raise an ERROR_EAGAIN if the socket would block. This 166 | * will return `nil` when an EOF is reached. 167 | * */ 168 | static VALUE 169 | channel_read_ex(VALUE self, VALUE rb_stream_id, VALUE rb_buffer_size) { 170 | int result; 171 | char *buffer; 172 | int stream_id; 173 | long buffer_size; 174 | LIBSSH2_CHANNEL *channel = get_channel(self); 175 | 176 | // Check types 177 | rb_check_type(rb_stream_id, T_FIXNUM); 178 | rb_check_type(rb_buffer_size, T_FIXNUM); 179 | 180 | // Verify parameters 181 | stream_id = NUM2INT(rb_stream_id); 182 | if (stream_id < 0) { 183 | rb_raise(rb_eArgError, "stream ID must be greater than or equal to 0"); 184 | return Qnil; 185 | } 186 | 187 | buffer_size = NUM2LONG(rb_buffer_size); 188 | if (buffer_size <= 0) { 189 | rb_raise(rb_eArgError, "buffer size must be greater than 0"); 190 | return Qnil; 191 | } 192 | 193 | // Create our buffer 194 | buffer = (char *)malloc(buffer_size); 195 | 196 | // Read from the channel 197 | result = libssh2_channel_read_ex(channel, stream_id, buffer, sizeof(buffer)); 198 | 199 | if (result > 0) { 200 | // Read succeeded. Create a string with the correct number of 201 | // bytes and return it. 202 | return rb_str_new(buffer, result); 203 | } else if (result == 0) { 204 | // No bytes read, this could signal EOF or just that no bytes 205 | // were ready. 206 | return Qnil; 207 | } else { 208 | HANDLE_LIBSSH2_RESULT(result); 209 | } 210 | } 211 | 212 | 213 | /* 214 | * call-seq: 215 | * channel.wait_closed -> true 216 | * 217 | * This blocks until the channel receives a CLOSE message from the remote 218 | * side. This can raise exceptions. 219 | * */ 220 | static VALUE 221 | channel_wait_closed(VALUE self) { 222 | int result = libssh2_channel_wait_closed(get_channel(self)); 223 | HANDLE_LIBSSH2_RESULT(result); 224 | } 225 | 226 | void init_libssh2_channel() { 227 | VALUE cChannel = rb_cLibSSH2_Native_Channel; 228 | rb_define_alloc_func(cChannel, channel_allocate); 229 | rb_define_method(cChannel, "initialize", channel_initialize, 1); 230 | rb_define_method(cChannel, "close", channel_close, 0); 231 | rb_define_method(cChannel, "exec", channel_exec, 1); 232 | rb_define_method(cChannel, "eof", channel_eof, 0); 233 | rb_define_method(cChannel, "get_exit_status", channel_get_exit_status, 0); 234 | rb_define_method(cChannel, "read", channel_read, 1); 235 | rb_define_method(cChannel, "read_ex", channel_read_ex, 2); 236 | rb_define_method(cChannel, "wait_closed", channel_wait_closed, 0); 237 | } 238 | -------------------------------------------------------------------------------- /ext/libssh2_ruby_c/error.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | * This convenience method takes an error code, finds the proper error 5 | * class, instantiates it, and returns it. 6 | * */ 7 | VALUE libssh2_ruby_wrap_error(int error) { 8 | VALUE error_code = INT2NUM(error); 9 | VALUE error_klass = rb_funcall( 10 | rb_mLibSSH2_Native_Error, 11 | rb_intern("error_for_code"), 12 | 1, 13 | error_code); 14 | 15 | return rb_class_new_instance(1, &error_code, error_klass); 16 | } 17 | -------------------------------------------------------------------------------- /ext/libssh2_ruby_c/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | require "rbconfig" 3 | 4 | # Allow "--with-libssh2-dir" configuration directives... 5 | dir_config("libssh2") 6 | 7 | # Called to "asplode" the install in case a dependency is missing for 8 | # this extension to compile properly. "asplode" seems to be the 9 | # idiomatic Ruby name for this method. 10 | def asplode(missing) 11 | abort "#{missing} is missing. Please install libssh2." 12 | end 13 | 14 | # Verify that we have libssh2 15 | asplode("libssh2.h") if !find_header("libssh2.h") 16 | 17 | # On Mac OS X, we can't actually statically compile a 64-bit version 18 | # of OpenSSL, so we just link against the shared versions as well. 19 | # Kind of a hack but it works fine. 20 | if RbConfig::CONFIG["host_os"] =~ /^darwin/ 21 | asplode("libcrypto") if !find_library("crypto", "CRYPTO_num_locks") 22 | asplode("openssl") if !find_library("ssl", "SSL_library_init") 23 | end 24 | 25 | # Verify libssh2 is usable 26 | asplode("libssh2") if !find_library("ssh2", "libssh2_init") 27 | 28 | # Create the makefile with the expected library name. 29 | create_makefile("libssh2_ruby_c") 30 | -------------------------------------------------------------------------------- /ext/libssh2_ruby_c/global.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | * call-seq: 5 | * LibSSH2::Native.init -> int 6 | * 7 | * Initializes libssh2 to run and returns the error code (0 if no error). 8 | * Note that this is not threadsafe and must be called before any other 9 | * libssh2 method. libssh2-ruby will automatically call this for you, usually, 10 | * but if you're only using the methods on Native, then you must call this 11 | * yourself. 12 | * 13 | * */ 14 | static VALUE 15 | init(VALUE module) { 16 | int result = libssh2_init(0); 17 | if (result != 0) { 18 | rb_exc_raise(libssh2_ruby_wrap_error(result)); 19 | return Qnil; 20 | } 21 | 22 | return Qtrue; 23 | } 24 | 25 | /* 26 | * call-seq: 27 | * Native.exit -> true 28 | * 29 | * Exits libssh2, deallocating any memory used internally. If this is called 30 | * Native.init must be called prior to doing anything with libssh2 again. 31 | * 32 | * */ 33 | static VALUE 34 | libexit(VALUE module) { 35 | libssh2_exit(); 36 | return Qtrue; 37 | } 38 | 39 | /* 40 | * call-seq: 41 | * LibSSH2::Native.version -> string 42 | * 43 | * Returns the version of libssh2 that is running. 44 | * 45 | * */ 46 | static VALUE 47 | version(VALUE module) { 48 | VALUE result = rb_cNilClass; 49 | const char *version_string = libssh2_version(0); 50 | 51 | // Technically `libssh2_version` can return NULL if the runtime 52 | // version is not new enough, but the "0" parameter above should 53 | // force this to always be true. Still, better safe than segfault. 54 | if (version_string != NULL) { 55 | result = rb_str_new_cstr(version_string); 56 | } 57 | 58 | return result; 59 | } 60 | 61 | void init_libssh2_global() { 62 | rb_const_set(rb_mLibSSH2_Native, 63 | rb_intern("SESSION_BLOCK_INBOUND"), 64 | INT2FIX(LIBSSH2_SESSION_BLOCK_INBOUND)); 65 | 66 | rb_const_set(rb_mLibSSH2_Native, 67 | rb_intern("SESSION_BLOCK_OUTBOUND"), 68 | INT2FIX(LIBSSH2_SESSION_BLOCK_OUTBOUND)); 69 | 70 | rb_define_singleton_method(rb_mLibSSH2_Native, "exit", libexit, 0); 71 | rb_define_singleton_method(rb_mLibSSH2_Native, "init", init, 0); 72 | rb_define_singleton_method(rb_mLibSSH2_Native, "version", version, 0); 73 | } 74 | -------------------------------------------------------------------------------- /ext/libssh2_ruby_c/libssh2_ruby.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBSSH2_RUBY_H 2 | #define LIBSSH2_RUBY_H 3 | 4 | #include 5 | #include 6 | 7 | /* 8 | * Makes the otherwise non-blocking libssh2 method call 9 | * a blocking call by waiting for it. 10 | * */ 11 | #define BLOCK(stmt) while ((stmt) == LIBSSH2_ERROR_EAGAIN) 12 | 13 | /* 14 | * Verifies the given argument is a valid session. This is called a lot 15 | * so I macro-fied it. 16 | * */ 17 | #define CHECK_SESSION(session) do{\ 18 | if (!rb_obj_is_kind_of(session, rb_cLibSSH2_Native_Session))\ 19 | rb_raise(rb_eArgError, "session must be a native session type");\ 20 | } while (0) 21 | 22 | /* 23 | * Convert Ruby socket's fileno into real Windows sockets that can be used 24 | * with LibSSH2. This is a no-op for any other OS. 25 | * */ 26 | #ifdef _WIN32 27 | # define TO_SOCKET(x) _get_osfhandle(x) 28 | #else 29 | # define TO_SOCKET(x) x 30 | #endif 31 | 32 | /* 33 | * Macro that handles generic libssh2 return values as we normally 34 | * do throughout the library: return true for success, and raise an 35 | * exception for any errors. 36 | * */ 37 | #define HANDLE_LIBSSH2_RESULT(value) do{\ 38 | if (value == 0)\ 39 | return Qtrue;\ 40 | rb_exc_raise(libssh2_ruby_wrap_error(value));\ 41 | return Qnil;\ 42 | } while (0) 43 | 44 | extern VALUE rb_mLibSSH2; 45 | extern VALUE rb_mLibSSH2_Native; 46 | extern VALUE rb_mLibSSH2_Native_Error; 47 | extern VALUE rb_cLibSSH2_Native_Channel; 48 | extern VALUE rb_cLibSSH2_Native_Session; 49 | 50 | /* 51 | * The struct embedded with LibSSH2::Native::Session classes 52 | * to store our internal C data. 53 | * */ 54 | typedef struct { 55 | LIBSSH2_SESSION *session; 56 | int refcount; 57 | } LibSSH2_Ruby_Session; 58 | 59 | /* 60 | * The struct embedded with LibSSH2::Native::Channel classes. 61 | * */ 62 | typedef struct { 63 | LIBSSH2_CHANNEL *channel; 64 | LibSSH2_Ruby_Session *session; 65 | } LibSSH2_Ruby_Channel; 66 | 67 | void init_libssh2_error(); 68 | void init_libssh2_global(); 69 | void init_libssh2_channel(); 70 | void init_libssh2_session(); 71 | 72 | void libssh2_ruby_session_retain(LibSSH2_Ruby_Session *); 73 | void libssh2_ruby_session_release(LibSSH2_Ruby_Session *); 74 | 75 | VALUE libssh2_ruby_wrap_error(int); 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /ext/libssh2_ruby_c/libssh2_ruby_c.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | VALUE rb_mLibSSH2; 4 | VALUE rb_mLibSSH2_Native; 5 | VALUE rb_mLibSSH2_Native_Error; 6 | VALUE rb_cLibSSH2_Native_Channel; 7 | VALUE rb_cLibSSH2_Native_Session; 8 | 9 | void Init_libssh2_ruby_c() { 10 | // Define the modules we're creating 11 | rb_mLibSSH2 = rb_define_module("LibSSH2"); 12 | rb_mLibSSH2_Native = rb_define_module_under(rb_mLibSSH2, "Native"); 13 | rb_mLibSSH2_Native_Error = rb_define_module_under(rb_mLibSSH2_Native, "Error"); 14 | rb_cLibSSH2_Native_Channel = rb_define_class_under(rb_mLibSSH2_Native, "Channel", rb_cObject); 15 | rb_cLibSSH2_Native_Session = rb_define_class_under(rb_mLibSSH2_Native, "Session", rb_cObject); 16 | 17 | // Initialize the various parts of the C-based API. The source 18 | // for these are in their respective files. i.e. global.c has 19 | // init_libssh2_global. 20 | init_libssh2_global(); 21 | init_libssh2_channel(); 22 | init_libssh2_session(); 23 | } 24 | -------------------------------------------------------------------------------- /ext/libssh2_ruby_c/session.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | * Increases the reference counter on the ruby session container. 5 | * */ 6 | void 7 | libssh2_ruby_session_retain(LibSSH2_Ruby_Session *session_data) { 8 | session_data->refcount++; 9 | } 10 | 11 | /* 12 | * Decrements the reference counter on the ruby session container. 13 | * When this goes to 0 then the session will be released. 14 | * */ 15 | void 16 | libssh2_ruby_session_release(LibSSH2_Ruby_Session *session_data) { 17 | // Decrease the reference count 18 | session_data->refcount--; 19 | 20 | // If the reference count is 0, free all the things! 21 | if (session_data->refcount == 0) { 22 | if (session_data->session != NULL) { 23 | BLOCK(libssh2_session_disconnect( 24 | session_data->session, 25 | "Normal shutdown by libssh2-ruby.")); 26 | BLOCK(libssh2_session_free(session_data->session)); 27 | } 28 | 29 | free(session_data); 30 | } 31 | } 32 | 33 | /* 34 | * Helper to return the LIBSSH2_SESSION pointer for the given 35 | * instance. 36 | * */ 37 | static inline LIBSSH2_SESSION * 38 | get_session(VALUE self) { 39 | LibSSH2_Ruby_Session *session; 40 | Data_Get_Struct(self, LibSSH2_Ruby_Session, session); 41 | return session->session; 42 | } 43 | 44 | /* 45 | * Called when the object is deallocated in order to deallocate the 46 | * interal state. 47 | * */ 48 | static void 49 | session_dealloc(LibSSH2_Ruby_Session *session_data) { 50 | libssh2_ruby_session_release(session_data); 51 | } 52 | 53 | /* 54 | * Called to allocate the memory associated with the object. We allocate 55 | * memory for internal structs and set them onto the object. 56 | * */ 57 | static VALUE 58 | allocate(VALUE self) { 59 | LibSSH2_Ruby_Session *session = malloc(sizeof(LibSSH2_Ruby_Session)); 60 | session->session = NULL; 61 | session->refcount = 0; 62 | 63 | return Data_Wrap_Struct(self, 0, session_dealloc, session); 64 | } 65 | 66 | /* 67 | * call-seq: 68 | * LibSSH2::Native::Session.new 69 | * 70 | * Initializes a new LibSSH2 session. This will raise an exception on 71 | * failure. 72 | * 73 | * */ 74 | static VALUE 75 | initialize(VALUE self) { 76 | LibSSH2_Ruby_Session *session; 77 | 78 | // Get the struct that stores our internal state out. This gets 79 | // setup in the `alloc` method. 80 | Data_Get_Struct(self, LibSSH2_Ruby_Session, session); 81 | 82 | session->session = libssh2_session_init(); 83 | if (session->session == NULL) { 84 | // ERROR! Make better exceptions plz. 85 | rb_raise(rb_eRuntimeError, "session init failed"); 86 | return Qnil; 87 | } 88 | 89 | // Retain so that we have a proper refcount 90 | libssh2_ruby_session_retain(session); 91 | 92 | return self; 93 | } 94 | 95 | /* 96 | * call-seq: 97 | * session.block_directions 98 | * 99 | * Returns an int that determines the direction to wait on the socket 100 | * in the case of an EAGAIN. This will be a binary mask that can be 101 | * checked with `Native::SESSION_BLOCK_INBOUND` and 102 | * `Native::SESSION_BLOCK_OUTBOUND`. 103 | * */ 104 | static VALUE 105 | block_directions(VALUE self) { 106 | return INT2FIX(libssh2_session_block_directions(get_session(self))); 107 | } 108 | 109 | /* 110 | * call-seq: 111 | * session.handshake(socket.fileno) -> int 112 | * 113 | * Initiates the handshake sequence for this session. You must 114 | * pass in the file number for the socket to use. This wll return 115 | * 0 on success, or raise an exception otherwise. 116 | * 117 | * */ 118 | static VALUE 119 | handshake(VALUE self, VALUE num_fd) { 120 | int fd = NUM2INT(num_fd); 121 | int ret = libssh2_session_handshake(get_session(self), TO_SOCKET(fd)); 122 | HANDLE_LIBSSH2_RESULT(ret); 123 | } 124 | 125 | /* 126 | * call-seq: 127 | * session.set_blocking(true) -> true 128 | * 129 | * If the argument is true, enables blocking semantics for this session, 130 | * otherwise enables non-blocking semantics. 131 | * 132 | * */ 133 | static VALUE 134 | set_blocking(VALUE self, VALUE blocking) { 135 | int blocking_arg = blocking == Qtrue ? 1 : 0; 136 | libssh2_session_set_blocking(get_session(self), blocking_arg); 137 | return blocking; 138 | } 139 | 140 | /* 141 | * call-seq: 142 | * session.userauth_authenticated -> true/false 143 | * 144 | * Returns a boolean of whether this session has been authenticated or 145 | * not. 146 | * 147 | * */ 148 | static VALUE 149 | userauth_authenticated(VALUE self) { 150 | return libssh2_userauth_authenticated(get_session(self)) == 1 ? 151 | Qtrue : 152 | Qfalse; 153 | } 154 | 155 | /* 156 | * call-seq: 157 | * session.userauth_password("username", "password") 158 | * 159 | * Attempts to authenticate using a username and password. 160 | * 161 | * */ 162 | static VALUE 163 | userauth_password(VALUE self, VALUE username, VALUE password) { 164 | int result; 165 | rb_check_type(username, T_STRING); 166 | rb_check_type(password, T_STRING); 167 | 168 | result = libssh2_userauth_password( 169 | get_session(self), 170 | StringValuePtr(username), 171 | StringValuePtr(password)); 172 | HANDLE_LIBSSH2_RESULT(result); 173 | } 174 | 175 | /* 176 | * call-seq: 177 | * session.userauth_publickey_fromfile("username", "/etc/key.pub", "/etc/key", "foo") 178 | * 179 | * Attempts to authenticate using public and private keys from files. 180 | * 181 | * */ 182 | static VALUE 183 | userauth_publickey_fromfile(VALUE self, 184 | VALUE username, 185 | VALUE publickey_path, 186 | VALUE privatekey_path, 187 | VALUE passphrase) { 188 | int result; 189 | rb_check_type(username, T_STRING); 190 | rb_check_type(publickey_path, T_STRING); 191 | rb_check_type(privatekey_path, T_STRING); 192 | rb_check_type(passphrase, T_STRING); 193 | 194 | result = libssh2_userauth_publickey_fromfile( 195 | get_session(self), 196 | StringValuePtr(username), 197 | StringValuePtr(publickey_path), 198 | StringValuePtr(privatekey_path), 199 | StringValuePtr(passphrase)); 200 | HANDLE_LIBSSH2_RESULT(result); 201 | } 202 | 203 | void init_libssh2_session() { 204 | VALUE cSession = rb_cLibSSH2_Native_Session; 205 | rb_define_alloc_func(cSession, allocate); 206 | rb_define_method(cSession, "initialize", initialize, 0); 207 | rb_define_method(cSession, "block_directions", block_directions, 0); 208 | rb_define_method(cSession, "handshake", handshake, 1); 209 | rb_define_method(cSession, "set_blocking", set_blocking, 1); 210 | rb_define_method(cSession, "userauth_authenticated", userauth_authenticated, 0); 211 | rb_define_method(cSession, "userauth_password", userauth_password, 2); 212 | rb_define_method(cSession, "userauth_publickey_fromfile", 213 | userauth_publickey_fromfile, 4); 214 | } 215 | -------------------------------------------------------------------------------- /lib/libssh2.rb: -------------------------------------------------------------------------------- 1 | # Require the C library. 2 | require "libssh2_ruby_c" 3 | 4 | # Methods that augment our native set 5 | require "libssh2/native" 6 | require "libssh2/native/error" 7 | 8 | # The pretty Ruby API 9 | require "libssh2/error" 10 | require "libssh2/session" 11 | require "libssh2/channel" 12 | -------------------------------------------------------------------------------- /lib/libssh2/channel.rb: -------------------------------------------------------------------------------- 1 | module LibSSH2 2 | # Represents a channel on top of an SSH session. Most communication 3 | # done via SSH is done on one or more channels. The actual communication 4 | # is then multiplexed onto a single connection. 5 | class Channel 6 | # The stream ID for the main data channel. This is always 0 as defined 7 | # by RFC 4254 8 | STREAM_DATA = 0 9 | 10 | # The stream ID for the extended data channel (typically used for stderr). 11 | # This is always 1 as defined by RFC 4254. 12 | STREAM_EXTENDED_DATA = 1 13 | 14 | # This gives you access to the native underlying channel object. 15 | # Use this **at your own risk**. If you start calling native methods, 16 | # then the safety of the rest of this class is no longer guaranteed. 17 | # 18 | # @return [Native::Channel] 19 | attr_reader :native_channel 20 | 21 | # The session that this channel belongs to. 22 | # 23 | # @return [Session] 24 | attr_reader :session 25 | 26 | # Opens a new channel. This should almost never be called directly, 27 | # since the parameter required is the native channel object. Instead 28 | # use helpers such as {Session#open_channel}. 29 | # 30 | # @param [Native::Channel] native_channel Native channel structure. 31 | # @param [Session] session Session that this channel belongs to. 32 | def initialize(native_channel, session) 33 | @native_channel = native_channel 34 | @session = session 35 | @closed = false 36 | @stream_callbacks = {} 37 | end 38 | 39 | # Sends a CLOSE request to the remote end, which signals that we will 40 | # not send any more data. Note that the remote end may continue sending 41 | # data until it sends its own respective CLOSE request. 42 | def close 43 | # Only one CLOSE request may be sent. Guard your close calls by 44 | # checking the value of {#closed?} 45 | raise DoubleCloseError if @closed 46 | 47 | # Send the CLOSE 48 | @session.blocking_call { @native_channel.close } 49 | 50 | # Mark that we closed 51 | @closed = true 52 | end 53 | 54 | # Returns a boolean of whether we closed or not. Note that this is 55 | # not an indicator of the remote end has closed the connection. 56 | # 57 | # @return [Boolean] 58 | def closed? 59 | @closed 60 | end 61 | 62 | # Executes the given command as if on the command line. This will 63 | # return immediately. Call `wait` on the channel to wait for the 64 | # channel to complete. 65 | # 66 | # @return [Process] 67 | def execute(command) 68 | @session.blocking_call do 69 | @native_channel.exec(command) 70 | end 71 | end 72 | 73 | # Specify a callback that is called when data is received on this 74 | # channel. 75 | # 76 | # @yield [data] Called every time data is received, with the data. 77 | def on_data(&callback) 78 | @stream_callbacks[STREAM_DATA] = callback 79 | end 80 | 81 | # Specify a callback that is called when extended data (typically 82 | # stderr) is received on this channel. 83 | # 84 | # @yield [data] Called every time data is received, with the data. 85 | def on_extended_data(&callback) 86 | @stream_callbacks[STREAM_EXTENDED_DATA] = callback 87 | end 88 | 89 | # Specify a callback that is called when the exit status is 90 | # received on this channel. 91 | # 92 | # @yield [exit_status] Called once when the exit status is received with 93 | # the exit status. 94 | def on_exit_status(&callback) 95 | @stream_callbacks[:exit_status] = callback 96 | end 97 | 98 | # Attempts reading from specific streams on the channel. This will not 99 | # block if data is unavailable. This typically doesn't need to be 100 | # called publicly but can be if you'd like. If data is found, it will 101 | # invoke the proper callback on the thread which calls this method. 102 | # 103 | # @return [Boolean] True if EOF is not seen, false if EOF is seen, 104 | # meaning no data is ever coming again. 105 | def attempt_read 106 | # Return false if we have nothing else to read 107 | return false if @native_channel.eof 108 | 109 | # Attempt to read from stdout/stderr 110 | @session.blocking_call { read_stream(STREAM_DATA) } 111 | @session.blocking_call { read_stream(STREAM_EXTENDED_DATA) } 112 | 113 | # Return true always 114 | true 115 | end 116 | 117 | # This blocks until the channel completes, and also initiates the 118 | # event loop so that data callbacks will be called when data is 119 | # received. Prior to this, data will be received and buffered until 120 | # this is called. This ensures that callbacks are always called on 121 | # the thread that `wait` is called on. 122 | # 123 | # This method will also implicitly call {#close}. 124 | def wait 125 | # Read all the data 126 | loop { break if !attempt_read } 127 | 128 | # Close our end, we won't be sending any more requests. 129 | close if !closed? 130 | 131 | # Wait for the remote end to close 132 | @session.blocking_call { @native_channel.wait_closed } 133 | 134 | # Grab our exit status if we care about it 135 | exit_status_cb = @stream_callbacks[:exit_status] 136 | exit_status_cb.call(@native_channel.get_exit_status) if exit_status_cb 137 | end 138 | 139 | protected 140 | 141 | # This will read from the given stream ID and call the proper 142 | # callbacks if they exist. 143 | # 144 | # @param [Fixnum] stream_id The stream to read. 145 | def read_stream(stream_id) 146 | data_cb = @stream_callbacks[stream_id] 147 | 148 | while true 149 | data = nil 150 | begin 151 | data = @native_channel.read_ex(stream_id, 1000) 152 | rescue Native::Error::ERROR_EAGAIN 153 | # When we get an EAGAIN then we're done. Return. 154 | return 155 | end 156 | 157 | # If we got nil as a return value then there is no more data 158 | # to read. 159 | return if data.nil? 160 | 161 | # Callback if we have data to send and we have a callback 162 | data_cb.call(data) if data_cb 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/libssh2/error.rb: -------------------------------------------------------------------------------- 1 | module LibSSH2 2 | # The superclass of all errors that are thrown in LibSSH2 3 | class Error < StandardError; end 4 | 5 | # Error thrown when a method requires authentication before being 6 | # called. 7 | class AuthenticationRequired < Error; end 8 | 9 | # Thrown when {Channel#close} is called multiple times. Only _one_ 10 | # CLOSE request may be sent over a channel. 11 | class DoubleCloseError < Error; end 12 | end 13 | -------------------------------------------------------------------------------- /lib/libssh2/native.rb: -------------------------------------------------------------------------------- 1 | module LibSSH2 2 | # This module is almost completely implemented in pure C. Additional 3 | # methods are defined here to make things easier to implement. 4 | module Native 5 | # This is a helper to define proxy methods on this module. Many 6 | # methods are proxied to their respective objects, and this lets us 7 | # do it really easily, concisely. 8 | def self.proxy_method(*args) 9 | if args.length != 2 && args.length != 3 10 | raise ArgumentError, "2 or 3 arguments required." 11 | end 12 | 13 | prefix = nil 14 | prefix = args.shift if args.length == 3 15 | name = args.shift 16 | klass = args.shift 17 | method_name = name 18 | method_name = "#{prefix}_#{name}" if prefix 19 | 20 | metaclass = class << self; self; end 21 | metaclass.send(:define_method, method_name) do |object, *args| 22 | if !object.kind_of?(klass) 23 | raise ArgumentError, "Receiving object must be a: #{klass}" 24 | end 25 | 26 | object.send(name, *args) 27 | end 28 | end 29 | 30 | #---------------------------------------------------------------- 31 | # Session Methods 32 | #---------------------------------------------------------------- 33 | def self.session_init 34 | Native::Session.new 35 | end 36 | 37 | proxy_method :session, :set_blocking, Native::Session 38 | proxy_method :session, :handshake, Native::Session 39 | proxy_method :userauth_authenticated, Native::Session 40 | proxy_method :userauth_password, Native::Session 41 | proxy_method :userauth_publickey_fromfile, Native::Session 42 | 43 | #---------------------------------------------------------------- 44 | # Channel Methods 45 | #---------------------------------------------------------------- 46 | def self.channel_open_session(session) 47 | Native::Channel.new(session) 48 | end 49 | 50 | proxy_method :channel, :close, Native::Channel 51 | proxy_method :channel, :exec, Native::Channel 52 | proxy_method :channel, :eof, Native::Channel 53 | proxy_method :channel, :get_exit_status, Native::Channel 54 | proxy_method :channel, :read, Native::Channel 55 | proxy_method :channel, :read_ex, Native::Channel 56 | proxy_method :channel, :wait_closed, Native::Channel 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/libssh2/native/error.rb: -------------------------------------------------------------------------------- 1 | require "libssh2/error" 2 | require "libssh2/native/error_codes" 3 | 4 | module LibSSH2 5 | module Native 6 | module Error 7 | # Returns an error class for the given numeric code. If no such error 8 | # class exists, then the generic class will be returned. 9 | def self.error_for_code(code) 10 | const_get(LIBSSH_ERRORS_BY_CODE[code]) 11 | end 12 | 13 | # The generic error that is the superclass for all the more specific 14 | # errors that libssh2 might throw. 15 | class Generic < LibSSH2::Error 16 | # The numeric error code for an instance. 17 | attr_reader :error_code 18 | 19 | # Create a generic error. 20 | # 21 | # @param [Fixnum] error_code The error code of the error. 22 | def initialize(error_code) 23 | @error_code = error_code 24 | end 25 | 26 | def to_s 27 | "Error: #{LIBSSH_ERRORS_BY_CODE[@error_code]} (#{@error_code})" 28 | end 29 | end 30 | 31 | # Define the error classes for every error we know about. 32 | LIBSSH_ERRORS_BY_KEY.each do |key, code| 33 | const_set(key, Class.new(Generic)) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/libssh2/native/error_codes.rb: -------------------------------------------------------------------------------- 1 | # The following is a map of the errors from key to the 2 | # error code. This is extracted directly from libssh2.h 3 | LIBSSH_ERRORS_BY_KEY = { 4 | "ERROR_SOCKET_NONE" => -1, 5 | "ERROR_BANNER_RECV" => -2, 6 | "ERROR_BANNER_SEND" => -3, 7 | "ERROR_INVALID_MAC" => -4, 8 | "ERROR_KEX_FAILURE" => -5, 9 | "ERROR_ALLOC" => -6, 10 | "ERROR_SOCKET_SEND" => -7, 11 | "ERROR_KEY_EXCHANGE_FAILURE" => -8, 12 | "ERROR_TIMEOUT" => -9, 13 | "ERROR_HOSTKEY_INIT" => -10, 14 | "ERROR_HOSTKEY_SIGN" => -11, 15 | "ERROR_DECRYPT" => -12, 16 | "ERROR_SOCKET_DISCONNECT" => -13, 17 | "ERROR_PROTO" => -14, 18 | "ERROR_PASSWORD_EXPIRED" => -15, 19 | "ERROR_FILE" => -16, 20 | "ERROR_METHOD_NONE" => -17, 21 | "ERROR_AUTHENTICATION_FAILED" => -18, 22 | "ERROR_PUBLICKEY_UNRECOGNIZED" => -18, 23 | "ERROR_PUBLICKEY_UNVERIFIED" => -19, 24 | "ERROR_CHANNEL_OUTOFORDER" => -20, 25 | "ERROR_CHANNEL_FAILURE" => -21, 26 | "ERROR_CHANNEL_REQUEST_DENIED" => -22, 27 | "ERROR_CHANNEL_UNKNOWN" => -23, 28 | "ERROR_CHANNEL_WINDOW_EXCEEDED" => -24, 29 | "ERROR_CHANNEL_PACKET_EXCEEDED" => -25, 30 | "ERROR_CHANNEL_CLOSED" => -26, 31 | "ERROR_CHANNEL_EOF_SENT" => -27, 32 | "ERROR_SCP_PROTOCOL" => -28, 33 | "ERROR_ZLIB" => -29, 34 | "ERROR_SOCKET_TIMEOUT" => -30, 35 | "ERROR_SFTP_PROTOCOL" => -31, 36 | "ERROR_REQUEST_DENIED" => -32, 37 | "ERROR_METHOD_NOT_SUPPORTED" => -33, 38 | "ERROR_INVAL" => -34, 39 | "ERROR_INVALID_POLL_TYPE" => -35, 40 | "ERROR_PUBLICKEY_PROTOCOL" => -36, 41 | "ERROR_EAGAIN" => -37, 42 | "ERROR_BUFFER_TOO_SMALL" => -38, 43 | "ERROR_BAD_USE" => -39, 44 | "ERROR_COMPRESS" => -40, 45 | "ERROR_OUT_OF_BOUNDARY" => -41, 46 | "ERROR_AGENT_PROTOCOL" => -42, 47 | "ERROR_SOCKET_RECV" => -43, 48 | "ERROR_ENCRYPT" => -44, 49 | "ERROR_BAD_SOCKET" => -45 50 | } 51 | 52 | # Also provide a lookup by code 53 | LIBSSH_ERRORS_BY_CODE = LIBSSH_ERRORS_BY_KEY.invert 54 | 55 | # Define the errors as constants on LibSSH2::Native so they 56 | # can be referenced by the code more easily. 57 | module LibSSH2 58 | module Native 59 | LIBSSH_ERRORS_BY_KEY.each do |key, value| 60 | const_set(key, value) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/libssh2/session.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | 3 | require "libssh2/channel" 4 | require "libssh2/error" 5 | 6 | module LibSSH2 7 | # Represents a session, or a connection to a remote host for SSH. 8 | class Session 9 | # This gives you access to the native underlying session object. 10 | # This should be used at **your own risk**. If you start calling 11 | # native methods, the safety of the rest of this class is not 12 | # guaranteed. 13 | # 14 | # @return [Native::Session] 15 | attr_reader :native_session 16 | 17 | # Initializes a new session for the given host and port. This will 18 | # open a connection with the remote host. 19 | # 20 | # @param [String] host Hostname or IP of the remote host. 21 | # @param [Fixnum] port Port on the host to connect to. 22 | def initialize(host, port) 23 | # Initialize state 24 | @host = host 25 | @port = port 26 | @channels = [] 27 | 28 | # Connect to the remote host 29 | @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) 30 | @socket.connect(Socket.sockaddr_in(@port, @host)) 31 | 32 | # Create the underlying LibSSH2 structures 33 | @native_session = Native.session_init 34 | @native_session.set_blocking(false) 35 | blocking_call { @native_session.handshake(@socket.fileno) } 36 | end 37 | 38 | # Returns a boolean denoting whether this session has been authenticated 39 | # yet or not. 40 | # 41 | # @return [Boolean] 42 | def authenticated? 43 | @native_session.userauth_authenticated 44 | end 45 | 46 | # Authenticates using a username and password. This will return true if 47 | # it succeeds or throw an exception if it doesn't. The reason an exception 48 | # is thrown instead of a basic "false" is because there can be many reasons 49 | # why authentication fails, and exceptions allow us to be more specific about 50 | # what went wrong. 51 | # 52 | # @param [String] username Username to authentiate with. 53 | # @param [String] password Associated password for the username. 54 | # @return True 55 | def auth_by_password(username, password) 56 | blocking_call { @native_session.userauth_password(username, password) } 57 | end 58 | 59 | # Authenticates using public key authentication. This will return true if 60 | # authentication was successful or throw an exception if it wasn't. The reason 61 | # an exception is thrown instead of a basic "false" is becuse there can be 62 | # many reasons why authentication fails, and exceptions allow us to be more specific 63 | # about what went wrong. 64 | # 65 | # @param [String] username Username to authenticate as. 66 | # @param [String] pubkey_path Path to the public key. This must be fully expanded. 67 | # @param [String] privatekey_path Path to the private key. This must be 68 | # fully expanded. 69 | # @param [optional, password] Password for the private key. 70 | # @return True 71 | def auth_by_publickey_fromfile(username, pubkey_path, privatekey_path, password=nil) 72 | # The C API requires that the password be a string, but it is 73 | # safer to default arguments to `nil`, so we have to change it 74 | # to an empty string here if not set. 75 | password ||= "" 76 | 77 | blocking_call do 78 | @native_session.userauth_publickey_fromfile( 79 | username, pubkey_path, privatekey_path, password) 80 | end 81 | end 82 | 83 | # Performs the given block until it doesn't raise ERROR_EAGAIN. ERROR_EAGAIN 84 | # is raised by libssh2 when a call would black and the session is non-blocking. 85 | # This method forces the non-blocking calls to block until they complete. 86 | # 87 | # **Note:** You should almost never have to call this on your own, but is 88 | # available should you need to execute a native method. 89 | # 90 | # @yield [] Called until ERROR_EAGAIN is not raised, returns the value. 91 | # @return [Object] Returns the value the block returns. 92 | def blocking_call 93 | while true 94 | begin 95 | return yield 96 | rescue Native::Error::ERROR_EAGAIN 97 | waitsocket 98 | end 99 | end 100 | end 101 | 102 | # Convenience method to execute a command with this session. This will 103 | # open a new channel and yield the channel to a block so that data listeners 104 | # can be attached to it, if needed. The channel is also returned. 105 | # 106 | # @param [String] command Command to execute 107 | # @yield [channel] Called with the opened channel prior to executing so 108 | # that data listeners can be attached. 109 | # @return [Channel] 110 | def execute(command, &block) 111 | channel = open_channel(&block) 112 | channel.execute(command) 113 | channel.wait 114 | channel 115 | end 116 | 117 | # Runs an event loop, kicking firing off any event listeners for channels. 118 | # This blocks, and will force any events to fire on this thread. This will 119 | # loop while the given block returns true. By default, if no block is given 120 | # then this will loop while there are any active channels. 121 | # 122 | # @yield [] If this returns true, then the loop will continue. Return false \ 123 | # to end the loop. This will be called periodically. 124 | def loop(&block) 125 | # This is the conditional that if it returns true, then the loop 126 | # continues. 127 | continue_conditional = block || Proc.new { !@channels.empty? } 128 | 129 | Kernel.loop do 130 | # Remove any channels that are closed 131 | @channels.delete_if { |c| c.closed? } 132 | 133 | # Read from the active channels 134 | @channels.each do |channel| 135 | channel.attempt_read 136 | end 137 | 138 | # Break if the conditional tells us to 139 | break if !continue_conditional.call 140 | end 141 | end 142 | 143 | # Opens a new channel and returns a {Channel} object. 144 | # 145 | # @yield [channel] Optional, if a block is given, the resulting channel is 146 | # yielded to it prior to returning. 147 | # @return [Channel] 148 | def open_channel 149 | # We need to check if we're authenticated here otherwise the next call 150 | # will actually block forever. 151 | raise AuthenticationRequired if !authenticated? 152 | 153 | # Open a new channel 154 | native_channel = Native::Channel.new(@native_session) 155 | result = Channel.new(native_channel, self) 156 | @channels << result 157 | 158 | # Yield if a block was given so some processing can be done, but 159 | # return the channel. 160 | yield result if block_given? 161 | result 162 | end 163 | 164 | # If an ERROR_EGAIN error is raised by libssh2 then this should be called 165 | # to wait for the socket to be ready to use again. 166 | # 167 | # Note that this generally **never** needs to be called by the general 168 | # public, but is provided as a convenience if you are using the native 169 | # session object. 170 | def waitsocket 171 | readfd = [] 172 | writefd = [] 173 | 174 | # Determine which direction to wait for... 175 | dir = @native_session.block_directions 176 | readfd << @socket if dir & Native::SESSION_BLOCK_INBOUND 177 | writefd << @socket if dir & Native::SESSION_BLOCK_OUTBOUND 178 | 179 | # Select on the file descriptors 180 | IO.select(readfd, writefd, nil, 10) 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/libssh2/version.rb: -------------------------------------------------------------------------------- 1 | module LibSSH2 2 | VERSION = "0.1.0.dev" 3 | end 4 | -------------------------------------------------------------------------------- /libssh2.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "libssh2/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "libssh2" 7 | s.version = LibSSH2::VERSION 8 | s.authors = ["Mitchell Hashimoto"] 9 | s.email = ["mitchell.hashimoto@gmail.com"] 10 | s.homepage = "" 11 | s.summary = "libssh2 Ruby bindings." 12 | s.description = "libssh2 Ruby bindings." 13 | 14 | s.rubyforge_project = "libssh2" 15 | 16 | s.extensions = ["ext/libssh2_ruby_c/extconf.rb"] 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_development_dependency "mini_portile", "~> 0.2.2" 23 | s.add_development_dependency "rake-compiler", "~> 0.8.0" 24 | s.add_development_dependency "rspec-core", "~> 2.8.0" 25 | s.add_development_dependency "rspec-expectations", "~> 2.8.0" 26 | s.add_development_dependency "rspec-mocks", "~> 2.8.0" 27 | s.add_development_dependency "yard", "~> 0.7.5" 28 | end 29 | -------------------------------------------------------------------------------- /spec/acceptance/auth_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../setup", __FILE__) 2 | 3 | describe "authentication" do 4 | include_context "acceptance" 5 | 6 | it "should not be authenticated by default" do 7 | session.should_not be_authenticated 8 | end 9 | 10 | describe "by password" do 11 | it "should authenticate" do 12 | session.auth_by_password(acceptance_config["user"], 13 | acceptance_config["password"]) 14 | 15 | session.should be_authenticated 16 | end 17 | 18 | it "should be able to fail" do 19 | expect { 20 | session.auth_by_password(acceptance_config["user"] + "_wrong", "wrong") 21 | }.to raise_error(LibSSH2::Native::Error::ERROR_PUBLICKEY_UNRECOGNIZED) 22 | 23 | session.should_not be_authenticated 24 | end 25 | end 26 | 27 | describe "by keypair" do 28 | it "should authentiate" do 29 | session.auth_by_publickey_fromfile(acceptance_config["user"], 30 | acceptance_config["public_key_path"], 31 | acceptance_config["private_key_path"]) 32 | 33 | session.should be_authenticated 34 | end 35 | 36 | it "should be able to fail" do 37 | pending "Need a fake keypair." 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/acceptance/channel_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../setup", __FILE__) 2 | 3 | describe "channels" do 4 | include_context "acceptance" 5 | 6 | let(:authed_session) do 7 | session.auth_by_password(acceptance_config["user"], 8 | acceptance_config["password"]) 9 | session 10 | end 11 | 12 | let(:channel) { authed_session.open_channel } 13 | 14 | it "can't open a channel before authenticating" do 15 | expect { session.open_channel }.to 16 | raise_error(LibSSH2::AuthenticationRequired) 17 | end 18 | 19 | it "can open a channel once authenticated" do 20 | ch = authed_session.open_channel 21 | ch.should be_kind_of(LibSSH2::Channel) 22 | end 23 | 24 | it "can close a channel" do 25 | channel.close 26 | channel.should be_closed 27 | end 28 | 29 | it "can't close a channel twice" do 30 | channel.close 31 | expect { channel.close }.to raise_error(LibSSH2::DoubleCloseError) 32 | end 33 | 34 | it "can execute a command and read the output" do 35 | result = "" 36 | channel.execute "echo foo" 37 | channel.on_data { |d| result << d } 38 | channel.wait 39 | 40 | result.should == "foo\n" 41 | end 42 | 43 | it "can read the exit status" do 44 | status = nil 45 | channel.execute "exit 5" 46 | channel.on_exit_status { |value| status = value } 47 | channel.wait 48 | 49 | status.should == 5 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/acceptance/execute_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../setup", __FILE__) 2 | 3 | describe "command execution" do 4 | include_context "acceptance" 5 | 6 | it "should execute and block a single command" do 7 | session.auth_by_password(acceptance_config["user"], 8 | acceptance_config["password"]) 9 | 10 | # Execute and build the result in this variable so we can check it later 11 | result = "" 12 | session.execute "echo foo" do |channel| 13 | channel.on_data { |d| result << d } 14 | end 15 | 16 | # The result should be what we asked for, since the above should block 17 | result.should == "foo\n" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/acceptance/setup.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "yaml" 3 | 4 | require "rspec/autorun" 5 | 6 | # Load the base context for acceptance tests 7 | require "support/acceptance_context" 8 | 9 | # Load our test configuration 10 | spec_config_file = File.expand_path("../../config.yml", __FILE__) 11 | raise "A config.yml must be present in the spec directory." if !File.file?(spec_config_file) 12 | $spec_config = YAML.load_file(spec_config_file.to_s) 13 | 14 | # Configure RSpec 15 | RSpec.configure do |c| 16 | c.expect_with :rspec 17 | end 18 | -------------------------------------------------------------------------------- /spec/config.yml.example: -------------------------------------------------------------------------------- 1 | acceptance: 2 | host: 127.0.0.1 3 | port: 2222 4 | user: vagrant 5 | password: vagrant 6 | public_key_path: /path/to/key.pub 7 | private_key_path: /path/to/key 8 | -------------------------------------------------------------------------------- /spec/support/acceptance_context.rb: -------------------------------------------------------------------------------- 1 | require "libssh2" 2 | 3 | shared_context "acceptance" do 4 | let(:acceptance_config) do 5 | # If we don't have test configuration, then throw an error 6 | if !$spec_config 7 | raise "A config.yml must be present in the spec directory. See the example." 8 | end 9 | 10 | # Return the actual acceptance-only config 11 | $spec_config["acceptance"] 12 | end 13 | 14 | let(:session) do 15 | # Return a session connected to our configured host/port 16 | LibSSH2::Session.new(acceptance_config["host"], acceptance_config["port"]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tasks/extension.rake: -------------------------------------------------------------------------------- 1 | require "bundler/gem_helper" 2 | require "rake/extensiontask" 3 | 4 | # This gives us access to our gemspec that is managed by Bundler. 5 | # This requires Bundler 1.1.0. 6 | gem_helper = Bundler::GemHelper.new(Dir.pwd) 7 | Rake::ExtensionTask.new("libssh2_ruby_c", gem_helper.gemspec) do |ext| 8 | ext.cross_compile = true 9 | ext.cross_platform = "i686-w64-mingw32" 10 | end 11 | -------------------------------------------------------------------------------- /tasks/ports.rake: -------------------------------------------------------------------------------- 1 | require "mini_portile" 2 | require "rake/extensioncompiler" 3 | 4 | libssh2_version = "1.4.0" 5 | openssl_version = "1.0.1" 6 | 7 | $recipes = {} 8 | $recipes[:libssh2] = MiniPortile.new("libssh2", libssh2_version) 9 | $recipes[:openssl] = MiniPortile.new("openssl", openssl_version) 10 | 11 | $recipes[:libssh2].files << "http://www.libssh2.org/download/libssh2-#{libssh2_version}.tar.gz" 12 | $recipes[:openssl].files << "http://www.openssl.org/source/openssl-#{openssl_version}.tar.gz" 13 | 14 | namespace :ports do 15 | directory "ports" 16 | 17 | desc "Compile libssh2" 18 | task :libssh2 => "ports" do 19 | recipe = $recipes[:libssh2] 20 | 21 | checkpoint = "ports/.#{recipe.name}.#{recipe.version}.#{recipe.host}.timestamp" 22 | if !File.exist?(checkpoint) 23 | recipe.cook 24 | touch checkpoint 25 | end 26 | 27 | recipe.activate 28 | end 29 | end 30 | 31 | namespace :cross do 32 | # This cross compiles OpenSSL for Windows. This is ONLY called by `cross` 33 | # and should not be invoked manually. 34 | task :openssl do 35 | recipe = $recipes[:openssl] 36 | 37 | class << recipe 38 | def configure 39 | cmd = ["perl"] 40 | cmd << "Configure" 41 | cmd << "mingw" 42 | cmd << "no-zlib" 43 | cmd << "no-shared" 44 | cmd << "--cross-compile-prefix=i686-w64-mingw32-" 45 | cmd << configure_prefix 46 | 47 | execute("configure", cmd.join(" ")) 48 | end 49 | 50 | def configured? 51 | false 52 | end 53 | end 54 | 55 | checkpoint = "ports/.#{recipe.name}.#{recipe.version}.#{recipe.host}.timestamp" 56 | if !File.exist?(checkpoint) 57 | recipe.cook 58 | touch checkpoint 59 | end 60 | 61 | recipe.activate 62 | end 63 | end 64 | 65 | # We need to patch the cross compilation task to compile our 66 | # ports prior to building. 67 | task :cross do 68 | host = ENV.fetch("HOST", Rake::ExtensionCompiler.mingw_host) 69 | $recipes.each do |_, recipe| 70 | recipe.host = host 71 | end 72 | 73 | # Make sure the port is compiled before cross compilation 74 | Rake::Task["compile"].prerequisites.unshift("cross:openssl", "ports:libssh2") 75 | end 76 | -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new 4 | --------------------------------------------------------------------------------