diff options
author | Ted Ross <tross@apache.org> | 2009-01-20 21:26:28 +0000 |
---|---|---|
committer | Ted Ross <tross@apache.org> | 2009-01-20 21:26:28 +0000 |
commit | 861692abf515cf136e86e70446d005e7849a0f87 (patch) | |
tree | 10025e3499876dbdae8697f2dda4c68b4ec55933 | |
parent | 15488eba1c867bac8b6db1f7ad870b277c4fa748 (diff) | |
download | qpid-python-861692abf515cf136e86e70446d005e7849a0f87.tar.gz |
QPID-1602 SASL Authentication, Authorization, and Security Layer for Ruby Client.
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk/qpid@736111 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r-- | ruby/Rakefile | 37 | ||||
-rw-r--r-- | ruby/ext/sasl/extconf.rb | 10 | ||||
-rw-r--r-- | ruby/ext/sasl/sasl.c | 432 | ||||
-rw-r--r-- | ruby/lib/qpid/delegates.rb | 36 | ||||
-rw-r--r-- | ruby/lib/qpid/framer.rb | 35 | ||||
-rw-r--r-- | ruby/lib/qpid/qmf.rb | 43 |
6 files changed, 564 insertions, 29 deletions
diff --git a/ruby/Rakefile b/ruby/Rakefile index 64044ca351..7ac6c09eeb 100644 --- a/ruby/Rakefile +++ b/ruby/Rakefile @@ -27,20 +27,48 @@ PKG_NAME='ruby-qpid' PKG_VERSION='0.10.2' GEM_NAME='qpid' +EXT_CONF="ext/sasl/extconf.rb" +MAKEFILE="ext/sasl/Makefile" +SASL_MODULE="ext/sasl/sasl.so" +SASL_SRC=SASL_MODULE.gsub(/.so$/, ".c") + # # Additional files for clean/clobber # -CLEAN.include [ "**/*~", "lib/*/spec_cache" ] +CLEAN.include [ "**/*~", "lib/*/spec_cache", SASL_MODULE, "ext/**/*.o" ] + +CLOBBER.include [ "config.save", "ext/**/mkmf.log", + MAKEFILE ] + +file MAKEFILE => EXT_CONF do |t| + Dir::chdir(File::dirname(EXT_CONF)) do + unless sh "ruby #{File::basename(EXT_CONF)}" + $stderr.puts "Failed to run extconf" + break + end + end +end + +file SASL_MODULE => [ MAKEFILE, SASL_SRC ] do |t| + Dir::chdir(File::dirname(EXT_CONF)) do + unless sh "make" + $stderr.puts "make failed" + break + end + end +end +desc "Build the native library" +task :build => SASL_MODULE Rake::TestTask.new(:test) do |t| t.test_files = FileList['tests/*.rb'].exclude("tests/util.rb") - t.libs = [ 'lib' ] + t.libs = [ 'lib', 'ext/sasl' ] end Rake::TestTask.new(:"test_0-8") do |t| t.test_files = FileList["tests_0-8/*.rb"] - t.libs = [ 'lib' ] + t.libs = [ 'lib', 'ext/sasl' ] end desc "Create cached versions of the AMQP specs" @@ -63,7 +91,8 @@ end PKG_FILES = FileList[ "DISCLAIMER", "LICENSE.txt", "NOTICE.txt", "Rakefile", "RELEASE_NOTES", - "lib/**/*.rb", "lib/*/spec_cache/*.rb*", "tests/**/*", "examples/**" + "lib/**/*.rb", "lib/*/spec_cache/*.rb*", "tests/**/*", "examples/**", "ext/**/*.[ch]", + "ext/**/MANIFEST", "ext/**/extconf.rb" ] DIST_FILES = FileList[ diff --git a/ruby/ext/sasl/extconf.rb b/ruby/ext/sasl/extconf.rb new file mode 100644 index 0000000000..5c9a24c35c --- /dev/null +++ b/ruby/ext/sasl/extconf.rb @@ -0,0 +1,10 @@ +require 'mkmf' + +extension_name = 'sasl' +have_library("c", "main") + +unless have_library("sasl2") + raise "Package cyrus-sasl-devel not found" +end + +create_makefile(extension_name) diff --git a/ruby/ext/sasl/sasl.c b/ruby/ext/sasl/sasl.c new file mode 100644 index 0000000000..2a2a829aa8 --- /dev/null +++ b/ruby/ext/sasl/sasl.c @@ -0,0 +1,432 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. +*/ + +#include <stdio.h> +#include <unistd.h> +#include <malloc.h> +#include <string.h> +#include <sasl/sasl.h> +#include <ruby.h> + +static VALUE mSasl; + +#define INPUT_SIZE 512 +#define MECH_SIZE 32 + +typedef void* sasl_context_t; + +#define QSASL_OK 0 +#define QSASL_CONTINUE 1 +#define QSASL_FAILED 2 + +typedef struct { + char magic[8]; + sasl_conn_t* conn; + sasl_callback_t callbacks[8]; + char* userName; + char* password; + unsigned int minSsf; + unsigned int maxSsf; + char mechanism[MECH_SIZE]; + char input[INPUT_SIZE]; +} context_t; + +// +// Resolve forward references +// +static VALUE qsasl_free(int, VALUE*, VALUE); + +// +// Validate an input string to ensure that it is either NULL or of reasonable size. +// +static int qsasl_valid(char* str) +{ + int idx; + + if (str == 0) + return 1; + + for (idx = 0; idx < INPUT_SIZE; idx++) { + if (str[idx] == '\0') + return 1; + } + + return 0; +} + +// +// SASL callback for identity and authentication identity. +// +static int qsasl_cb_user(void* _context, int id, const char **result, unsigned *len) +{ + context_t* context = (context_t*) _context; + + if (context->userName) + *result = context->userName; + + return SASL_OK; +} + +// +// SASL callback for passwords. +// +static int qsasl_cb_password(sasl_conn_t* conn, void* _context, int id, sasl_secret_t **psecret) +{ + context_t* context = (context_t*) _context; + sasl_secret_t* secret; + size_t length; + + if (context->password) + length = strlen(context->password); + else + length = 0; + + secret = (sasl_secret_t*) malloc(sizeof(sasl_secret_t) + length); + secret->len = length; + if (length) + memcpy(secret->data, context->password, length); + *psecret = secret; + + return SASL_OK; +} + +// +// Interactively prompt the user for authentication data. +// +static void qsasl_prompt(sasl_context_t _context, sasl_interact_t* interact) +{ + context_t* context = (context_t*) _context; + char *pass; + char *input; + char passwdPrompt[100]; + + if (interact->id == SASL_CB_PASS) { + strncpy(passwdPrompt, interact->prompt, 95); + strcat(passwdPrompt, ": "); + pass = getpass(passwdPrompt); + strncpy(context->input, pass, INPUT_SIZE - 1); + context->input[INPUT_SIZE - 1] = '\0'; + } else { + printf(interact->prompt); + if (interact->defresult) { + printf(" (%s)", interact->defresult); + } + printf(": "); + input = fgets(context->input, INPUT_SIZE, stdin); + if (input != context->input) { + rb_raise(rb_eRuntimeError, "Unexpected EOF on interactive prompt"); + } + } + + interact->result = context->input; + interact->len = strlen(context->input); +} + +// +// Initialize the SASL client library. +// +static VALUE qsasl_client_init() +{ + int result; + + result = sasl_client_init(0); + if (result != SASL_OK) + rb_raise(rb_eRuntimeError, + "sasl_client_init failed: %d - %s", + result, sasl_errstring(result, -0, 0)); + return Qnil; +} + +// +// Allocate a new SASL client context. +// +static VALUE qsasl_client_new(int argc, VALUE *argv, VALUE obj) +{ + char* mechanism = 0; + char* serviceName = 0; + char* hostName = 0; + char* userName = 0; + char* password = 0; + unsigned int minSsf = 0; + unsigned int maxSsf = 65535; + + int result; + int i = 0; + context_t *context; + sasl_security_properties_t secprops; + + if (argc != 7) + rb_raise(rb_eRuntimeError, "Wrong number of arguments"); + + if (!NIL_P(argv[0])) + mechanism = StringValuePtr(argv[0]); + if (!NIL_P(argv[1])) + serviceName = StringValuePtr(argv[1]); + if (!NIL_P(argv[2])) + hostName = StringValuePtr(argv[2]); + if (!NIL_P(argv[3])) + userName = StringValuePtr(argv[3]); + if (!NIL_P(argv[4])) + password = StringValuePtr(argv[4]); + minSsf = FIX2INT(argv[5]); + maxSsf = FIX2INT(argv[6]); + + if (!qsasl_valid(mechanism) || !qsasl_valid(serviceName) || + !qsasl_valid(hostName) || !qsasl_valid(userName) || + !qsasl_valid(password)) { + rb_raise(rb_eRuntimeError, "Invalid string argument"); + } + + context = (context_t*) malloc(sizeof(context_t)); + memset(context, 0, sizeof(context_t)); + strcpy(context->magic, "QSASL01"); + + context->minSsf = minSsf; + context->maxSsf = maxSsf; + if (mechanism != 0) { + strncpy(context->mechanism, mechanism, MECH_SIZE - 1); + context->mechanism[MECH_SIZE - 1] = '\0'; + } + + context->callbacks[i].id = SASL_CB_GETREALM; + context->callbacks[i].proc = 0; + context->callbacks[i++].context = 0; + + if (userName != 0 && userName[0] != '\0') { + context->userName = (char*) malloc(strlen(userName) + 1); + strcpy(context->userName, userName); + + context->callbacks[i].id = SASL_CB_USER; + context->callbacks[i].proc = qsasl_cb_user; + context->callbacks[i++].context = context; + + context->callbacks[i].id = SASL_CB_AUTHNAME; + context->callbacks[i].proc = qsasl_cb_user; + context->callbacks[i++].context = context; + } + + context->callbacks[i].id = SASL_CB_PASS; + if (password != 0 && password[0] != '\0') { + context->password = (char*) malloc(strlen(password) + 1); + strcpy(context->password, password); + + context->callbacks[i].proc = qsasl_cb_password; + } else + context->callbacks[i].proc = 0; + context->callbacks[i++].context = context; + + context->callbacks[i].id = SASL_CB_LIST_END; + context->callbacks[i].proc = 0; + context->callbacks[i++].context = 0; + + result = sasl_client_new(serviceName, hostName, 0, 0, + context->callbacks, 0, &context->conn); + + if (result != SASL_OK) { + context->conn = 0; + qsasl_free(1, (VALUE*) &context, Qnil); + rb_raise(rb_eRuntimeError, "sasl_client_new failed: %d - %s", + result, sasl_errstring(result, 0, 0)); + } + + secprops.min_ssf = minSsf; + secprops.max_ssf = maxSsf; + secprops.maxbufsize = 65535; + secprops.property_names = 0; + secprops.property_values = 0; + secprops.security_flags = 0;//TODO: provide means for application to configure these + + result = sasl_setprop(context->conn, SASL_SEC_PROPS, &secprops); + if (result != SASL_OK) { + qsasl_free(1, (VALUE*) &context, Qnil); + rb_raise(rb_eRuntimeError, "sasl_setprop failed: %d - %s", + result, sasl_errdetail(context->conn)); + } + + return (VALUE) context; +} + +// +// Free a SASL client context. +// +static VALUE qsasl_free(int argc, VALUE *argv, VALUE obj) +{ + context_t* context; + + if (argc == 1) + context = (context_t*) argv[0]; + else + rb_raise(rb_eRuntimeError, "Wrong Number of Arguments"); + + if (context->conn) + sasl_dispose(&context->conn); + if (context->userName) + free(context->userName); + if (context->password) + free(context->password); + free(context); + + return Qnil; +} + +// +// Start the SASL exchange from the client's point of view. +// +static VALUE qsasl_client_start(int argc, VALUE *argv, VALUE obj) +{ + context_t* context; + char* mechList; + char* mechToUse; + int result; + const char* response; + unsigned int len; + sasl_interact_t* interact = 0; + const char* chosen; + + if (argc == 2) { + context = (context_t*) argv[0]; + mechList = StringValuePtr(argv[1]); + } else + rb_raise(rb_eRuntimeError, "Wrong Number of Arguments"); + + if (strlen(context->mechanism) == 0) + mechToUse = mechList; + else + mechToUse = context->mechanism; + + do { + result = sasl_client_start(context->conn, mechToUse, &interact, + &response, &len, &chosen); + if (result == SASL_INTERACT) { + qsasl_prompt(context, interact); + } + } while (result == SASL_INTERACT); + + if (result != SASL_OK && result != SASL_CONTINUE) + rb_raise(rb_eRuntimeError, "sasl_client_start failed: %d - %s", + result, sasl_errdetail(context->conn)); + + return rb_ary_new3(3, INT2NUM(result), rb_str_new(response, len), rb_str_new2(chosen)); +} + +// +// Take a step in the SASL exchange (only needed for multi-challenge mechanisms). +// +static VALUE qsasl_client_step(int argc, VALUE *argv, VALUE obj) +{ + context_t* context; + VALUE challenge; + int result; + const char* response; + unsigned int len; + sasl_interact_t* interact = 0; + + if (argc == 2) { + context = (context_t*) argv[0]; + challenge = argv[1]; + } + else + rb_raise(rb_eRuntimeError, "Wrong Number of Arguments"); + + do { + result = sasl_client_step(context->conn, + RSTRING(challenge)->ptr, RSTRING(challenge)->len, + &interact, &response, &len); + if (result == SASL_INTERACT) { + qsasl_prompt(context, interact); + } + } while (result == SASL_INTERACT); + + if (result != SASL_OK && result != SASL_CONTINUE) + return QSASL_FAILED; + + return rb_ary_new3(2, INT2NUM(result), rb_str_new(response, len)); +} + +// +// Encode transport data for the security layer. +// +static VALUE qsasl_encode(int argc, VALUE *argv, VALUE obj) +{ + context_t* context; + VALUE clearText; + const char* outBuffer; + unsigned int outSize; + int result; + + if (argc == 2) { + context = (context_t*) argv[0]; + clearText = argv[1]; + } + else + rb_raise(rb_eRuntimeError, "Wrong Number of Arguments"); + + result = sasl_encode(context->conn, + RSTRING(clearText)->ptr, RSTRING(clearText)->len, + &outBuffer, &outSize); + if (result != SASL_OK) + rb_raise(rb_eRuntimeError, "sasl_encode failed: %d - %s", + result, sasl_errdetail(context->conn)); + + return rb_str_new(outBuffer, outSize); +} + +// +// Decode transport data for the security layer. +// +static VALUE qsasl_decode(int argc, VALUE *argv, VALUE obj) +{ + context_t* context; + VALUE cipherText; + const char* outBuffer; + unsigned int outSize; + int result; + + if (argc == 2) { + context = (context_t*) argv[0]; + cipherText = argv[1]; + } + else + rb_raise(rb_eRuntimeError, "Wrong Number of Arguments"); + + result = sasl_decode(context->conn, + RSTRING(cipherText)->ptr, RSTRING(cipherText)->len, + &outBuffer, &outSize); + if (result != SASL_OK) + rb_raise(rb_eRuntimeError, "sasl_decode failed: %d - %s", + result, sasl_errdetail(context->conn)); + + return rb_str_new(outBuffer, outSize); +} + +// +// Initialize the Sasl module. +// +void Init_sasl() +{ + mSasl = rb_define_module("Sasl"); + + rb_define_module_function(mSasl, "client_init", qsasl_client_init, -1); + rb_define_module_function(mSasl, "client_new", qsasl_client_new, -1); + rb_define_module_function(mSasl, "free", qsasl_free, -1); + rb_define_module_function(mSasl, "client_start", qsasl_client_start, -1); + rb_define_module_function(mSasl, "client_step", qsasl_client_step, -1); + rb_define_module_function(mSasl, "encode", qsasl_encode, -1); + rb_define_module_function(mSasl, "decode", qsasl_decode, -1); +} diff --git a/ruby/lib/qpid/delegates.rb b/ruby/lib/qpid/delegates.rb index 21513fc677..9707cdbc76 100644 --- a/ruby/lib/qpid/delegates.rb +++ b/ruby/lib/qpid/delegates.rb @@ -18,6 +18,7 @@ # require 'rbconfig' +require 'sasl' module Qpid @@ -173,9 +174,17 @@ module Qpid def initialize(connection, args) super(connection) - @username = args[:username] || "guest" - @password = args[:password] || "guest" - @mechanism= args[:mechanism] || "PLAIN" + result = Sasl::client_init + + @mechanism= args[:mechanism] + @username = args[:username] + @password = args[:password] + @service = args[:service] || "qpidd" + @min_ssf = args[:min_ssf] || 0 + @max_ssf = args[:max_ssf] || 65535 + + @saslConn = Sasl.client_new(@mechanism, @service, args[:host], + @username, @password, @min_ssf, @max_ssf) end def start @@ -184,18 +193,31 @@ module Qpid end def connection_start(ch, start) - r = "\0%s\0%s" % [@username, @password] + mech_list = "" + start.mechanisms.each do |m| + mech_list += m + " " + end + resp = Sasl.client_start(@saslConn, mech_list) ch.connection_start_ok(:client_properties => PROPERTIES, - :mechanism => @mechanism, - :response => r) + :mechanism => resp[2], + :response => resp[1]) + end + + def connection_secure(ch, secure) + resp = Sasl.client_step(@saslConn, secure.challenge) + ch.connection_secure_ok(:response => resp[1]) end def connection_tune(ch, tune) - ch.connection_tune_ok() + ch.connection_tune_ok(:channel_max => tune.channel_max, + :max_frame_size => tune.max_frame_size, + :heartbeat => 0) ch.connection_open() + @connection.security_layer_tx = @saslConn end def connection_open_ok(ch, open_ok) + @connection.security_layer_rx = @saslConn @connection.opened = true @connection.signal end diff --git a/ruby/lib/qpid/framer.rb b/ruby/lib/qpid/framer.rb index 2a565a69a8..abac221f00 100644 --- a/ruby/lib/qpid/framer.rb +++ b/ruby/lib/qpid/framer.rb @@ -19,6 +19,7 @@ require 'monitor' require 'logger' +require 'sasl' module Qpid @@ -109,21 +110,31 @@ module Qpid def initialize(sock) @sock = sock @sock.extend(MonitorMixin) - @buf = "" + @tx_buf = "" + @rx_buf = "" + @security_layer_tx = nil + @security_layer_rx = nil + @maxbufsize = 65535 end attr_reader :sock + attr_accessor :security_layer_tx, :security_layer_rx def aborted? ; false ; end def write(buf) - @buf += buf + @tx_buf += buf end def flush @sock.synchronize do - _write(@buf) - @buf = "" + if @security_layer_tx + cipher_buf = Sasl.encode(@security_layer_tx, @tx_buf) + _write(cipher_buf) + else + _write(@tx_buf) + end + @tx_buf = "" frm.debug("FLUSHED") if frm end end @@ -139,12 +150,14 @@ module Qpid end def read(n) - data = "" - while data.size < n + while @rx_buf.size < n begin - s = @sock.read(n - data.size) + s = @sock.recv(@maxbufsize) + if @security_layer_rx + s = Sasl.decode(@security_layer_rx, s) + end rescue IOError => e - raise e if data != "" + raise e if @rx_buf != "" @sock.close unless @sock.closed? raise Closed end @@ -153,9 +166,11 @@ module Qpid @sock.close unless @sock.closed? raise Closed end - data += s - raw.debug("RECV #{n}/#{data.size} #{s.inspect}") if raw + @rx_buf += s + raw.debug("RECV #{n}/#{@rx_buf.size} #{s.inspect}") if raw end + data = @rx_buf[0, n] + @rx_buf = @rx_buf[n, @rx_buf.size - n] return data end diff --git a/ruby/lib/qpid/qmf.rb b/ruby/lib/qpid/qmf.rb index 0309b65a6c..ee165305c3 100644 --- a/ruby/lib/qpid/qmf.rb +++ b/ruby/lib/qpid/qmf.rb @@ -65,16 +65,15 @@ module Qpid::Qmf class BrokerURL - attr_reader :host, :port, :auth_name, :auth_pass, :auth_mech + attr_reader :host, :port, :auth_name, :auth_pass def initialize(text) uri = URI.parse(text) @host = uri.host @port = uri.port ? uri.port : 5672 - @auth_name = uri.user ? uri.user : "guest" - @auth_pass = uri.password ? uri.password: "guest" - @auth_mech = "PLAIN" + @auth_name = uri.user + @auth_pass = uri.password return uri end @@ -178,9 +177,32 @@ module Qpid::Qmf end # Connect to a Qpid broker. Returns an object of type Broker - def add_broker(target="amqp://localhost") + # + # To supply a username for authentication, use the URL syntax: + # + # amqp://username@hostname:port + # + # If the broker needs a password for the client, an interactive prompt will be + # provided to the user. + # + # To supply a username and a password, use + # + # amqp://username:password@hostname:port + # + # The following keyword arguments may be used to control authentication: + # + # :mechanism - SASL mechanism (i.e. "PLAIN", "GSSAPI", "ANONYMOUS", etc. + # - defaults to unspecified (the system chooses for you) + # :service - SASL service name (i.e. the kerberos principal of the broker) + # - defaults to "qpidd" + # :min_ssf - Minimum Security Strength Factor for SASL security layers + # - defaults to 0 + # :max_ssf - Maximum Security Strength Factor for SASL security layers + # - defaults to 65535 + # + def add_broker(target = "amqp://localhost", kwargs = {}) url = BrokerURL.new(target) - broker = Broker.new(self, url.host, url.port, url.auth_mech, url.auth_name, url.auth_pass) + broker = Broker.new(self, url.host, url.port, url.auth_name, url.auth_pass, kwargs) unless broker.connected? || @manage_connections raise broker.error end @@ -1201,7 +1223,7 @@ module Qpid::Qmf attr_accessor :broker_id, :sync_result - def initialize(session, host, port, auth_mech, auth_name, auth_pass) + def initialize(session, host, port, auth_name, auth_pass, kwargs) super() # For debugging.. @@ -1212,6 +1234,8 @@ module Qpid::Qmf @port = port @auth_name = auth_name @auth_pass = auth_pass + @auth_mechanism = kwargs[:mechanism] + @auth_service = kwargs[:service] @broker_bank = 1 @agents = {} @agents["1.0"] = Agent.new(self, 0, "BrokerAgent") @@ -1368,8 +1392,11 @@ module Qpid::Qmf # FIXME: Need sth for Qpid::Util::connect @conn = Qpid::Connection.new(TCPSocket.new(@host, @port), + :mechanism => @auth_mechanism, :username => @auth_name, - :password => @auth_pass) + :password => @auth_pass, + :host => @host, + :service => @auth_service) @conn.start @reply_name = "reply-%s" % amqp_session_id @amqp_session = @conn.session(@amqp_session_id) |