Compare commits

...

16 Commits

Author SHA1 Message Date
HoneyryderChuck
0576e81774 bump version to 1.6.1 2025-09-23 11:39:50 +01:00
HoneyryderChuck
267bbea7f9 Merge branch 'gh-103' into 'master'
fix: do not check if multi-homed network at boot time, instead do it a runtime

See merge request os85/httpx!408
2025-09-23 10:03:12 +00:00
HoneyryderChuck
861e6ca94b native resolver: do not rely on resolver supported ip families if options for ip_families is explicitly used
option from user takes precedence
2025-09-22 15:45:08 +01:00
HoneyryderChuck
57629cb9b0 fix: do not check if multi-homed network at boot time, instead do it a runtime
this requires calling , which calls the  syscall, which may be blocked at boot time in certain constrained environments

this is moved into a module function, which memoizes the result; it's done in a non-thread safe way, but since the result is expected to be the same, it should be ok to race
2025-09-22 15:45:08 +01:00
Oliver Morgan
872a7390a7 added audience to oauth 2025-09-22 09:19:20 +01:00
HoneyryderChuck
1d6e735bd6 fix for last commit: Comparable overrides == to call <=>
this cascades down terrible since we're using entries as proxies for strings...
2025-09-19 16:58:50 +01:00
HoneyryderChuck
25fb2b75e3 on call to .addresses?, reorder besides removing expired
in case there was misalignment when doing HEv2, this brings ipv6 address forward in case the socket started with ipv4 addresses and got ipv6 addresses meanwhile
2025-09-19 14:19:27 +01:00
HoneyryderChuck
d8547e8ad0 Merge branch 'gh-100' into 'master'
fixes to socket addresses expiration rebalancing

See merge request os85/httpx!407
2025-09-19 10:34:20 +00:00
HoneyryderChuck
cee84e1226 http2: aways emit goaway error and teardown the connection independent of the error code
a case was reported where a goaway error was received with a cancel error code; this code seems to have been misused by the peer server, since it is a stream error code, however the client should react to it by just closing it anyway, as the http2 parser already moved on to half-closed anyway, so not accepting new streams anymore

fixes github.com/HoneyryderChuck/httpx/issues/102
2025-09-19 11:09:54 +01:00
HoneyryderChuck
e7699950b2 fix: do not reattempt connecting if there are no available addresses
usually, a connection connects when there are available IPs to try.
in a persistent connection scenario, the same holds. however, in a
fiber-based environment, fiber 1 may wait for request 1, while fiber 2
may, while attempting request 2, receive a GOAWAY frame, and initiate
termination/reconnection/resend while switching on the DNS request
phase,
whereas when the context is switched back to fiber 1, request 1 has
already
failed due to what happened in fiber 2 and should be reattempted,
however the
connection is still waiting on the DNS answer, so the reattempt should
just be enqueued.

This fixes by relying on the assumption that an idle connection with no
addresses in the select loop is a connection that has been sent back to
the name resolution loop, so the flow needs to ignore it and go back to
the request handling loop, which will forward the request-to-retry and
wait on the same resolution.
2025-09-19 07:29:52 +01:00
HoneyryderChuck
bbd4a59892 when addresses from the list expire, always assume that one of them is the current one, and point to the last element of the list (or none) 2025-09-16 14:31:01 +01:00
HoneyryderChuck
9a11846f11 fix: on connection coalescing, do not merge ssl session if the main connection already has one ssl session established
ssl context object gets frozen after initialization and cannot therefore be changed

Fixes https://github.com/HoneyryderChuck/httpx/issues/101
2025-09-16 14:31:01 +01:00
HoneyryderChuck
3c42c6cc7f tcp: make sure that ip index decrement on error allows going all the way to -1
so that recalcuation on adding new addresses works the same
2025-09-16 14:31:01 +01:00
HoneyryderChuck
7c65886a9c fix: when an ipv6 address gets added to the tcp addresses list, ip index must be updated to continue pointing to the same address 2025-09-16 14:31:01 +01:00
HoneyryderChuck
fb95825990 tcp socket: make sure all ivars from the class are initialized in a consistent order
so there's only 1 object shape
2025-09-16 14:31:01 +01:00
HoneyryderChuck
25b6be7113 do not freeze the :debug option
this gets assigned objects like stdout or stderr, which break logging when frozen; this should therefore be a case of a known exception, considering that the main goal of this is to make options shareable across ractors
2025-09-16 14:30:45 +01:00
20 changed files with 295 additions and 60 deletions

