diff --git a/CHANGES.md b/CHANGES.md index a4c08999..1d708345 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -## [4.2.0](https://github.com/Nexmo/ruby-nexmo/tree/v4.2.0) +# 4.2.0 * Added get_sms_pricing method @@ -10,7 +10,7 @@ * Added more clearly named methods for Verify API -## [4.1.0](https://github.com/Nexmo/ruby-nexmo/tree/v4.1.0) +# 4.1.0 * Added topup method @@ -18,7 +18,7 @@ * Added api_host option -## [4.0.0](https://github.com/Nexmo/ruby-nexmo/tree/v4.0.0) +# 4.0.0 * Removed exception behaviour from #send_message @@ -44,13 +44,13 @@ * Changed license from LGPL-3.0 to MIT -## [3.1.0](https://github.com/Nexmo/ruby-nexmo/tree/v3.1.0) +# 3.1.0 * Renamed #number_search method to #get_available_numbers * Added #control_verification_request method -## [3.0.0](https://github.com/Nexmo/ruby-nexmo/tree/v3.0.0) +# 3.0.0 * Removed :http accessor @@ -64,7 +64,7 @@ * Added license info -## [2.0.0](https://github.com/Nexmo/ruby-nexmo/tree/v2.0.0) +# 2.0.0 * Dropped support for Ruby 1.8.7 @@ -88,7 +88,7 @@ * Added Voice API methods -## [1.3.0](https://github.com/Nexmo/ruby-nexmo/tree/v1.3.0) +# 1.3.0 * Added Nexmo::Client#buy_number method @@ -98,19 +98,19 @@ * Added :host option for specifying a different hostname to connect to -## [1.2.0](https://github.com/Nexmo/ruby-nexmo/tree/v1.2.0) +# 1.2.0 * Added initializer block functionality for tweaking response behaviour * Deprecated the :json option (use an initializer block instead) -## [1.1.0](https://github.com/Nexmo/ruby-nexmo/tree/v1.1.0) +# 1.1.0 * Added default lookup of NEXMO_API_KEY and NEXMO_API_SECRET environment variables * Added preliminary/experimental support for [Nexmo OAuth](https://labs.nexmo.com/#oauth) (beta) -## [1.0.0](https://github.com/Nexmo/ruby-nexmo/tree/v1.0.0) +# 1.0.0 * Ruby 1.8.7 compatibility @@ -118,7 +118,7 @@ * Added Nexmo::Client#send_message! method -## [0.5.0](https://github.com/Nexmo/ruby-nexmo/tree/v0.5.0) +# 0.5.0 * Added Nexmo::Client#search_messages method @@ -126,7 +126,7 @@ * Added :json option for specifying an alternate JSON implementation -## [0.4.0](https://github.com/Nexmo/ruby-nexmo/tree/v0.4.0) +# 0.4.0 * Added Nexmo::Client#get_balance method @@ -142,30 +142,30 @@ * Added Nexmo::Client#get_message_rejections method -## [0.3.1](https://github.com/Nexmo/ruby-nexmo/tree/v0.3.1) +# 0.3.1 * Fixed content type checking (thanks @dbrock) -## [0.3.0](https://github.com/Nexmo/ruby-nexmo/tree/v0.3.0) +# 0.3.0 * Fixed Nexmo::Client#send_message for unexpected HTTP responses -## [0.2.2](https://github.com/Nexmo/ruby-nexmo/tree/v0.2.2) +# 0.2.2 * Added Nexmo status code to error messages -## [0.2.1](https://github.com/Nexmo/ruby-nexmo/tree/v0.2.1) +# 0.2.1 * No significant changes -## [0.2.0](https://github.com/Nexmo/ruby-nexmo/tree/v0.2.0) +# 0.2.0 * Added Nexmo::Client#headers method -## [0.1.1](https://github.com/Nexmo/ruby-nexmo/tree/v0.1.1) +# 0.1.1 * Ruby 1.8.7 compatibility -## [0.1.0](https://github.com/Nexmo/ruby-nexmo/tree/v0.1.0) +# 0.1.0 * First version! diff --git a/README.md b/README.md index dc81f21f..559b7558 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ need a Nexmo account. Sign up [for free at nexmo.com][signup]. * [Installation](#installation) * [Usage](#usage) -* [Examples](#examples) +* [SMS API](#sms-api) +* [Voice API](#voice-api) +* [Verify API](#verify-api) +* [Application API](#application-api) * [Coverage](#api-coverage) * [License](#license) @@ -28,33 +31,36 @@ Alternatively you can clone the repository: Usage ----- -Specify your credentials using the `NEXMO_API_KEY` and `NEXMO_API_SECRET` -environment variables; require the nexmo library; and construct a client object. -For example: +Begin by requiring the nexmo library: ```ruby require 'nexmo' - -client = Nexmo::Client.new ``` -Alternatively you can specify your credentials directly using the `key` -and `secret` options: +Then construct a client object with your key and secret: ```ruby -require 'nexmo' - client = Nexmo::Client.new(key: 'YOUR-API-KEY', secret: 'YOUR-API-SECRET') ``` +For production you can specify the `NEXMO_API_KEY` and `NEXMO_API_SECRET` +environment variables instead of specifying the key and secret explicitly. + +For newer endpoints that support JWT authentication such as the Voice API, +you can also specify the `application_id` and `private_key` arguments: + +```ruby +client = Nexmo::Client.new(application_id: application_id, private_key: private_key) +``` -Examples --------- +In order to check signatures for incoming webhook requests, you'll also need +to specify the `signature_secret` argument (or the `NEXMO_SIGNATURE_SECRET` +environment variable). -### Sending a message -To use [Nexmo's SMS API][doc_sms] to send an SMS message, call the Nexmo::Client#send_message -method with a hash containing the API parameters. For example: +## SMS API + +### Send a text message ```ruby response = client.send_message(from: 'Ruby', to: 'YOUR NUMBER', text: 'Hello world') @@ -66,72 +72,109 @@ else end ``` -Specify `type: 'unicode'` if sending unicode characters. For example: +Docs: [https://docs.nexmo.com/messaging/sms-api/api-reference#request](https://docs.nexmo.com/messaging/sms-api/api-reference#request?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + + +## Voice API + +### Make a call ```ruby -response = client.send_message(from: 'Ruby', to: 'YOUR NUMBER', text: "Unicode \u2713", type: 'unicode') +response = client.create_call({ + to: [{type: 'phone', number: '14843331234'}], + from: {type: 'phone', number: '14843335555'}, + answer_url: ['https://example.com/answer'] +}) ``` -Specify phone numbers in international format, prefixed with country code. +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#call_create](https://docs.nexmo.com/voice/voice-api/api-reference#call_create?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) -Check `response['messages'][0]['status']` to see if the message was sent -successfully or if there was an error with the request. +### Retrieve a list of calls -Country specific restrictions may apply. For example, US messages must -originate from either a pre-approved long number or short code. +```ruby +response = client.get_calls +``` -### Fetching a message +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#call_retrieve](https://docs.nexmo.com/voice/voice-api/api-reference#call_retrieve?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) -You can retrieve a message log from the API using the ID of the message: +### Retrieve a single call ```ruby -message = client.get_message('02000000DA7C52E7') +response = client.get_call(uuid) +``` + +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#call_retrieve_single](https://docs.nexmo.com/voice/voice-api/api-reference#call_retrieve_single?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Update a call -puts "The body of the message was: #{message['body']}" +```ruby +response = client.update_call(uuid, action: 'hangup') ``` -### Starting a verification +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#call_modify_single](https://docs.nexmo.com/voice/voice-api/api-reference#call_modify_single?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Stream audio to a call + +```ruby +stream_url = 'https://nexmo-community.github.io/ncco-examples/assets/voice_api_audio_streaming.mp3' + +response = client.send_audio(uuid, stream_url: stream_url) +``` -Nexmo's [Verify API][doc_verify] makes it easy to prove that a user has provided their -own phone number during signup, or implement second factor authentication during signin. +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#stream_put](https://docs.nexmo.com/voice/voice-api/api-reference#stream_put?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) -You can start the verification process by calling the start_verification method: +### Stop streaming audio to a call ```ruby -response = client.start_verification(number: '441632960960', brand: 'MyApp') +response = client.stop_audio(uuid) +``` -if response['status'] == '0' - puts "Started verification request_id=#{response['request_id']}" -else - puts "Error: #{response['error_text']}" -end +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#stream_delete](https://docs.nexmo.com/voice/voice-api/api-reference#stream_delete?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Send a synthesized speech message to a call + +```ruby +response = client.send_speech(uuid, text: 'Hello') ``` -The response contains a verification request id which you will need to -store temporarily (in the session, database, url etc). +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#talk_put](https://docs.nexmo.com/voice/voice-api/api-reference#talk_put?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Stop sending a synthesized speech message to a call + +```ruby +response = client.stop_speech(uuid) +``` -### Controlling a verification +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#talk_delete](https://docs.nexmo.com/voice/voice-api/api-reference#talk_delete?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) -Call the cancel_verification method with the verification request id -to cancel an in-progress verification: +### Send DTMF tones to a call ```ruby -client.cancel_verification('00e6c3377e5348cdaf567e1417c707a5') +response = client.send_dtmf(uuid, digits: '1234') ``` -Call the trigger_next_verification_event method with the verification -request id to trigger the next attempt to send the confirmation code: +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#dtmf_put](https://docs.nexmo.com/voice/voice-api/api-reference#dtmf_put?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + + +## Verify API + +### Start a verification ```ruby -client.trigger_next_verification_event('00e6c3377e5348cdaf567e1417c707a5') +response = client.start_verification(number: '441632960960', brand: 'MyApp') + +if response['status'] == '0' + puts "Started verification request_id=#{response['request_id']}" +else + puts "Error: #{response['error_text']}" +end ``` -The verification request id comes from the call to the start_verification method. +Docs: [https://docs.nexmo.com/verify/api-reference/api-reference#vrequest](https://docs.nexmo.com/verify/api-reference/api-reference#vrequest?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) -### Checking a verification +The response contains a verification request id which you will need to store temporarily. -Call the check_verification method with the verification request id and the -PIN code to complete the verification process: +### Check a verification ```ruby response = client.check_verification('00e6c3377e5348cdaf567e1417c707a5', code: '1234') @@ -143,9 +186,88 @@ else end ``` +Docs: [https://docs.nexmo.com/verify/api-reference/api-reference#check](https://docs.nexmo.com/verify/api-reference/api-reference#check?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + The verification request id comes from the call to the start_verification method. + The PIN code is entered into your application by the user. +### Cancel a verification + +```ruby +client.cancel_verification('00e6c3377e5348cdaf567e1417c707a5') +``` + +Docs: [https://docs.nexmo.com/verify/api-reference/api-reference#control](https://docs.nexmo.com/verify/api-reference/api-reference#control?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Trigger next verification step + +```ruby +client.trigger_next_verification_event('00e6c3377e5348cdaf567e1417c707a5') +``` + +Docs: [https://docs.nexmo.com/verify/api-reference/api-reference#control](https://docs.nexmo.com/verify/api-reference/api-reference#control?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + + +## Application API + +### Create an application + +```ruby +response = client.create_application(name: 'Example App', type: 'voice', answer_url: answer_url) +``` + +Docs: [https://docs.nexmo.com/tools/application-api/api-reference#create](https://docs.nexmo.com/tools/application-api/api-reference#create?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Retrieve a list of applications + +```ruby +response = client.get_applications +``` + +Docs: [https://docs.nexmo.com/tools/application-api/api-reference#list](https://docs.nexmo.com/tools/application-api/api-reference#list?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Retrieve a single application + +```ruby +response = client.get_application(uuid) +``` + +Docs: [https://docs.nexmo.com/tools/application-api/api-reference#retrieve](https://docs.nexmo.com/tools/application-api/api-reference#retrieve?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Update an application + +```ruby +response = client.update_application(uuid, answer_method: 'POST') +``` + +Docs: [https://docs.nexmo.com/tools/application-api/api-reference#update](https://docs.nexmo.com/tools/application-api/api-reference#update?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +### Delete an application + +```ruby +response = client.delete_application(uuid) +``` + +Docs: [https://docs.nexmo.com/tools/application-api/api-reference#delete](https://docs.nexmo.com/tools/application-api/api-reference#delete?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + + +## Validate webhook signatures + +```ruby +client = Nexmo::Client.new(signature_secret: 'secret') + +if client.check_signature(request.params): + # valid signature +else: + # invalid signature +``` + +Docs: [https://docs.nexmo.com/messaging/signing-messages](https://docs.nexmo.com/messaging/signing-messages?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library) + +Note: you'll need to contact support@nexmo.com to enable message signing on +your account before you can validate webhook signatures. + API Coverage ------------ @@ -196,6 +318,4 @@ License This library is released under the [MIT License][license] [signup]: https://dashboard.nexmo.com/sign-up?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library -[doc_sms]: https://docs.nexmo.com/messaging/sms-api?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library -[doc_verify]: https://docs.nexmo.com/verify/api-reference?utm_source=DEV_REL&utm_medium=github&utm_campaign=ruby-client-library [license]: LICENSE.txt diff --git a/lib/nexmo.rb b/lib/nexmo.rb index b4e914f1..e4545c2d 100644 --- a/lib/nexmo.rb +++ b/lib/nexmo.rb @@ -1,17 +1,15 @@ require 'nexmo/version' +require 'nexmo/params' +require 'nexmo/jwt' +require 'nexmo/signature' +require 'nexmo/errors/error' +require 'nexmo/errors/client_error' +require 'nexmo/errors/server_error' +require 'nexmo/errors/authentication_error' require 'net/http' require 'json' -require 'cgi' module Nexmo - class Error < StandardError; end - - class ClientError < Error; end - - class ServerError < Error; end - - class AuthenticationError < ClientError; end - class Client attr_accessor :key, :secret @@ -20,6 +18,12 @@ def initialize(options = {}) @secret = options.fetch(:secret) { ENV.fetch('NEXMO_API_SECRET') } + @signature_secret = options.fetch(:signature_secret) { ENV['NEXMO_SIGNATURE_SECRET'] } + + @application_id = options[:application_id] + + @private_key = options[:private_key] + @host = options.fetch(:host) { 'rest.nexmo.com' } @api_host = options.fetch(:api_host) { 'api.nexmo.com' } @@ -134,14 +138,20 @@ def sns_subscribe(recipient, params) end def initiate_call(params) + Kernel.warn "#{self.class}##{__method__} is deprecated (use the Voice API instead)." + post(@host, '/call/json', params) end def initiate_tts_call(params) + Kernel.warn "#{self.class}##{__method__} is deprecated (use the Voice API instead)." + post(@api_host, '/tts/json', params) end def initiate_tts_prompt_call(params) + Kernel.warn "#{self.class}##{__method__} is deprecated (use the Voice API instead)." + post(@api_host, '/tts-prompt/json', params) end @@ -201,16 +211,75 @@ def request_number_insight(params) post(@host, '/ni/json', params) end + def get_applications(params = {}) + get(@api_host, '/v1/applications', params) + end + + def get_application(id) + get(@api_host, "/v1/applications/#{id}") + end + + def create_application(params) + post(@api_host, '/v1/applications', params) + end + + def update_application(id, params) + put(@api_host, "/v1/applications/#{id}", params) + end + + def delete_application(id) + delete(@api_host, "/v1/applications/#{id}") + end + + def create_call(params) + api_request(Net::HTTP::Post, '/v1/calls', params) + end + + def get_calls(params = nil) + api_request(Net::HTTP::Get, '/v1/calls', params) + end + + def get_call(uuid) + api_request(Net::HTTP::Get, "/v1/calls/#{uuid}") + end + + def update_call(uuid, params) + api_request(Net::HTTP::Put, "/v1/calls/#{uuid}", params) + end + + def send_audio(uuid, params) + api_request(Net::HTTP::Put, "/v1/calls/#{uuid}/stream", params) + end + + def stop_audio(uuid) + api_request(Net::HTTP::Delete, "/v1/calls/#{uuid}/stream") + end + + def send_speech(uuid, params) + api_request(Net::HTTP::Put, "/v1/calls/#{uuid}/talk", params) + end + + def stop_speech(uuid) + api_request(Net::HTTP::Delete, "/v1/calls/#{uuid}/talk") + end + + def send_dtmf(uuid, params) + api_request(Net::HTTP::Put, "/v1/calls/#{uuid}/dtmf", params) + end + + def check_signature(params) + Signature.check(params, @signature_secret) + end + private def get(host, request_uri, params = {}) uri = URI('https://' + host + request_uri) - uri.query = query_string(params.merge(api_key: @key, api_secret: @secret)) + uri.query = Params.encode(params.merge(api_key: @key, api_secret: @secret)) message = Net::HTTP::Get.new(uri.request_uri) - message['User-Agent'] = @user_agent - parse(request(uri, message), host) + request(uri, message) end def post(host, request_uri, params) @@ -218,19 +287,60 @@ def post(host, request_uri, params) message = Net::HTTP::Post.new(uri.request_uri) message.form_data = params.merge(api_key: @key, api_secret: @secret) - message['User-Agent'] = @user_agent - parse(request(uri, message), host) + request(uri, message) + end + + def put(host, request_uri, params) + uri = URI('https://' + host + request_uri) + + message = Net::HTTP::Put.new(uri.request_uri) + message.form_data = params.merge(api_key: @key, api_secret: @secret) + + request(uri, message) + end + + def delete(host, request_uri) + uri = URI('https://' + host + request_uri) + uri.query = Params.encode({api_key: @key, api_secret: @secret}) + + message = Net::HTTP::Delete.new(uri.request_uri) + + request(uri, message) + end + + def api_request(message_class, path, params = nil) + uri = URI('https://' + @api_host + path) + + unless message_class::REQUEST_HAS_BODY || params.nil? || params.empty? + uri.query = Params.encode(params) + end + + message = message_class.new(uri.request_uri) + + if message_class::REQUEST_HAS_BODY + message['Content-Type'] = 'application/json' + message.body = JSON.generate(params) + end + + auth_payload = {application_id: @application_id} + + message['Authorization'] = JWT.auth_header(auth_payload, @private_key) + + request(uri, message) end def request(uri, message) http = Net::HTTP.new(uri.host, Net::HTTP.https_default_port) http.use_ssl = true - http.request(message) - end - def parse(http_response, host) + message['User-Agent'] = @user_agent + + http_response = http.request(message) + case http_response + when Net::HTTPNoContent + :no_content when Net::HTTPSuccess if http_response['Content-Type'].split(';').first == 'application/json' JSON.parse(http_response.body) @@ -238,22 +348,14 @@ def parse(http_response, host) http_response.body end when Net::HTTPUnauthorized - raise AuthenticationError, "#{http_response.code} response from #{host}" + raise AuthenticationError, "#{http_response.code} response from #{uri.host}" when Net::HTTPClientError - raise ClientError, "#{http_response.code} response from #{host}" + raise ClientError, "#{http_response.code} response from #{uri.host}" when Net::HTTPServerError - raise ServerError, "#{http_response.code} response from #{host}" + raise ServerError, "#{http_response.code} response from #{uri.host}" else - raise Error, "#{http_response.code} response from #{host}" + raise Error, "#{http_response.code} response from #{uri.host}" end end - - def query_string(params) - params.flat_map { |k, vs| Array(vs).map { |v| "#{escape(k)}=#{escape(v)}" } }.join('&') - end - - def escape(component) - CGI.escape(component.to_s) - end end end diff --git a/lib/nexmo/errors/authentication_error.rb b/lib/nexmo/errors/authentication_error.rb new file mode 100644 index 00000000..433a7d24 --- /dev/null +++ b/lib/nexmo/errors/authentication_error.rb @@ -0,0 +1,4 @@ +module Nexmo + class AuthenticationError < ClientError + end +end diff --git a/lib/nexmo/errors/client_error.rb b/lib/nexmo/errors/client_error.rb new file mode 100644 index 00000000..c7092858 --- /dev/null +++ b/lib/nexmo/errors/client_error.rb @@ -0,0 +1,4 @@ +module Nexmo + class ClientError < Error + end +end diff --git a/lib/nexmo/errors/error.rb b/lib/nexmo/errors/error.rb new file mode 100644 index 00000000..64972800 --- /dev/null +++ b/lib/nexmo/errors/error.rb @@ -0,0 +1,4 @@ +module Nexmo + class Error < StandardError + end +end diff --git a/lib/nexmo/errors/server_error.rb b/lib/nexmo/errors/server_error.rb new file mode 100644 index 00000000..929f93f7 --- /dev/null +++ b/lib/nexmo/errors/server_error.rb @@ -0,0 +1,4 @@ +module Nexmo + class ServerError < Error + end +end diff --git a/lib/nexmo/jwt.rb b/lib/nexmo/jwt.rb new file mode 100644 index 00000000..37c3085c --- /dev/null +++ b/lib/nexmo/jwt.rb @@ -0,0 +1,19 @@ +require 'securerandom' +require 'openssl' +require 'jwt' + +module Nexmo + module JWT + def self.auth_header(payload, private_key) + payload[:iat] = iat = Time.now.to_i + payload[:exp] = iat + 60 + payload[:jti] = SecureRandom.uuid + + private_key = OpenSSL::PKey::RSA.new(private_key) unless private_key.respond_to?(:sign) + + token = ::JWT.encode(payload, private_key, 'RS256') + + "Bearer #{token}" + end + end +end diff --git a/lib/nexmo/params.rb b/lib/nexmo/params.rb new file mode 100644 index 00000000..63d75dd9 --- /dev/null +++ b/lib/nexmo/params.rb @@ -0,0 +1,13 @@ +require 'cgi' + +module Nexmo + module Params + def self.encode(params) + params.flat_map { |k, vs| Array(vs).map { |v| "#{escape(k)}=#{escape(v)}" } }.join('&') + end + + def self.escape(component) + CGI.escape(component.to_s) + end + end +end diff --git a/lib/nexmo/signature.rb b/lib/nexmo/signature.rb new file mode 100644 index 00000000..31c5a5d0 --- /dev/null +++ b/lib/nexmo/signature.rb @@ -0,0 +1,26 @@ +require 'digest/md5' +require 'jwt' + +module Nexmo + module Signature + def self.check(params, secret) + params = params.dup + + signature = params.delete(:sig) + + ::JWT.secure_compare(signature, digest(params, secret)) + end + + def self.digest(params, secret) + md5 = Digest::MD5.new + + params.sort.each do |k, v| + md5.update("&#{k}=#{v}") + end + + md5.update(secret) + + md5.hexdigest + end + end +end diff --git a/nexmo.gemspec b/nexmo.gemspec index 0013c0d8..f428097c 100644 --- a/nexmo.gemspec +++ b/nexmo.gemspec @@ -12,6 +12,7 @@ Gem::Specification.new do |s| s.summary = 'This is the Ruby client library for Nexmo\'s API. To use it you\'ll need a Nexmo account. Sign up for free at https://www.nexmo.com' s.files = Dir.glob('{lib,spec}/**/*') + %w(LICENSE.txt README.md nexmo.gemspec) s.required_ruby_version = '>= 1.9.3' + s.add_dependency('jwt') s.add_development_dependency('rake', '~> 10.1') s.add_development_dependency('webmock', '~> 1.18') s.add_development_dependency('minitest', '~> 5.0') diff --git a/spec/nexmo_spec.rb b/spec/nexmo_spec.rb index 5f8f5ee7..81aaf5fb 100644 --- a/spec/nexmo_spec.rb +++ b/spec/nexmo_spec.rb @@ -8,6 +8,10 @@ @api_secret = 'api_secret_xxx' + @application_id = 'nexmo-application-id' + + @private_key = File.read('test/private_key.txt') + @base_url = 'https://rest.nexmo.com' @api_base_url = 'https://api.nexmo.com' @@ -18,16 +22,14 @@ @response_object = {'key' => 'value'} - @example_message_hash = {from: 'ruby', to: 'number', text: 'Hey!'} - - @client = Nexmo::Client.new(key: @api_key, secret: @api_secret) + @client = Nexmo::Client.new(key: @api_key, secret: @api_secret, application_id: @application_id, private_key: @private_key) end describe 'send_message method' do it 'posts to the sms resource and returns the response object' do expect_post "#@base_url/sms/json", "from=ruby&to=number&text=Hey!&api_key=#@api_key&api_secret=#@api_secret" - @client.send_message(@example_message_hash).must_equal(@response_object) + @client.send_message(from: 'ruby', to: 'number', text: 'Hey!').must_equal(@response_object) end end @@ -161,7 +163,7 @@ it 'posts to the ussd resource and returns the response object' do expect_post "#@base_url/ussd/json", "api_key=#@api_key&api_secret=#@api_secret&from=MyCompany20&to=447525856424&text=Hello" - @client.send_ussd_push_message(from: 'MyCompany20', to: '447525856424', text: 'Hello') + @client.send_ussd_push_message(from: 'MyCompany20', to: '447525856424', text: 'Hello').must_equal(@response_object) end end @@ -169,7 +171,7 @@ it 'posts to the ussd prompt resource and returns the response object' do expect_post "#@base_url/ussd-prompt/json", "api_key=#@api_key&api_secret=#@api_secret&from=virtual-number&to=447525856424&text=Hello" - @client.send_ussd_prompt_message(from: 'virtual-number', to: '447525856424', text: 'Hello') + @client.send_ussd_prompt_message(from: 'virtual-number', to: '447525856424', text: 'Hello').must_equal(@response_object) end end @@ -177,7 +179,7 @@ it 'posts to the short code two factor authentication resource and returns the response object' do expect_post "#@base_url/sc/us/2fa/json", "api_key=#@api_key&api_secret=#@api_secret&to=16365553226&pin=1234" - @client.send_2fa_message(to: '16365553226', pin: 1234) + @client.send_2fa_message(to: '16365553226', pin: 1234).must_equal(@response_object) end end @@ -185,7 +187,7 @@ it 'posts to the short code alert resource and returns the response object' do expect_post "#@base_url/sc/us/alert/json", "api_key=#@api_key&api_secret=#@api_secret&to=16365553226&server=host&link=http://example.com/" - @client.send_event_alert_message(to: '16365553226', server: 'host', link: 'http://example.com/') + @client.send_event_alert_message(to: '16365553226', server: 'host', link: 'http://example.com/').must_equal(@response_object) end end @@ -193,7 +195,7 @@ it 'posts to the short code marketing resource and returns the response object' do expect_post "#@base_url/sc/us/marketing/json", "api_key=#@api_key&api_secret=#@api_secret&from=666&to=16365553226&keyword=NEXMO&text=Hello" - @client.send_marketing_message(from: '666', to: '16365553226', keyword: 'NEXMO', text: 'Hello') + @client.send_marketing_message(from: '666', to: '16365553226', keyword: 'NEXMO', text: 'Hello').must_equal(@response_object) end end @@ -201,7 +203,7 @@ it 'fetches the short code alert opt-in query resource and returns the response object' do expect_get "#@base_url/sc/us/alert/opt-in/query/json?api_key=#@api_key&api_secret=#@api_secret" - @client.get_event_alert_numbers + @client.get_event_alert_numbers.must_equal(@response_object) end end @@ -209,7 +211,7 @@ it 'posts to the short code alert opt-in manage resource and returns the response object' do expect_post "#@base_url/sc/us/alert/opt-in/manage/json", "api_key=#@api_key&api_secret=#@api_secret&msisdn=441632960960" - @client.resubscribe_event_alert_number(msisdn: '441632960960') + @client.resubscribe_event_alert_number(msisdn: '441632960960').must_equal(@response_object) end end @@ -217,7 +219,7 @@ it 'posts to the sns resource and returns the response object' do expect_post "#@sns_base_url/sns/json", "api_key=#@api_key&api_secret=#@api_secret&cmd=publish&message=Hello&topic=arn&from=MyCompany20" - @client.sns_publish('Hello', topic: 'arn', from: 'MyCompany20') + @client.sns_publish('Hello', topic: 'arn', from: 'MyCompany20').must_equal(@response_object) end end @@ -225,7 +227,7 @@ it 'posts to the sns resource and returns the response object' do expect_post "#@sns_base_url/sns/json", "api_key=#@api_key&api_secret=#@api_secret&cmd=subscribe&to=447525856424&topic=arn" - @client.sns_subscribe('447525856424', topic: 'arn') + @client.sns_subscribe('447525856424', topic: 'arn').must_equal(@response_object) end end @@ -233,7 +235,9 @@ it 'posts to the call resource and returns the response object' do expect_post "#@base_url/call/json", "api_key=#@api_key&api_secret=#@api_secret&to=16365553226&answer_url=http://example.com/answer" - @client.initiate_call(to: '16365553226', answer_url: 'http://example.com/answer') + Kernel.stub :warn, proc { |message| message.must_match(/initiate_call is deprecated/) } do + @client.initiate_call(to: '16365553226', answer_url: 'http://example.com/answer').must_equal(@response_object) + end end end @@ -241,7 +245,9 @@ it 'posts to the tts resource and returns the response object' do expect_post "#@api_base_url/tts/json", "api_key=#@api_key&api_secret=#@api_secret&to=16365553226&text=Hello" - @client.initiate_tts_call(to: '16365553226', text: 'Hello') + Kernel.stub :warn, proc { |message| message.must_match(/initiate_tts_call is deprecated/) } do + @client.initiate_tts_call(to: '16365553226', text: 'Hello').must_equal(@response_object) + end end end @@ -249,7 +255,9 @@ it 'posts to the tts prompt resource and returns the response object' do expect_post "#@api_base_url/tts-prompt/json", "api_key=#@api_key&api_secret=#@api_secret&to=16365553226&text=Hello&max_digits=4&bye_text=Goodbye" - @client.initiate_tts_prompt_call(to: '16365553226', text: 'Hello', max_digits: 4, bye_text: 'Goodbye') + Kernel.stub :warn, proc { |message| message.must_match(/initiate_tts_prompt_call is deprecated/) } do + @client.initiate_tts_prompt_call(to: '16365553226', text: 'Hello', max_digits: 4, bye_text: 'Goodbye').must_equal(@response_object) + end end end @@ -257,7 +265,7 @@ it 'posts to the verify json resource and returns the response object' do expect_post "#@api_base_url/verify/json", "api_key=#@api_key&api_secret=#@api_secret&number=447525856424&brand=MyApp" - @client.start_verification(number: '447525856424', brand: 'MyApp') + @client.start_verification(number: '447525856424', brand: 'MyApp').must_equal(@response_object) end end @@ -266,7 +274,7 @@ expect_post "#@api_base_url/verify/json", "api_key=#@api_key&api_secret=#@api_secret&number=447525856424&brand=MyApp" Kernel.stub :warn, proc { |message| message.must_match(/send_verification_request is deprecated/) } do - @client.send_verification_request(number: '447525856424', brand: 'MyApp') + @client.send_verification_request(number: '447525856424', brand: 'MyApp').must_equal(@response_object) end end end @@ -275,7 +283,7 @@ it 'posts to the verify check resource and returns the response object' do expect_post "#@api_base_url/verify/check/json", "api_key=#@api_key&api_secret=#@api_secret&request_id=8g88g88eg8g8gg9g90&code=123445" - @client.check_verification('8g88g88eg8g8gg9g90', code: '123445') + @client.check_verification('8g88g88eg8g8gg9g90', code: '123445').must_equal(@response_object) end end @@ -284,7 +292,7 @@ expect_post "#@api_base_url/verify/check/json", "api_key=#@api_key&api_secret=#@api_secret&request_id=8g88g88eg8g8gg9g90&code=123445" Kernel.stub :warn, proc { |message| message.must_match(/check_verification_request is deprecated/) } do - @client.check_verification_request(request_id: '8g88g88eg8g8gg9g90', code: '123445') + @client.check_verification_request(request_id: '8g88g88eg8g8gg9g90', code: '123445').must_equal(@response_object) end end end @@ -293,7 +301,7 @@ it 'fetches the verify search resource with the given request id and returns the response object' do expect_get "#@api_base_url/verify/search/json?api_key=#@api_key&api_secret=#@api_secret&request_id=8g88g88eg8g8gg9g90" - @client.get_verification('8g88g88eg8g8gg9g90') + @client.get_verification('8g88g88eg8g8gg9g90').must_equal(@response_object) end end @@ -302,7 +310,7 @@ expect_get "#@api_base_url/verify/search/json?api_key=#@api_key&api_secret=#@api_secret&request_id=8g88g88eg8g8gg9g90" Kernel.stub :warn, proc { |message| message.must_match(/get_verification_request is deprecated/) } do - @client.get_verification_request('8g88g88eg8g8gg9g90') + @client.get_verification_request('8g88g88eg8g8gg9g90').must_equal(@response_object) end end end @@ -311,7 +319,7 @@ it 'posts to the verify control resource and returns the response object' do expect_post "#@api_base_url/verify/control/json", "api_key=#@api_key&api_secret=#@api_secret&request_id=8g88g88eg8g8gg9g90&cmd=cancel" - @client.cancel_verification('8g88g88eg8g8gg9g90') + @client.cancel_verification('8g88g88eg8g8gg9g90').must_equal(@response_object) end end @@ -319,7 +327,7 @@ it 'posts to the verify control resource and returns the response object' do expect_post "#@api_base_url/verify/control/json", "api_key=#@api_key&api_secret=#@api_secret&request_id=8g88g88eg8g8gg9g90&cmd=trigger_next_event" - @client.trigger_next_verification_event('8g88g88eg8g8gg9g90') + @client.trigger_next_verification_event('8g88g88eg8g8gg9g90').must_equal(@response_object) end end @@ -328,7 +336,7 @@ expect_post "#@api_base_url/verify/control/json", "api_key=#@api_key&api_secret=#@api_secret&request_id=8g88g88eg8g8gg9g90&cmd=cancel" Kernel.stub :warn, proc { |message| message.must_match(/control_verification_request is deprecated/) } do - @client.control_verification_request(request_id: '8g88g88eg8g8gg9g90', cmd: 'cancel') + @client.control_verification_request(request_id: '8g88g88eg8g8gg9g90', cmd: 'cancel').must_equal(@response_object) end end end @@ -337,7 +345,7 @@ it 'fetches the number format resource and returns the response object' do expect_get "#@api_base_url/number/format/json?api_key=#@api_key&api_secret=#@api_secret&number=447525856424" - @client.get_basic_number_insight(number: '447525856424') + @client.get_basic_number_insight(number: '447525856424').must_equal(@response_object) end end @@ -345,7 +353,7 @@ it 'fetches the number lookup resource and returns the response object' do expect_get "#@api_base_url/number/lookup/json?api_key=#@api_key&api_secret=#@api_secret&number=447525856424" - @client.get_number_insight(number: '447525856424') + @client.get_number_insight(number: '447525856424').must_equal(@response_object) end end @@ -353,34 +361,162 @@ it 'posts to the number insight resource and returns the response object' do expect_post "#@base_url/ni/json", "api_key=#@api_key&api_secret=#@api_secret&number=447525856424&callback=https://example.com" - @client.request_number_insight(number: '447525856424', callback: 'https://example.com') + @client.request_number_insight(number: '447525856424', callback: 'https://example.com').must_equal(@response_object) + end + end + + describe 'get_applications method' do + it 'fetches the applications resource and returns the response object' do + expect_get "#@api_base_url/v1/applications?api_key=#@api_key&api_secret=#@api_secret" + + @client.get_applications.must_equal(@response_object) + end + end + + describe 'get_application method' do + it 'fetches the application resource with the given id and returns the response object' do + expect_get "#@api_base_url/v1/applications/xx-xx-xx-xx?api_key=#@api_key&api_secret=#@api_secret" + + @client.get_application('xx-xx-xx-xx').must_equal(@response_object) + end + end + + describe 'create_application method' do + it 'posts to the applications resource and returns the response object' do + expect_post "#@api_base_url/v1/applications", "api_key=#@api_key&api_secret=#@api_secret&name=Example+App&type=voice" + + @client.create_application(name: 'Example App', type: 'voice') + end + end + + describe 'update_application method' do + it 'puts to the application resource with the given id and returns the response object' do + expect_put "#@api_base_url/v1/applications/xx-xx-xx-xx", "api_key=#@api_key&api_secret=#@api_secret&answer_url=https%3A%2F%2Fexample.com%2Fncco" + + @client.update_application('xx-xx-xx-xx', answer_url: 'https://example.com/ncco') + end + end + + describe 'delete_application method' do + it 'deletes the application resource with the given id' do + expect_delete "#@api_base_url/v1/applications/xx-xx-xx-xx?api_key=#@api_key&api_secret=#@api_secret" + + @client.delete_application('xx-xx-xx-xx') + end + end + + describe 'create_call method' do + it 'posts to the calls resource and returns the response object' do + params = { + to: [{type: 'phone', number: '14843331234'}], + from: {type: 'phone', number: '14843335555'}, + answer_url: ['https://example.com/answer'] + } + + expect :post, "#@api_base_url/v1/calls", params + + @client.create_call(params).must_equal(@response_object) + end + end + + describe 'get_calls method' do + it 'fetches the calls resource and returns the response object' do + expect :get, "#@api_base_url/v1/calls?status=completed" + + @client.get_calls(status: 'completed').must_equal(@response_object) + end + end + + describe 'get_call method' do + it 'fetches the call resource with the given uuid and returns the response object' do + expect :get, "#@api_base_url/v1/calls/xx-xx-xx-xx" + + @client.get_call('xx-xx-xx-xx').must_equal(@response_object) + end + end + + describe 'update_call method' do + it 'puts to the call resource with the given uuid and returns the response object' do + expect :put, "#@api_base_url/v1/calls/xx-xx-xx-xx", {action: 'hangup'} + + @client.update_call('xx-xx-xx-xx', action: 'hangup').must_equal(@response_object) + end + end + + describe 'send_audio method' do + it 'puts to the call stream resource with the given uuid and returns the response object' do + expect :put, "#@api_base_url/v1/calls/xx-xx-xx-xx/stream", {stream_url: 'http://example.com/audio.mp3'} + + @client.send_audio('xx-xx-xx-xx', stream_url: 'http://example.com/audio.mp3').must_equal(@response_object) + end + end + + describe 'stop_audio method' do + it 'deletes the call stream resource with the given uuid' do + expect :delete, "#@api_base_url/v1/calls/xx-xx-xx-xx/stream" + + @client.stop_audio('xx-xx-xx-xx') + end + end + + describe 'send_speech method' do + it 'puts to the call talk resource with the given uuid and returns the response object' do + expect :put, "#@api_base_url/v1/calls/xx-xx-xx-xx/talk", {text: 'Hello'} + + @client.send_speech('xx-xx-xx-xx', text: 'Hello').must_equal(@response_object) + end + end + + describe 'stop_speech method' do + it 'deletes the call talk resource with the given uuid' do + expect :delete, "#@api_base_url/v1/calls/xx-xx-xx-xx/talk" + + @client.stop_speech('xx-xx-xx-xx') + end + end + + describe 'send_dtmf method' do + it 'puts to the call dtmf resource with the given uuid and returns the response object' do + expect :put, "#@api_base_url/v1/calls/xx-xx-xx-xx/dtmf", {digits: '1234'} + + @client.send_dtmf('xx-xx-xx-xx', digits: '1234').must_equal(@response_object) + end + end + + describe 'check_signature method' do + it 'returns true if the signature in the given params is correct' do + params = {'a': '1', 'b': '2', 'timestamp': '1461605396', 'sig': '6af838ef94998832dbfc29020b564830'} + + client = Nexmo::Client.new(key: @api_key, secret: @api_secret, signature_secret: 'secret') + + client.check_signature(params).must_equal(true) end end it 'raises an authentication error exception if the response code is 401' do - stub_request(:post, "#@base_url/sms/json").to_return(status: 401) + stub_request(:get, /#{@base_url}/).to_return(status: 401) - proc { @client.send_message(@example_message_hash) }.must_raise(Nexmo::AuthenticationError) + proc { @client.get_balance }.must_raise(Nexmo::AuthenticationError) end it 'raises a client error exception if the response code is 4xx' do - stub_request(:post, "#@base_url/sms/json").to_return(status: 400) + stub_request(:get, /#{@base_url}/).to_return(status: 400) - proc { @client.send_message(@example_message_hash) }.must_raise(Nexmo::ClientError) + proc { @client.get_balance }.must_raise(Nexmo::ClientError) end it 'raises a server error exception if the response code is 5xx' do - stub_request(:post, "#@base_url/sms/json").to_return(status: 500) + stub_request(:get, /#{@base_url}/).to_return(status: 500) - proc { @client.send_message(@example_message_hash) }.must_raise(Nexmo::ServerError) + proc { @client.get_balance }.must_raise(Nexmo::ServerError) end it 'includes a user-agent header with the library version number and ruby version number' do headers = {'User-Agent' => "ruby-nexmo/#{Nexmo::VERSION}/#{RUBY_VERSION}"} - stub_request(:post, "#@base_url/sms/json").with(headers: headers).to_return(@response_body) + stub_request(:get, /#{@base_url}/).with(headers: headers).to_return(@response_body) - @client.send_message(@example_message_hash) + @client.get_balance end it 'provides options for application name and version to be included in the user-agent header' do @@ -388,11 +524,11 @@ headers = {'User-Agent' => "ruby-nexmo/#{Nexmo::VERSION}/#{RUBY_VERSION}/#{app_name}/#{app_version}"} - stub_request(:post, "#@base_url/sms/json").with(headers: headers).to_return(@response_body) + stub_request(:get, /#{@base_url}/).with(headers: headers).to_return(@response_body) @client = Nexmo::Client.new(key: @api_key, secret: @api_secret, app_name: app_name, app_version: app_version) - @client.send_message(@example_message_hash) + @client.get_balance end it 'provides an option for specifying a different hostname to connect to' do @@ -413,6 +549,22 @@ private + def expect(method_symbol, url, body = nil) + headers = {'Authorization' => /\ABearer (.+)\.(.+)\.(.+)\z/} + + @request = stub_request(method_symbol, url) + + if method_symbol == :delete + @request.with(headers: headers).to_return(status: 204) + elsif body.nil? + @request.with(headers: headers).to_return(@response_body) + else + headers['Content-Type'] = 'application/json' + + @request.with(headers: headers, body: body).to_return(@response_body) + end + end + def expect_get(url) @request = stub_request(:get, url).to_return(@response_body) end @@ -425,6 +577,18 @@ def expect_post(url, data) @request = stub_request(:post, url).with(body: body, headers: headers).to_return(@response_body) end + def expect_put(url, data) + body = WebMock::Util::QueryMapper.query_to_values(data) + + headers = {'Content-Type' => 'application/x-www-form-urlencoded'} + + @request = stub_request(:put, url).with(body: body, headers: headers).to_return(@response_body) + end + + def expect_delete(url) + @request = stub_request(:delete, url).to_return(status: 204) + end + after do assert_requested(@request) if defined?(@request) end diff --git a/test/private_key.txt b/test/private_key.txt new file mode 100644 index 00000000..163ff367 --- /dev/null +++ b/test/private_key.txt @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQdAHqJHs/a+Ra +2ubvSd1vz/aWlJ9BqnMUtB7guTlyggdENAbleIkzep6mUHepDJdQh8Qv6zS3lpUe +K0UkDfr1/FvsvxurGw/YYPagUEhP/HxMbs2rnQTiAdWOT+Ux9vPABoyNYvZB90xN +IVhBDRWgkz1HPQBRNjFcm3NOol83h5Uwp5YroGTWx+rpmIiRhQj3mv6luk102d95 +4ulpPpzcYWKIpJNdclJrEkBZaghDZTOpbv79qd+ds9AVp1j8i9cG/owBJpsJWxfw +StMDpNeEZqopeQWmA121sSEsxpAbKJ5DA7F/lmckx74sulKHX1fDWT76cRhloaEQ +VmETdj0VAgMBAAECggEAZ+SBtchz8vKbsBqtAbM/XcR5Iqi1TR2eWMHDJ/65HpSm ++XuyujjerN0e6EZvtT4Uxmq8QaPJNP0kmhI31hXvsB0UVcUUDa4hshb1pIYO3Gq7 +Kr8I29EZB2mhndm9Ii9yYhEBiVA66zrNeR225kkWr97iqjhBibhoVr8Vc6oiqcIP +nFy5zSFtQSkhucaPge6rW00JSOD3wg2GM+rgS6r22t8YmqTzAwvwfil5pQfUngal +oywqLOf6CUYXPBleJc1KgaIIP/cSvqh6b/t25o2VXnI4rpRhtleORvYBbH6K6xLa +OWgg6B58T+0/QEqtZIAn4miYtVCkYLB78Ormc7Q9ewKBgQDuSytuYqxdZh/L/RDU +CErFcNO5I1e9fkLAs5dQEBvvdQC74+oA1MsDEVv0xehFa1JwPKSepmvB2UznZg9L +CtR7QKMDZWvS5xx4j0E/b+PiNQ/tlcFZB2UZ0JwviSxdd7omOTscq9c3RIhFHar1 +Y38Fixkfm44Ij/K3JqIi2v2QMwKBgQDf8TYOOmAr9UuipUDxMsRSqTGVIY8B+aEJ +W+2aLrqJVkLGTRfrbjzXWYo3+n7kNJjFgNkltDq6HYtufHMYRs/0PPtNR0w0cDPS +Xr7m2LNHTDcBalC/AS4yKZJLNLm+kXA84vkw4qiTjc0LSFxJkouTQzkea0l8EWHt +zRMv/qYVlwKBgBaJOWRJJK/4lo0+M7c5yYh+sSdTNlsPc9Sxp1/FBj9RO26JkXne +pgx2OdIeXWcjTTqcIZ13c71zhZhkyJF6RroZVNFfaCEcBk9IjQ0o0c504jq/7Pc0 +gdU9K2g7etykFBDFXNfLUKFDc/fFZIOskzi8/PVGStp4cqXrm23cdBqNAoGBAKtf +A2bP9ViuVjsZCyGJIAPBxlfBXpa8WSe4WZNrvwPqJx9pT6yyp4yE0OkVoJUyStaZ +S5M24NocUd8zDUC+r9TP9d+leAOI+Z87MgumOUuOX2mN2kzQsnFgrrsulhXnZmSx +rNBkI20HTqobrcP/iSAgiU1l/M4c3zwDe3N3A9HxAoGBAM2hYu0Ij6htSNgo/WWr +IEYYXuwf8hPkiuwzlaiWhD3eocgd4S8SsBu/bTCY19hQ2QbBPaYyFlNem+ynQyXx +IOacrgIHCrYnRCxjPfFF/MxgUHJb8ZoiexprP/FME5p0PoRQIEFYa+jVht3hT5wC +9aedWufq4JJb+akO6MVUjTvs +-----END PRIVATE KEY----- diff --git a/test/public_key.txt b/test/public_key.txt new file mode 100644 index 00000000..a1715089 --- /dev/null +++ b/test/public_key.txt @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0HQB6iR7P2vkWtrm70nd +b8/2lpSfQapzFLQe4Lk5coIHRDQG5XiJM3qeplB3qQyXUIfEL+s0t5aVHitFJA36 +9fxb7L8bqxsP2GD2oFBIT/x8TG7Nq50E4gHVjk/lMfbzwAaMjWL2QfdMTSFYQQ0V +oJM9Rz0AUTYxXJtzTqJfN4eVMKeWK6Bk1sfq6ZiIkYUI95r+pbpNdNnfeeLpaT6c +3GFiiKSTXXJSaxJAWWoIQ2UzqW7+/anfnbPQFadY/IvXBv6MASabCVsX8ErTA6TX +hGaqKXkFpgNdtbEhLMaQGyieQwOxf5ZnJMe+LLpSh19Xw1k++nEYZaGhEFZhE3Y9 +FQIDAQAB +-----END PUBLIC KEY-----