diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5420cd..91ed7c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [2.5, 2.6, 2.7, jruby, truffleruby] + ruby: [2.5, 2.6, 2.7, jruby] runs-on: ubuntu-latest steps: - uses: ruby/setup-ruby@v1 @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: os: [macos] - ruby: [2.5, 2.6, 2.7, jruby, truffleruby] + ruby: [2.5, 2.6, 2.7, jruby] runs-on: macos-latest steps: - uses: ruby/setup-ruby@v1 diff --git a/.rubocop.yml b/.rubocop.yml index cccf698..9e3c81b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -19,12 +19,16 @@ Metrics/BlockLength: Exclude: - 'gems/aws-crt/Rakefile' - '**/*.rake' - - 'gems/aws-crt/spec/**/*.rb' + - 'gems/**/spec/**/*.rb' Metrics/MethodLength: + Max: 25 Exclude: - 'gems/aws-crt/ext/compile.rb' +Metrics/AbcSize: + Max: 20 + Naming/FileName: Exclude: - 'gems/aws-crt/lib/aws-crt.rb' diff --git a/format-check.sh b/format-check.sh index 8429423..9d19e14 100755 --- a/format-check.sh +++ b/format-check.sh @@ -10,7 +10,7 @@ if NOT type $CLANG_FORMAT 2> /dev/null ; then fi FAIL=0 -SOURCE_FILES=`find aws-crt/native/src -type f \( -name '*.h' -o -name '*.c' \)` +SOURCE_FILES=`find gems/aws-crt/native/src -type f \( -name '*.h' -o -name '*.c' \)` for i in $SOURCE_FILES do $CLANG_FORMAT -output-replacements-xml $i | grep -c " /dev/null diff --git a/gems/aws-crt-auth/lib/aws-crt-auth.rb b/gems/aws-crt-auth/lib/aws-crt-auth.rb index 46ba442..ee45cba 100644 --- a/gems/aws-crt-auth/lib/aws-crt-auth.rb +++ b/gems/aws-crt-auth/lib/aws-crt-auth.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'aws-crt' +require_relative 'aws-crt-auth/credentials' module Aws module Crt diff --git a/gems/aws-crt-auth/lib/aws-crt-auth/credentials.rb b/gems/aws-crt-auth/lib/aws-crt-auth/credentials.rb new file mode 100644 index 0000000..1c62941 --- /dev/null +++ b/gems/aws-crt-auth/lib/aws-crt-auth/credentials.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Aws + module Crt + module Auth + # Utility class for Credentials. + class Credentials + include Aws::Crt::ManagedNative + native_destroy Aws::Crt::Native.method(:credentials_release) + + UINT64_MAX = 18_446_744_073_709_551_615 + + # @param [String] access_key_id + # @param [String] secret_access_key + # @param [String] session_token (nil) + # @param [Time|int] expiration (nil) - Either a Time or an int + # seconds since unix epoch + def initialize(access_key_id, secret_access_key, + session_token = nil, expiration = nil) + if !access_key_id || access_key_id.empty? + raise ArgumentError, 'access_key_id must be set' + end + + if !secret_access_key || secret_access_key.empty? + raise ArgumentError, 'secret_access_key must be set' + end + + manage_native do + Aws::Crt::Native.credentials_new( + access_key_id, + secret_access_key, + session_token, + expiration&.to_i || UINT64_MAX + ) + end + end + + # @return [String] + def access_key_id + Aws::Crt::Native.credentials_get_access_key_id(native).to_s + end + + # @return [String] + def secret_access_key + Aws::Crt::Native.credentials_get_secret_access_key(native).to_s + end + + # @return [String, nil] + def session_token + Aws::Crt::Native.credentials_get_session_token(native).to_s + end + + # @return [Time,nil] + def expiration + exp = Aws::Crt::Native.credentials_get_expiration_timepoint_seconds!( + native + ) + return if exp == UINT64_MAX + + Time.at(exp) + end + + # Removing the secret access key from the default inspect string. + # @api private + def inspect + "#<#{self.class.name} access_key_id=#{access_key_id.inspect}>" + end + end + end + end +end diff --git a/gems/aws-crt-auth/spec/credentials_spec.rb b/gems/aws-crt-auth/spec/credentials_spec.rb new file mode 100644 index 0000000..a86b232 --- /dev/null +++ b/gems/aws-crt-auth/spec/credentials_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' +require 'weakref' + +module Aws + module Crt + module Auth #:nodoc: + describe Credentials do + describe '#initilize' do + it 'raises an ArgumentError when missing access_key_id' do + expect { Credentials.new(nil, 'secret') } + .to raise_error(ArgumentError) + end + + it 'raises an ArgumentError when missing secret_access_key' do + expect { Credentials.new('akid', nil) } + .to raise_error(ArgumentError) + end + + it 'defaults the session token to nil' do + expect(Credentials.new('akid', 'secret').session_token).to be nil + end + + it 'defaults the expiration to nil' do + expect(Credentials.new('akid', 'secret').expiration) + .to be_nil + end + + it 'accepts a Time for expiration' do + exp = Time.now + creds = Credentials.new('akid', 'secret', 'token', exp) + expect(creds.expiration.to_i).to eq exp.to_i + end + + it 'accepts an epoch (integer) for expiration' do + exp = Time.now + creds = Credentials.new('akid', 'secret', 'token', exp.to_i) + expect(creds.expiration.to_i).to eq exp.to_i + end + end + + describe 'accessors' do + let(:exp) { Time.now } + let(:creds) { Credentials.new('akid', 'secret', 'token', exp) } + + it 'provides access to the access key id' do + expect(creds.access_key_id).to eq('akid') + end + + it 'provides access to the secret access key' do + expect(creds.secret_access_key).to eq('secret') + end + + it 'provides access to the session token' do + expect(creds.session_token).to eq('token') + end + + it 'provides access to the expiration' do + expect(creds.expiration.to_i).to eq exp.to_i + end + end + + describe '#inspect' do + let(:creds) { Credentials.new('akid', 'secret', 'token') } + + it 'does not include the secret_access_key' do + expect(creds.inspect).not_to include 'secret' + end + end + + describe '.on_release' do + it 'cleans up with release' do + creds = Credentials.new('akid', 'secret') + expect(creds).to_not be_nil + + creds.release + check_for_clean_shutdown + end + + if garbage_collect_is_immediate? + it 'cleans up with GC' do + creds = Credentials.new('akid', 'secret', 'session') + weakref = WeakRef.new(creds) + expect(weakref.weakref_alive?).to be true + + # force cleanup via GC + creds = nil # rubocop:disable Lint/UselessAssignment + ObjectSpace.garbage_collect + expect(weakref.weakref_alive?).to be_falsey + check_for_clean_shutdown + end + end + end + end + end + end +end diff --git a/gems/aws-crt-auth/spec/spec_helper.rb b/gems/aws-crt-auth/spec/spec_helper.rb index 1f72a1f..9f4b41c 100644 --- a/gems/aws-crt-auth/spec/spec_helper.rb +++ b/gems/aws-crt-auth/spec/spec_helper.rb @@ -5,4 +5,5 @@ # use the local version of aws-crt $LOAD_PATH.unshift File.expand_path('../../aws-crt/lib', __dir__) +require_relative '../../aws-crt/spec/spec_helper' require 'aws-crt-auth' diff --git a/gems/aws-crt/lib/aws-crt.rb b/gems/aws-crt/lib/aws-crt.rb index cafc1e2..3bf052b 100644 --- a/gems/aws-crt/lib/aws-crt.rb +++ b/gems/aws-crt/lib/aws-crt.rb @@ -3,6 +3,7 @@ require_relative 'aws-crt/platforms' require_relative 'aws-crt/native' require_relative 'aws-crt/errors' +require_relative 'aws-crt/managed_native' require_relative 'aws-crt/io' # Top level Amazon Web Services (AWS) namespace @@ -11,19 +12,5 @@ module Aws module Crt # Ensure native init() is called when gem loads Aws::Crt::Native.init - - # Invoke native call, and raise exception if it failed - def self.call - res = yield - # functions that return void cannot fail - return unless res - - # for functions that return int, non-zero indicates failure - Errors.raise_last_error if res.is_a?(Integer) && res != 0 - - # for functions that return pointer, NULL indicates failure - Errors.raise_last_error if res.is_a?(FFI::Pointer) && res.null? - res - end end end diff --git a/gems/aws-crt/lib/aws-crt/io.rb b/gems/aws-crt/lib/aws-crt/io.rb index 8066fad..0ef1dbb 100644 --- a/gems/aws-crt/lib/aws-crt/io.rb +++ b/gems/aws-crt/lib/aws-crt/io.rb @@ -9,6 +9,9 @@ module IO # Classes that need to do async work will ask the EventLoopGroup # for an event-loop to use. class EventLoopGroup + include Aws::Crt::ManagedNative + native_destroy Aws::Crt::Native.method(:event_loop_group_release) + def initialize(max_threads = nil) unless max_threads.nil? || (max_threads.is_a?(Integer) && max_threads.positive?) @@ -18,26 +21,9 @@ def initialize(max_threads = nil) # Ruby uses nil to request default values, native code uses 0 max_threads = 0 if max_threads.nil? - native = Aws::Crt.call do + manage_native do Aws::Crt::Native.event_loop_group_new(max_threads) end - - @native = FFI::AutoPointer.new(native, self.class.method(:on_release)) - end - - # Immediately release this instance's attachment to the underlying - # resources, without waiting for the garbage collector. - # Note that underlying resources will remain alive until nothing - # else is using them. - def release - return unless @native - - @native.free - @native = nil - end - - def self.on_release(native) - Aws::Crt::Native.event_loop_group_release(native) end end end diff --git a/gems/aws-crt/lib/aws-crt/managed_native.rb b/gems/aws-crt/lib/aws-crt/managed_native.rb new file mode 100644 index 0000000..e1f8081 --- /dev/null +++ b/gems/aws-crt/lib/aws-crt/managed_native.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Aws + module Crt + # A mixin module for generic managed native functionality + # Example: + # + # class C + # include Aws::Crt::ManagedNative + # native_destroy Aws::Crt::Native.method(:test_struct_destroy) + # + # def initialize + # manage_native { Aws::Crt::Native::test_struct_new() } + # end + # + # def use_native + # Aws::Crt::Native::test_method(native) #use that getter for native + # end + # end + module ManagedNative + def self.included(sub_class) + sub_class.extend(ClassMethods) + end + + # expects a block that returns a :pointer to the native resource + # that this class manages + def manage_native(&block) + # check that a destructor has been registered + unless self.class.instance_variable_get('@destructor') + raise 'No native destructor registered. use native_destroy to ' \ + 'set the method used to cleanup the native object this ' \ + 'class manages.' + end + native = block.call + @native = FFI::AutoPointer.new(native, self.class.method(:on_release)) + end + + # @param [Boolean] safe (true) - raise an exception if the native object + # is not set (has been freed or never created) + # @return [FFI:Pointer] + def native(safe: true) + raise '@native is unset or has been freed.' if safe && !@native + + @native + end + + # @return [Boolean] + def native_set? + !!@native + end + + # Immediately release this instance's attachment to the underlying + # resources, without waiting for the garbage collector. + # Note that underlying resources will remain alive until nothing + # else is using them. + def release + return unless @native + + @native.free + @native = nil + end + + # ClassMethods for ManagedNative + module ClassMethods + # Register the method used to cleanup the native object this class + # manages. Must be a method, use object.method(:method_name). + # + # Example: + # native_destroy Aws::Crt::Native.method(:test_release) + def native_destroy(destructor) + unless destructor.is_a?(Method) + raise ArgumentError, 'destructor must be a Method. ' \ + 'Use object.method(:method_name)' + end + @destructor = destructor + end + + # Do not call directly + # method passed to FFI Autopointer to call the destructor + def on_release(native) + @destructor.call(native) + end + end + end + end +end diff --git a/gems/aws-crt/lib/aws-crt/native.rb b/gems/aws-crt/lib/aws-crt/native.rb index 7ed5562..9b202e6 100644 --- a/gems/aws-crt/lib/aws-crt/native.rb +++ b/gems/aws-crt/lib/aws-crt/native.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'ffi' - module Aws module Crt # FFI Bindings to native CRT functions @@ -10,20 +9,81 @@ module Native ffi_lib [crt_bin_path(local_platform), 'libaws-crt'] + # aws_byte_cursor binding + class ByteCursor < FFI::Struct + layout :len, :size_t, + :ptr, :pointer + + def to_s + return unless (self[:len]).positive? && !(self[:ptr]).null? + + self[:ptr].read_string(self[:len]) + end + end + + # Extends FFI::attach_function + # + # 1. Allows us to only supply the aws_crt C name and removes + # the aws_crt. + # 2. Wraps the call in an error-raise checker (unless options[:raise] + # = false) + # 3. Creates a bang method that does not do automatic error checking. + def self.attach_function(c_name, params, returns, options = {}) + ruby_name = c_name.to_s.sub(/aws_crt_/, '').to_sym + raise_errors = options.fetch(:raise, true) + options.delete(:raise) + unless raise_errors + return super(ruby_name, c_name, params, returns, options) + end + + bang_name = "#{ruby_name}!" + + super(ruby_name, c_name, params, returns, options) + alias_method(bang_name, ruby_name) + + define_method(ruby_name) do |*args, &block| + res = public_send(bang_name, *args, &block) + # functions that return void cannot fail + return unless res + + # for functions that return int, non-zero indicates failure + Errors.raise_last_error if res.is_a?(Integer) && res != 0 + + # for functions that return pointer, NULL indicates failure + Errors.raise_last_error if res.is_a?(FFI::Pointer) && res.null? + + res + end + + module_function ruby_name + module_function bang_name + end + # Core API - attach_function :init, :aws_crt_init, [], :void - attach_function :last_error, :aws_crt_last_error, [], :int - attach_function :error_str, :aws_crt_error_str, [:int], :string - attach_function :error_name, :aws_crt_error_name, [:int], :string - attach_function :error_debug_str, :aws_crt_error_debug_str, [:int], :string - attach_function :reset_error, :aws_crt_reset_error, [], :void - attach_function :global_thread_creator_shutdown_wait_for, :aws_crt_global_thread_creator_shutdown_wait_for, [:uint32], :int + attach_function :aws_crt_init, [], :void, raise: false + attach_function :aws_crt_last_error, [], :int, raise: false + attach_function :aws_crt_error_str, [:int], :string, raise: false + attach_function :aws_crt_error_name, [:int], :string, raise: false + attach_function :aws_crt_error_debug_str, [:int], :string, raise: false + attach_function :aws_crt_reset_error, [], :void, raise: false + + attach_function :aws_crt_global_thread_creator_shutdown_wait_for, [:uint32], :int + # IO API - attach_function :event_loop_group_new, :aws_crt_event_loop_group_new, [:uint16], :pointer - attach_function :event_loop_group_release, :aws_crt_event_loop_group_release, [:pointer], :void + attach_function :aws_crt_event_loop_group_new, [:uint16], :pointer + attach_function :aws_crt_event_loop_group_release, [:pointer], :void + + # Auth API + attach_function :aws_crt_credentials_new, %i[string string string uint64], :pointer + attach_function :aws_crt_credentials_release, [:pointer], :void + attach_function :aws_crt_credentials_get_access_key_id, [:pointer], ByteCursor.by_value + attach_function :aws_crt_credentials_get_secret_access_key, [:pointer], ByteCursor.by_value + attach_function :aws_crt_credentials_get_session_token, [:pointer], ByteCursor.by_value + attach_function :aws_crt_credentials_get_expiration_timepoint_seconds, [:pointer], :uint64 + # Internal testing API - attach_function :test_error, :aws_crt_test_error, [:int], :int - attach_function :test_pointer_error, :aws_crt_test_pointer_error, [], :pointer + attach_function :aws_crt_test_error, [:int], :int + attach_function :aws_crt_test_pointer_error, [], :pointer end end end diff --git a/gems/aws-crt/native/src/api.h b/gems/aws-crt/native/src/api.h index 910b0f0..102e065 100644 --- a/gems/aws-crt/native/src/api.h +++ b/gems/aws-crt/native/src/api.h @@ -42,6 +42,18 @@ AWS_CRT_API void aws_crt_reset_error(void); AWS_CRT_API struct aws_event_loop_group *aws_crt_event_loop_group_new(uint16_t max_threads); AWS_CRT_API void aws_crt_event_loop_group_release(struct aws_event_loop_group *elg); +/* Auth */ +AWS_CRT_API struct aws_credentials *aws_crt_credentials_new( + const char *access_key_id, + const char *secret_access_key, + const char *session_token, + uint64_t expiration_timepoint_seconds); +AWS_CRT_API void aws_crt_credentials_release(struct aws_credentials *credentials); +AWS_CRT_API struct aws_byte_cursor aws_crt_credentials_get_access_key_id(const struct aws_credentials *credentials); +AWS_CRT_API struct aws_byte_cursor aws_crt_credentials_get_secret_access_key(const struct aws_credentials *credentials); +AWS_CRT_API struct aws_byte_cursor aws_crt_credentials_get_session_token(const struct aws_credentials *credentials); +AWS_CRT_API uint64_t aws_crt_credentials_get_expiration_timepoint_seconds(const struct aws_credentials *credentials); + AWS_EXTERN_C_END #endif /* AWS_CRT_API_H */ diff --git a/gems/aws-crt/native/src/credentials.c b/gems/aws-crt/native/src/credentials.c new file mode 100644 index 0000000..5888439 --- /dev/null +++ b/gems/aws-crt/native/src/credentials.c @@ -0,0 +1,43 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include "crt.h" + +#include +#include + +struct aws_credentials *aws_crt_credentials_new( + const char *access_key_id, + const char *secret_access_key, + const char *session_token, + uint64_t expiration_timepoint_seconds) { + + struct aws_allocator *allocator = aws_crt_allocator(); + return aws_credentials_new( + allocator, + aws_byte_cursor_from_c_str(access_key_id), + aws_byte_cursor_from_c_str(secret_access_key), + aws_byte_cursor_from_c_str(session_token), + expiration_timepoint_seconds); +} + +struct aws_byte_cursor aws_crt_credentials_get_access_key_id(const struct aws_credentials *credentials) { + return aws_credentials_get_access_key_id(credentials); +} + +struct aws_byte_cursor aws_crt_credentials_get_secret_access_key(const struct aws_credentials *credentials) { + return aws_credentials_get_secret_access_key(credentials); +} + +struct aws_byte_cursor aws_crt_credentials_get_session_token(const struct aws_credentials *credentials) { + return aws_credentials_get_session_token(credentials); +} + +uint64_t aws_crt_credentials_get_expiration_timepoint_seconds(const struct aws_credentials *credentials) { + return aws_credentials_get_expiration_timepoint_seconds(credentials); +} + +void aws_crt_credentials_release(struct aws_credentials *credentials) { + aws_credentials_release(credentials); +} diff --git a/gems/aws-crt/spec/crt_spec.rb b/gems/aws-crt/spec/crt_spec.rb deleted file mode 100644 index 960c1ed..0000000 --- a/gems/aws-crt/spec/crt_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require_relative 'spec_helper' - -describe Aws::Crt do - describe '#call' do - it 'raises an error when called on a function that returns an int' do - expect do - Aws::Crt.call { Aws::Crt::Native.test_error(3) } - end.to raise_error(Aws::Crt::Error) - end - - it 'raises an error when called on a function that returns a pointer' do - expect do - Aws::Crt.call { Aws::Crt::Native.test_pointer_error } - end.to raise_error(NoMemoryError) - end - end -end diff --git a/gems/aws-crt/spec/errors_spec.rb b/gems/aws-crt/spec/errors_spec.rb index 500c433..8385a45 100644 --- a/gems/aws-crt/spec/errors_spec.rb +++ b/gems/aws-crt/spec/errors_spec.rb @@ -5,21 +5,21 @@ describe Aws::Crt::Errors do describe '.raise_last_error' do it 'translates and raises the last error' do - Aws::Crt::Native.test_error(3) # generate an error + Aws::Crt::Native.test_error!(3) # generate an error expect do Aws::Crt::Errors.raise_last_error end.to raise_error(Aws::Crt::Error) end it 'does not raise when no error' do - Aws::Crt::Native.test_error(0) # success + Aws::Crt::Native.test_error!(0) # success expect do Aws::Crt::Errors.raise_last_error end.not_to raise_error end it 'resets the error after raising it' do - Aws::Crt::Native.test_error(3) # raise error + Aws::Crt::Native.test_error!(3) # raise error expect do Aws::Crt::Errors.raise_last_error end.to raise_error(Aws::Crt::Error) diff --git a/gems/aws-crt/spec/native_spec.rb b/gems/aws-crt/spec/native_spec.rb new file mode 100644 index 0000000..49ad05f --- /dev/null +++ b/gems/aws-crt/spec/native_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' + +describe Aws::Crt::Native do + describe '.attach_function' do + it 'removes the aws_crt_ prefix from C functions' do + expect(Aws::Crt::Native).to respond_to(:test_error) + expect(Aws::Crt::Native).not_to respond_to(:aws_crt_test_error) + end + + it 'creates a ! version of the function' do + expect(Aws::Crt::Native).to respond_to(:test_error!) + end + + it 'raises an error when called on a function that returns an int' do + expect do + Aws::Crt::Native.test_error(3) + end.to raise_error(Aws::Crt::Error) + end + + it 'raises an error when called on a function that returns a pointer' do + expect do + Aws::Crt::Native.test_pointer_error + end.to raise_error(NoMemoryError) + end + + it 'the ! function does not raise an error' do + expect do + Aws::Crt::Native.test_error!(3) + end.not_to raise_error + end + end +end diff --git a/gems/aws-crt/spec/spec_helper.rb b/gems/aws-crt/spec/spec_helper.rb index bdbe1c4..0a5c549 100644 --- a/gems/aws-crt/spec/spec_helper.rb +++ b/gems/aws-crt/spec/spec_helper.rb @@ -15,5 +15,5 @@ def garbage_collect_is_immediate? def check_for_clean_shutdown ObjectSpace.garbage_collect - Aws::Crt.call { Aws::Crt::Native.global_thread_creator_shutdown_wait_for(10) } + Aws::Crt::Native.global_thread_creator_shutdown_wait_for(10) end