View File

@ -0,0 +1,17 @@
# 1.6.1
## Improvements
* `:oauth` plugin: `.oauth_session` can be called with an `:audience` parameter, which has the effect of adding it as an extra form body parameter of the token request.
## Bugfixes
* options: when freezing the options, skip freezing `:debug`; it's usually a file/IO/stream object (stdout, stderr...), which makes it error when log messages are written.
* tcp: fixed adding IPv6 addresses to a tcp object when IPv4 connection probe is ongoing so that the next try uses the first ipv6 address.
* tcp: reorder addresses on reconnection, so ipv6 is tried first in case it is still valid.
* tcp: make sure ip index is decremented on error, so the next tried IP may be a valid one.
* tcp: do not reattempt connecting if there are no available addresses to connect. This may happen in a fiber-aware context, where fiber A waits on connection, fiber B reconnects as a result on an error or GOAWAY frame and waits on the resolver DNS answer, and when context is passed back to fiber B, it should go back to the invalidate the response and try again while waiting on the resolver as well.
* ssl: on connection coalescing, do not merge the ssl sessions, as these are frozen post-initialization.
* http2: all received GOAWAY frames emit goaway error and teardown the connection independent of the error code (it was only doing it for `:noerror`, but others may appear).
* do not check at require time whether the network is multi-homed; instead, defer it to first use and cache (this can break environments which block access to certain syscalls during boot time).
* options: do not ignore when user sets `:ip_families` in name resolution.

View File

@ -44,7 +44,7 @@ module HTTPX
attr_accessor :current_session, :family
protected :sibling
protected :ssl_session, :sibling
def initialize(uri, options)
@current_session = @current_selector =
@ -177,7 +177,7 @@ module HTTPX
def merge(connection)
@origins |= connection.instance_variable_get(:@origins)
if connection.ssl_session
if @ssl_session.nil? && connection.ssl_session
@ssl_session = connection.ssl_session
@io.session_new_cb do |sess|
@ssl_session = sess

View File

@ -23,8 +23,8 @@ module HTTPX
end
class GoawayError < Error
def initialize
super(0, :no_error)
def initialize(code = :no_error)
super(0, code)
end
end
@ -385,12 +385,10 @@ module HTTPX
while (request = @pending.shift)
emit(:error, request, error)
end
when :no_error
ex = GoawayError.new
else
ex = GoawayError.new(error)
@pending.unshift(*@streams.keys)
teardown
else
ex = Error.new(0, error)
end
if ex

View File

