diff --git a/CHANGELOG.md b/CHANGELOG.md index abad609..2ae2a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog][Keep a Changelog] and this project adh ## [Unreleased] +### [0.4.2] - 2026-05-08 + +### Fixed + +-ssl verify option and cert file location for non valid private links in gov + ## [0.4.0] - 2023-05-10 ### Fixed diff --git a/README.md b/README.md index 90841fa..8733fb1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,16 @@ Two environment variables are used: - 'SUBSCRIPTION_KEY': the API key you can generate on your Azure account. +Optional environment variables: + +- 'GOVERNMENT': set to `true` to use `microsoft.us` endpoints. + +- 'PRIVATE_LINK': set to your private endpoint base URL (for example `https://a2103tsapp1`). + +- 'SSL_VERIFY_PEER': set to `false` to disable TLS certificate verification (unsafe, but can help with private endpoints that do not have matching certificates). + +- 'SSL_CA_FILE': path to a custom CA bundle used to validate private certificates. + You can look at the file `env.sample` and change the values. If you do not want to use environment variables, you can configure the values like so: @@ -53,13 +63,44 @@ If you do not want to use environment variables, you can configure the values li AzureSTT.configure do |config| config.region = 'your_region' config.subscription_key = 'your_key' + config.government = false + config.private_link = nil + config.ssl_verify_peer = true + config.ssl_ca_file = nil +end +``` + +If your private link certificate hostname does not match the endpoint hostname, requests can fail with `certificate verify failed (Hostname mismatch)`. +You can work around it by disabling peer verification: + +```ruby +AzureSTT.configure do |config| + config.private_link = 'https://a2103tsapp1' + config.ssl_verify_peer = false +end +``` + +Prefer using a private CA when possible: + +```ruby +AzureSTT.configure do |config| + config.private_link = 'https://a2103tsapp1' + config.ssl_verify_peer = true + config.ssl_ca_file = '/path/to/private-ca.pem' end ``` Finally, the class `AzureSTT::Session` uses by the default the values from the configuration, but you can initialize the session with custom values: ```ruby -session = AzureSTT::Session.new(region: 'your_region', subscription_key: 'your_key') +session = AzureSTT::Session.new( + region: 'your_region', + subscription_key: 'your_key', + government: false, + private_link: nil, + ssl_verify_peer: true, + ssl_ca_file: nil +) ``` ### Start a transcription diff --git a/env.sample b/env.sample index 5e31ef2..dd1b251 100644 --- a/env.sample +++ b/env.sample @@ -1,2 +1,6 @@ REGION=centralus -SUBSCRIPTION_KEY=k3ah8ztrc4ojeh98r05zh7v6x9w62lqp \ No newline at end of file +SUBSCRIPTION_KEY=k3ah8ztrc4ojeh98r05zh7v6x9w62lqp +GOVERNMENT=false +PRIVATE_LINK= +SSL_VERIFY_PEER=false +SSL_CA_FILE= diff --git a/lib/azure_stt.rb b/lib/azure_stt.rb index 8702f6c..a66117a 100644 --- a/lib/azure_stt.rb +++ b/lib/azure_stt.rb @@ -4,6 +4,9 @@ # Top level module for AzureSTT # module AzureSTT + def self.env_true?(value) + %w[1 true yes on].include?(value.to_s.strip.downcase) + end end require_relative 'azure_stt/version' @@ -17,4 +20,8 @@ module AzureSTT AzureSTT.configure do |config| config.subscription_key = ENV.fetch('SUBSCRIPTION_KEY', nil) config.region = ENV.fetch('REGION', 'uscentral') + config.government = AzureSTT.env_true?(ENV.fetch('GOVERNMENT', 'false')) + config.private_link = ENV.fetch('PRIVATE_LINK', nil) + config.ssl_verify_peer = AzureSTT.env_true?(ENV.fetch('SSL_VERIFY_PEER', 'false')) + config.ssl_ca_file = ENV.fetch('SSL_CA_FILE', nil) end diff --git a/lib/azure_stt/client.rb b/lib/azure_stt/client.rb index 0518dc3..3c1682e 100644 --- a/lib/azure_stt/client.rb +++ b/lib/azure_stt/client.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'httparty' +require 'openssl' module AzureSTT # # Client class that uses HTTParty to communicate with the API @@ -8,7 +9,8 @@ module AzureSTT class Client include HTTParty - attr_reader :region, :subscription_key + attr_reader :region, :subscription_key, :government, :private_link, + :ssl_verify_peer, :ssl_ca_file # # Initialize the client @@ -16,10 +18,18 @@ class Client # @param [String] subscription_key Cognitive Services API Key # @param [String] region The region of your resources # - def initialize(region:, subscription_key:) + def initialize(region:, subscription_key:, government: false, private_link: nil, + ssl_verify_peer: true, ssl_ca_file: nil) @subscription_key = subscription_key @region = region - self.class.base_uri "https://#{region}.api.cognitive.microsoft.com/speechtotext/v3.1" + @government = government + @private_link = private_link + @ssl_verify_peer = ssl_verify_peer + @ssl_ca_file = ssl_ca_file + + base_url = "https://#{region}.api.cognitive.microsoft.#{government ? 'us' : 'com'}" + base_url = @private_link if value_present?(@private_link) + self.class.base_uri "#{base_url.chomp('/')}/speechtotext/v3.1" end # @@ -82,7 +92,9 @@ def get_transcriptions(skip: nil, top: nil) # @return [Boolean] true if the transcription had been deleted, raises an error else # def delete_transcription(id) - response = self.class.delete("/transcriptions/#{id}", headers: headers) + response = with_network_error_handling do + self.class.delete("/transcriptions/#{id}", request_options(headers: headers)) + end handle_response(response) true @@ -111,7 +123,9 @@ def get_transcription_files(id) # @return [Hash] the file parsed # def get_file(file_url) - response = self.class.get(file_url) + response = with_network_error_handling do + HTTParty.get(file_url, request_options) + end results = handle_response(response) @@ -129,12 +143,9 @@ def get_file(file_url) # @return [HTTParty::Response] # def post(path, body) - options = { - headers: headers, - body: body - } - - response = self.class.post(path, options) + response = with_network_error_handling do + self.class.post(path, request_options(headers: headers, body: body)) + end handle_response(response) end @@ -147,13 +158,37 @@ def post(path, body) # @return [HTTParty::Response] # def get(path, parameters = nil) - options = { + response = with_network_error_handling do + self.class.get(path, request_options(headers: headers, query: parameters)) + end + handle_response(response) + end + + def request_options(headers: nil, query: nil, body: nil) + { headers: headers, - query: parameters + query: query, + body: body + }.merge(ssl_options).compact + end + + def ssl_options + return { verify: false } unless ssl_verify_peer + + { + verify: true, + ssl_ca_file: value_present?(ssl_ca_file) ? ssl_ca_file : nil }.compact + end - response = self.class.get(path, options) - handle_response(response) + def with_network_error_handling + yield + rescue OpenSSL::SSL::SSLError => e + raise NetError.new( + code: 0, + message: "SSL connection failed: #{e.message}. " \ + 'Set ssl_verify_peer: false for private endpoints with mismatched certificates, or set ssl_ca_file to trust your private CA.' + ) end # @@ -200,5 +235,9 @@ def headers 'Content-Type' => 'application/json' } end + + def value_present?(value) + !value.nil? && !value.to_s.strip.empty? + end end end diff --git a/lib/azure_stt/configuration.rb b/lib/azure_stt/configuration.rb index 2b04856..9499f89 100644 --- a/lib/azure_stt/configuration.rb +++ b/lib/azure_stt/configuration.rb @@ -6,7 +6,8 @@ module AzureSTT # the key is in a .env file # class Configuration - attr_accessor :subscription_key, :region + attr_accessor :subscription_key, :region, :government, :private_link, + :ssl_verify_peer, :ssl_ca_file end # diff --git a/lib/azure_stt/session.rb b/lib/azure_stt/session.rb index b53346d..82dcdfc 100644 --- a/lib/azure_stt/session.rb +++ b/lib/azure_stt/session.rb @@ -18,8 +18,19 @@ class Session # read from configuration # def initialize(region: AzureSTT.configuration.region, - subscription_key: AzureSTT.configuration.subscription_key) - @client = Client.new(region: region, subscription_key: subscription_key) + subscription_key: AzureSTT.configuration.subscription_key, + government: AzureSTT.configuration.government, + private_link: AzureSTT.configuration.private_link, + ssl_verify_peer: AzureSTT.configuration.ssl_verify_peer, + ssl_ca_file: AzureSTT.configuration.ssl_ca_file) + @client = Client.new( + region: region, + subscription_key: subscription_key, + government: government, + private_link: private_link, + ssl_verify_peer: ssl_verify_peer, + ssl_ca_file: ssl_ca_file + ) end # diff --git a/lib/azure_stt/version.rb b/lib/azure_stt/version.rb index 6148533..d326475 100644 --- a/lib/azure_stt/version.rb +++ b/lib/azure_stt/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module AzureStt - VERSION = '0.4.0' + VERSION = '0.4.2' end diff --git a/spec/azure_stt/client_spec.rb b/spec/azure_stt/client_spec.rb index 19048ca..fc0c44d 100644 --- a/spec/azure_stt/client_spec.rb +++ b/spec/azure_stt/client_spec.rb @@ -188,4 +188,55 @@ expect(delete_transcription).to be_truthy end end + + describe 'TLS options' do + let(:id) do + '9c142230-a9e4-4dbb-8cc7-70ca43d5cc91' + end + + let(:response_double) do + instance_double(HTTParty::Response, code: 200, parsed_response: {}) + end + + it 'disables certificate verification when ssl_verify_peer is false' do + insecure_client = described_class.new( + region: 'region', + subscription_key: 'ljdhfkjfh', + ssl_verify_peer: false + ) + + expect(described_class) + .to receive(:get) + .with("/transcriptions/#{id}", hash_including(verify: false)) + .and_return(response_double) + + insecure_client.get_transcription(id) + end + + it 'uses the configured CA file when verification is enabled' do + ca_file = '/tmp/private-ca.pem' + secure_client = described_class.new( + region: 'region', + subscription_key: 'ljdhfkjfh', + ssl_verify_peer: true, + ssl_ca_file: ca_file + ) + + expect(described_class) + .to receive(:get) + .with('/transcriptions', hash_including(verify: true, ssl_ca_file: ca_file)) + .and_return(response_double) + + secure_client.get_transcriptions + end + + it 'wraps SSL errors as AzureSTT::NetError' do + allow(described_class) + .to receive(:get) + .and_raise(OpenSSL::SSL::SSLError, 'hostname mismatch') + + expect { client.get_transcription(id) } + .to raise_error(AzureSTT::NetError, /hostname mismatch/) + end + end end diff --git a/spec/azure_stt/session_spec.rb b/spec/azure_stt/session_spec.rb index 820267b..5f89945 100644 --- a/spec/azure_stt/session_spec.rb +++ b/spec/azure_stt/session_spec.rb @@ -17,6 +17,30 @@ .and_return(client) end + describe '#initialize' do + it 'passes endpoint and TLS options to the client' do + described_class.new( + region: 'usgovvirginia', + subscription_key: 'dfhd', + government: true, + private_link: 'https://a2103tsapp1', + ssl_verify_peer: false, + ssl_ca_file: '/tmp/private-ca.pem' + ) + + expect(AzureSTT::Client) + .to have_received(:new) + .with( + region: 'usgovvirginia', + subscription_key: 'dfhd', + government: true, + private_link: 'https://a2103tsapp1', + ssl_verify_peer: false, + ssl_ca_file: '/tmp/private-ca.pem' + ) + end + end + describe '#create_transcription' do subject(:create_transcription) do session.create_transcription(**params)