Skip to content
Open
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,61 @@ 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:

```ruby
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
Expand Down
6 changes: 5 additions & 1 deletion env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
REGION=centralus
SUBSCRIPTION_KEY=k3ah8ztrc4ojeh98r05zh7v6x9w62lqp
SUBSCRIPTION_KEY=k3ah8ztrc4ojeh98r05zh7v6x9w62lqp
GOVERNMENT=false
PRIVATE_LINK=
SSL_VERIFY_PEER=false
SSL_CA_FILE=
7 changes: 7 additions & 0 deletions lib/azure_stt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
69 changes: 54 additions & 15 deletions lib/azure_stt/client.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
# frozen_string_literal: true

require 'httparty'
require 'openssl'
module AzureSTT
#
# Client class that uses HTTParty to communicate with the API
#
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
#
# @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

#
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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

Expand All @@ -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

#
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion lib/azure_stt/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

#
Expand Down
15 changes: 13 additions & 2 deletions lib/azure_stt/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

#
Expand Down
2 changes: 1 addition & 1 deletion lib/azure_stt/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module AzureStt
VERSION = '0.4.0'
VERSION = '0.4.2'
end
51 changes: 51 additions & 0 deletions spec/azure_stt/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions spec/azure_stt/session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down