@ -14,7 +14,10 @@ module HTTPX
def initialize(origin, addresses, options)
@state = :idle
@keep_open = false
@addresses = []
@ip_index = -1
@ip = nil
@hostname = origin.host
@options = options
@fallback_protocol = @options.fallback_protocol
@ -53,20 +56,24 @@ module HTTPX
@addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]]
else
@addresses.unshift(*addrs)
@ip_index += addrs.size if @ip_index
end
@ip_index += addrs.size
end
# eliminates expired entries and returns whether there are still any left.
def addresses?
prev_addr_size = @addresses.size
@addresses.delete_if(&:expired?)
unless (decr = prev_addr_size - @addresses.size).zero?
@ip_index = @addresses.size - decr
@addresses.delete_if(&:expired?).sort! do |addr1, addr2|
if addr1.ipv6?
addr2.ipv6? ? 0 : 1
else
addr2.ipv6? ? -1 : 0
end
end
@ip_index = @addresses.size - 1 if prev_addr_size != @addresses.size
@addresses.any?
end
@ -81,6 +88,17 @@ module HTTPX
def connect
return unless closed?
if @addresses.empty?
# an idle connection trying to connect with no available addresses is a connection
# out of the initial context which is back to the DNS resolution loop. This may
# happen in a fiber-aware context where a connection reconnects with expired addresses,
# and context is passed back to a fiber on the same connection while waiting for the
# DNS answer.
log { "tried connecting while resolving, skipping..." }
return
end
if !@io || @io.closed?
transition(:idle)
@io = build_socket
@ -88,29 +106,33 @@ module HTTPX
try_connect
rescue Errno::EHOSTUNREACH,
Errno::ENETUNREACH => e
raise e if @ip_index <= 0
@ip_index -= 1
raise e if @ip_index.negative?
log { "failed connecting to #{@ip} (#{e.message}), evict from cache and trying next..." }
Resolver.cached_lookup_evict(@hostname, @ip)
@ip_index -= 1
@io = build_socket
retry
rescue Errno::ECONNREFUSED,
Errno::EADDRNOTAVAIL,
SocketError,
IOError => e
raise e if @ip_index <= 0
@ip_index -= 1
raise e if @ip_index.negative?
log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
@ip_index -= 1
@io = build_socket
retry
rescue Errno::ETIMEDOUT => e
raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
@ip_index -= 1
raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index.negative?
log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
@ip_index -= 1
@io = build_socket
retry
end

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
require "socket"
module HTTPX
# Contains a set of options which are passed and shared across from session to its requests or
# responses.
@ -151,6 +149,14 @@ module HTTPX
def freeze
self.class.options_names.each do |ivar|
# avoid freezing debug option, as when it's set, it's usually an
# object which cannot be frozen, like stderr or stdout. It's a
# documented exception then, and still does not defeat the purpose
# here, which is to make option objects shareable across ractors,
# and in most cases debug should be nil, or one of the objects
# which will eventually be shareable, like STDOUT or STDERR.
next if ivar == :debug
instance_variable_get(:"@#{ivar}").freeze
end
super
@ -406,18 +412,6 @@ module HTTPX
end
end
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
ip_address_families = begin
list = Socket.ip_address_list
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
[Socket::AF_INET6, Socket::AF_INET]
else
[Socket::AF_INET]
end
rescue NotImplementedError
[Socket::AF_INET]
end.freeze
DEFAULT_OPTIONS = {
:max_requests => Float::INFINITY,
:debug => nil,
@ -462,7 +456,7 @@ module HTTPX
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
:resolver_options => { cache: true }.freeze,
:pool_options => EMPTY_HASH,
:ip_families => ip_address_families,
:ip_families => nil,
:close_on_fork => false,
}.freeze
end

View File

@ -16,7 +16,7 @@ module HTTPX
SUPPORTED_AUTH_METHODS = %w[client_secret_basic client_secret_post].freeze
class OAuthSession
attr_reader :grant_type, :client_id, :client_secret, :access_token, :refresh_token, :scope
attr_reader :grant_type, :client_id, :client_secret, :access_token, :refresh_token, :scope, :audience
def initialize(
issuer:,
@ -25,6 +25,7 @@ module HTTPX
access_token: nil,
refresh_token: nil,
scope: nil,
audience: nil,
token_endpoint: nil,
response_type: nil,
grant_type: nil,
@ -41,6 +42,7 @@ module HTTPX
when Array
scope
end
@audience = audience
@access_token = access_token
@refresh_token = refresh_token
@token_endpoint_auth_method = String(token_endpoint_auth_method) if token_endpoint_auth_method
@ -125,7 +127,11 @@ module HTTPX
grant_type = oauth_session.grant_type
headers = {}
form_post = { "grant_type" => grant_type, "scope" => Array(oauth_session.scope).join(" ") }.compact
form_post = {
"grant_type" => grant_type,
"scope" => Array(oauth_session.scope).join(" "),
"audience" => oauth_session.audience,
}.compact
# auth
case oauth_session.token_endpoint_auth_method

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true
require "socket"
require "resolv"
module HTTPX
@ -22,6 +23,20 @@ module HTTPX
module_function
def supported_ip_families
@supported_ip_families ||= begin
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
list = Socket.ip_address_list
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
[Socket::AF_INET6, Socket::AF_INET]
else
[Socket::AF_INET]
end
rescue NotImplementedError
[Socket::AF_INET]
end.freeze
end
def resolver_for(resolver_type, options)
case resolver_type
when Symbol

View File

@ -15,7 +15,9 @@ module HTTPX
@options = options
@resolver_options = @options.resolver_options
@resolvers = options.ip_families.map do |ip_family|
ip_families = options.ip_families || Resolver.supported_ip_families
@resolvers = ip_families.map do |ip_family|
resolver = resolver_type.new(ip_family, options)
resolver.multi = self
resolver
@ -67,8 +69,12 @@ module HTTPX
addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
return false unless addresses
ip_families = connection.options.ip_families || Resolver.supported_ip_families
resolved = false
addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
next unless ip_families.include?(family)
# try to match the resolver by family. However, there are cases where that's not possible, as when
# the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
@ -85,7 +91,11 @@ module HTTPX
end
def lazy_resolve(connection)
ip_families = connection.options.ip_families || Resolver.supported_ip_families
@resolvers.each do |resolver|
next unless ip_families.include?(resolver.family)
resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
next if resolver.empty?

View File

@ -79,12 +79,18 @@ module HTTPX
"answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
end
if !early_resolve && # do not apply resolution delay for non-dns name resolution
@current_selector && # just in case...
family == Socket::AF_INET && # resolution delay only applies to IPv4
!connection.io && # connection already has addresses and initiated/ended handshake
connection.options.ip_families.size > 1 && # no need to delay if not supporting dual stack IP
addresses.first.to_s != connection.peer.host.to_s # connection URL host is already the IP (early resolve included perhaps?)
# do not apply resolution delay for non-dns name resolution
if !early_resolve &&
# just in case...
@current_selector &&
# resolution delay only applies to IPv4
family == Socket::AF_INET &&
# connection already has addresses and initiated/ended handshake
!connection.io &&
# no need to delay if not supporting dual stack / multi-homed IP
(connection.options.ip_families || Resolver.supported_ip_families).size > 1 &&
# connection URL host is already the IP (early resolve included perhaps?)
addresses.first.to_s != connection.peer.host.to_s
log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
@current_selector.after(0.05) do

View File

@ -187,7 +187,9 @@ module HTTPX
transition(:open)
connection.options.ip_families.each do |family|
ip_families = connection.options.ip_families || Resolver.supported_ip_families
ip_families.each do |family|
@queries << [family, connection]
end
async_resolve(connection, hostname, scheme)
@ -195,7 +197,7 @@ module HTTPX
end
def async_resolve(connection, hostname, scheme)
families = connection.options.ip_families
families = connection.options.ip_families || Resolver.supported_ip_families
log { "resolver: query for #{hostname}" }
timeouts = @timeouts[connection.peer.host]
resolve_timeout = timeouts.first

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
module HTTPX
VERSION = "1.6.0"
VERSION = "1.6.1"
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require "test_helper"
require "support/http_helpers"
require "webmock/minitest"
class Bug_1_6_1_Test < Minitest::Test
include HTTPHelpers
def test_retries_should_retry_on_goaway_cancel
start_test_servlet(GoawayCancelErrorServer) do |server|
http = HTTPX.plugin(SessionWithPool)
.plugin(RequestInspector)
.plugin(:retries)
.with(ssl: { verify_mode: OpenSSL::SSL::VERIFY_NONE })
uri = "#{server.origin}/"
response = http.get(uri)
verify_status(response, 200)
assert http.calls == 1, "expect request to be built 1 more time (was #{http.calls})"
http.close
end
end
class GoawayCancelErrorServer < TestHTTP2Server
def initialize(**)
@sent = Hash.new(false)
super
end
private
def handle_stream(conn, stream)
if @cancelled
super
else
conn.goaway(:cancel)
@cancelled = true
end
end
end
end

View File

@ -106,7 +106,7 @@ module HTTPX
end
class GoawayError < Error
def initialize: () -> void
def initialize: (?Symbol code) -> void
end
class PingError < Error

View File

@ -130,7 +130,7 @@ module HTTPX
attr_reader pool_options: pool_options
# ip_families
attr_reader ip_families: Array[ip_family]
attr_reader ip_families: Array[ip_family]?
def ==: (Options other) -> bool
@ -195,7 +195,7 @@ module HTTPX
def option_addresses: (ipaddr | _ToAry[ipaddr] value) -> Array[ipaddr]
def option_ip_families: (Integer | _ToAry[Integer] value) -> Array[Integer]
def option_ip_families: (ip_family | _ToAry[ip_family] value) -> Array[ip_family]
end
type options = Options | Hash[Symbol, untyped]

View File

@ -27,7 +27,21 @@ module HTTPX
attr_reader scope: Array[String]?
def initialize: (issuer: uri, client_id: String, client_secret: String, ?access_token: String?, ?refresh_token: String?, ?scope: (Array[String] | String)?, ?token_endpoint: String?, ?response_type: String?, ?grant_type: String?, ?token_endpoint_auth_method: ::String) -> void
attr_reader audience: String?
def initialize: (
issuer: uri,
client_id: String,
client_secret: String,
?access_token: String?,
?refresh_token: String?,
?scope: (Array[String] | String)?,
?token_endpoint: String?,
?response_type: String?,
?grant_type: String?,
?token_endpoint_auth_method: ::String,
?audience: ::String
) -> void
def token_endpoint: () -> String

View File

@ -23,6 +23,8 @@ module HTTPX
def self?.hosts_resolve: (String hostname) -> Array[Entry]?
def self?.supported_ip_families: () -> Array[ip_family]
def self?.resolver_for: (Symbol | singleton(Resolver) resolver_type, Options options) -> singleton(Resolver)
def self?.cached_lookup: (String hostname) -> Array[Entry]?

63
test/io/tcp_test.rb Normal file
View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require "tempfile"
require_relative "../test_helper"
class TCPTest < Minitest::Test
include HTTPX
def test_tcp_ip_index_rebalance_on_new_addresses
origin = URI("http://example.com")
options = Options.new
tcp_class = Class.new(TCP) do
attr_accessor :ip_index
end
# initialize with no addresses, ip index points nowhere
tcp = tcp_class.new(origin, [], options)
assert tcp.ip_index == -1
# initialize with addresses, ip index points to the last element
tcp1 = tcp_class.new(origin, [Resolver::Entry.new("127.0.0.1")], options)
assert tcp1.addresses == ["127.0.0.1"]
assert tcp1.ip_index.zero?
tcp2 = tcp_class.new(origin, [Resolver::Entry.new("127.0.0.1"), Resolver::Entry.new("127.0.0.2")], options)
assert tcp2.addresses == ["127.0.0.1", "127.0.0.2"]
assert tcp2.ip_index == 1
tcp3 = tcp_class.new(origin, [Resolver::Entry.new("::1")], options)
assert tcp3.addresses == ["::1"]
assert tcp3.ip_index.zero?
# add addresses, ip index must point to previous ip after address expansion
tcp.add_addresses([Resolver::Entry.new("::1")])
assert tcp.addresses == ["::1"]
assert tcp.ip_index.zero?
tcp1.add_addresses([Resolver::Entry.new("::1")])
assert tcp1.addresses == ["::1", "127.0.0.1"]
assert tcp1.ip_index == 1
# makes the ipv6 address the next address to try
tcp2.add_addresses([Resolver::Entry.new("::1")])
assert tcp2.addresses == ["127.0.0.1", "::1", "127.0.0.2"]
assert tcp2.ip_index == 2
tcp3.add_addresses([Resolver::Entry.new("127.0.0.1")])
assert tcp3.addresses == ["127.0.0.1", "::1"]
assert tcp3.ip_index == 1
tcp3.add_addresses([Resolver::Entry.new("::2")])
assert tcp3.addresses == ["127.0.0.1", "::2", "::1"]
assert tcp3.ip_index == 2
# expiring entries should recalculate the pointer
now = Utils.now
tcp4 = tcp_class.new(origin, [Resolver::Entry.new("127.0.0.1", now + 1), Resolver::Entry.new("127.0.0.2", now + 4)], options)
assert tcp4.addresses == ["127.0.0.1", "127.0.0.2"]
assert tcp4.ip_index == 1
sleep(2)
assert tcp4.addresses?
assert tcp4.addresses == ["127.0.0.2"]
assert tcp4.ip_index.zero?
sleep(2)
assert !tcp4.addresses?
assert tcp4.ip_index == -1
end
end

View File

@ -16,6 +16,21 @@ module Requests
assert opts.oauth_session.token_endpoint.to_s == "#{server.origin}/token"
assert opts.oauth_session.token_endpoint_auth_method == "client_secret_basic"
assert opts.oauth_session.scope == %w[all]
assert opts.oauth_session.audience.nil?
# with audience
opts = HTTPX.plugin(:oauth).oauth_auth(
issuer: server.origin,
client_id: "CLIENT_ID", client_secret: "SECRET",
scope: "all",
audience: "audience"
).instance_variable_get(:@options)
assert opts.oauth_session.grant_type == "client_credentials"
assert opts.oauth_session.token_endpoint.to_s == "#{server.origin}/token"
assert opts.oauth_session.token_endpoint_auth_method == "client_secret_basic"
assert opts.oauth_session.scope == %w[all]
assert opts.oauth_session.audience == "audience"
# from options, pointing to refresh
opts = HTTPX.plugin(:oauth).oauth_auth(
@ -75,6 +90,26 @@ module Requests
end
end
def test_plugin_oauth_access_token_audience
with_oauth_metadata do |server|
http = HTTPX.plugin(:oauth).oauth_auth(
issuer: server.origin,
client_id: "CLIENT_ID", client_secret: "SECRET",
scope: "all",
)
http_aud = http.oauth_auth(
issuer: server.origin,
client_id: "CLIENT_ID", client_secret: "SECRET",
scope: "all", audience: "audience"
)
http_opts = http.with_access_token.instance_variable_get(:@options)
http_aud_opts = http_aud.with_access_token.instance_variable_get(:@options)
assert http_opts.oauth_session.access_token == "CLIENT-CREDS-AUTH"
assert http_aud_opts.oauth_session.access_token == "CLIENT-CREDS-AUTH-audience"
end
end
def test_plugin_oauth_client_credentials
with_oauth_metadata do |server|
session = HTTPX.plugin(:oauth).oauth_auth(

View File

@ -3,14 +3,13 @@
require_relative "test"
class ByIpCertServer < TestServer
USE_IPV6 = HTTPX::Options::DEFAULT_OPTIONS[:ip_families].size > 1
CERTS_DIR = File.expand_path("../ci/certs", __dir__)
def initialize
cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERTS_DIR, "localhost-server.crt")))
key = OpenSSL::PKey.read(File.read(File.join(CERTS_DIR, "localhost-server.key")))
super(
:BindAddress => USE_IPV6 ? "::1" : "127.0.0.1",
:BindAddress => HTTPX::Resolver.supported_ip_families.size > 1 ? "::1" : "127.0.0.1",
:SSLEnable => true,
:SSLCertificate => cert,
:SSLPrivateKey => key,

View File

@ -26,20 +26,30 @@ class OAuthProviderServer < TestServer
end
res["content-type"] = "application/json"
token = +""
case body["grant_type"]
when "client_credentials"
if user == "CLIENT_ID" && pass == "SECRET"
res.body = JSON.dump({ "access_token" => "CLIENT-CREDS-AUTH", "expires_in" => 3600, "token_type" => "bearer" })
nil
token << "CLIENT-CREDS-AUTH" if user == "CLIENT_ID" && pass == "SECRET"
if (aud = body["audience"])
token << "-" << aud
end
res.body = JSON.dump({ "access_token" => token, "expires_in" => 3600, "token_type" => "bearer" })
when "refresh_token"
if user == "CLIENT_ID" && pass == "SECRET" && body["refresh_token"] == "REFRESH_TOKEN"
res.body = JSON.dump({ "access_token" => "REFRESH-TOKEN-AUTH", "expires_in" => 3600, "token_type" => "bearer" })
nil
token << "REFRESH-TOKEN-AUTH" if user == "CLIENT_ID" && pass == "SECRET" && body["refresh_token"] == "REFRESH_TOKEN"
if (aud = body["audience"])
token << "-" << aud
end
else
raise "unsupported"
end
raise "unsupported" if token.empty?
res.body = JSON.dump({ "access_token" => token, "expires_in" => 3600, "token_type" => "bearer" })
end
end