Compare commits

..

No commits in common. "master" and "v1.6.0" have entirely different histories.

21 changed files with 71 additions and 310 deletions

View File

@ -1,17 +0,0 @@
# 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

@ -180,7 +180,7 @@ module Datadog::Tracing
end end
module RequestMethods module RequestMethods
attr_accessor :init_time attr_reader :init_time
# intercepts request initialization to inject the tracing logic. # intercepts request initialization to inject the tracing logic.
def initialize(*) def initialize(*)
@ -193,30 +193,26 @@ module Datadog::Tracing
RequestTracer.call(self) RequestTracer.call(self)
end end
def response=(*) def response=(response)
# init_time should be set when it's send to a connection. if response.is_a?(::HTTPX::ErrorResponse) && response.error.respond_to?(:connection)
# However, there are situations where connection initialization fails. # handles the case when the +error+ happened during name resolution, which means
# Example is the :ssrf_filter plugin, which raises an error on # that the tracing start point hasn't been triggered yet; in such cases, the approximate
# initialize if the host is an IP which matches against the known set. # initial resolving time is collected from the connection, and used as span start time,
# in such cases, we'll just set here right here. # and the tracing object in inserted before the on response callback is called.
@init_time ||= ::Datadog::Core::Utils::Time.now.utc @init_time = response.error.connection.init_time
end
super super
end end
end end
module ConnectionMethods module ConnectionMethods
attr_reader :init_time
def initialize(*) def initialize(*)
super super
@init_time = ::Datadog::Core::Utils::Time.now.utc @init_time = ::Datadog::Core::Utils::Time.now.utc
end end
def send(request)
request.init_time ||= @init_time
super
end
end end
end end

View File

@ -44,7 +44,7 @@ module HTTPX
attr_accessor :current_session, :family attr_accessor :current_session, :family
protected :ssl_session, :sibling protected :sibling
def initialize(uri, options) def initialize(uri, options)
@current_session = @current_selector = @current_session = @current_selector =
@ -177,7 +177,7 @@ module HTTPX
def merge(connection) def merge(connection)
@origins |= connection.instance_variable_get(:@origins) @origins |= connection.instance_variable_get(:@origins)
if @ssl_session.nil? && connection.ssl_session if connection.ssl_session
@ssl_session = connection.ssl_session @ssl_session = connection.ssl_session
@io.session_new_cb do |sess| @io.session_new_cb do |sess|
@ssl_session = sess @ssl_session = sess
@ -243,10 +243,6 @@ module HTTPX
case @state case @state
when :idle when :idle
connect connect
# when opening the tcp or ssl socket fails
return if @state == :closed
consume consume
when :closed when :closed
return return

View File

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

View File

@ -14,10 +14,7 @@ module HTTPX
def initialize(origin, addresses, options) def initialize(origin, addresses, options)
@state = :idle @state = :idle
@keep_open = false
@addresses = [] @addresses = []
@ip_index = -1
@ip = nil
@hostname = origin.host @hostname = origin.host
@options = options @options = options
@fallback_protocol = @options.fallback_protocol @fallback_protocol = @options.fallback_protocol
@ -56,23 +53,19 @@ module HTTPX
@addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]] @addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]]
else else
@addresses.unshift(*addrs) @addresses.unshift(*addrs)
@ip_index += addrs.size if @ip_index
end end
@ip_index += addrs.size
end end
# eliminates expired entries and returns whether there are still any left. # eliminates expired entries and returns whether there are still any left.
def addresses? def addresses?
prev_addr_size = @addresses.size prev_addr_size = @addresses.size
@addresses.delete_if(&:expired?).sort! do |addr1, addr2| @addresses.delete_if(&:expired?)
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 unless (decr = prev_addr_size - @addresses.size).zero?
@ip_index = @addresses.size - decr
end
@addresses.any? @addresses.any?
end end
@ -88,17 +81,6 @@ module HTTPX
def connect def connect
return unless closed? 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? if !@io || @io.closed?
transition(:idle) transition(:idle)
@io = build_socket @io = build_socket
@ -106,33 +88,29 @@ module HTTPX
try_connect try_connect
rescue Errno::EHOSTUNREACH, rescue Errno::EHOSTUNREACH,
Errno::ENETUNREACH => e Errno::ENETUNREACH => e
@ip_index -= 1 raise e if @ip_index <= 0
raise e if @ip_index.negative?
log { "failed connecting to #{@ip} (#{e.message}), evict from cache and trying next..." } log { "failed connecting to #{@ip} (#{e.message}), evict from cache and trying next..." }
Resolver.cached_lookup_evict(@hostname, @ip) Resolver.cached_lookup_evict(@hostname, @ip)
@ip_index -= 1
@io = build_socket @io = build_socket
retry retry
rescue Errno::ECONNREFUSED, rescue Errno::ECONNREFUSED,
Errno::EADDRNOTAVAIL, Errno::EADDRNOTAVAIL,
SocketError, SocketError,
IOError => e IOError => e
@ip_index -= 1 raise e if @ip_index <= 0
raise e if @ip_index.negative?
log { "failed connecting to #{@ip} (#{e.message}), trying next..." } log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
@ip_index -= 1
@io = build_socket @io = build_socket
retry retry
rescue Errno::ETIMEDOUT => e rescue Errno::ETIMEDOUT => e
@ip_index -= 1 raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index.negative?
log { "failed connecting to #{@ip} (#{e.message}), trying next..." } log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
@ip_index -= 1
@io = build_socket @io = build_socket
retry retry
end end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require "socket"
module HTTPX module HTTPX
# Contains a set of options which are passed and shared across from session to its requests or # Contains a set of options which are passed and shared across from session to its requests or
# responses. # responses.
@ -149,14 +151,6 @@ module HTTPX
def freeze def freeze
self.class.options_names.each do |ivar| 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 instance_variable_get(:"@#{ivar}").freeze
end end
super super
@ -412,6 +406,18 @@ module HTTPX
end end
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 = { DEFAULT_OPTIONS = {
:max_requests => Float::INFINITY, :max_requests => Float::INFINITY,
:debug => nil, :debug => nil,
@ -456,7 +462,7 @@ module HTTPX
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym, :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
:resolver_options => { cache: true }.freeze, :resolver_options => { cache: true }.freeze,
:pool_options => EMPTY_HASH, :pool_options => EMPTY_HASH,
:ip_families => nil, :ip_families => ip_address_families,
:close_on_fork => false, :close_on_fork => false,
}.freeze }.freeze
end end

View File

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

View File

@ -1,6 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require "socket"
require "resolv" require "resolv"
module HTTPX module HTTPX
@ -23,20 +22,6 @@ module HTTPX
module_function 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) def resolver_for(resolver_type, options)
case resolver_type case resolver_type
when Symbol when Symbol

View File

@ -15,9 +15,7 @@ module HTTPX
@options = options @options = options
@resolver_options = @options.resolver_options @resolver_options = @options.resolver_options
ip_families = options.ip_families || Resolver.supported_ip_families @resolvers = options.ip_families.map do |ip_family|
@resolvers = ip_families.map do |ip_family|
resolver = resolver_type.new(ip_family, options) resolver = resolver_type.new(ip_family, options)
resolver.multi = self resolver.multi = self
resolver resolver
@ -69,12 +67,8 @@ module HTTPX
addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname)) addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
return false unless addresses return false unless addresses
ip_families = connection.options.ip_families
resolved = false resolved = false
addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs| addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
next unless ip_families.nil? || ip_families.include?(family)
# try to match the resolver by family. However, there are cases where that's not possible, as when # 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. # 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 resolver = @resolvers.find { |r| r.family == family } || @resolvers.first

View File

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

View File

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

View File

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

View File

@ -1,42 +0,0 @@
# 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 end
class GoawayError < Error class GoawayError < Error
def initialize: (?Symbol code) -> void def initialize: () -> void
end end
class PingError < Error class PingError < Error

View File

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

View File

@ -27,21 +27,7 @@ module HTTPX
attr_reader scope: Array[String]? attr_reader scope: Array[String]?
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) -> void
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 def token_endpoint: () -> String

View File

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

View File

@ -1,63 +0,0 @@
# 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,21 +16,6 @@ module Requests
assert opts.oauth_session.token_endpoint.to_s == "#{server.origin}/token" 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.token_endpoint_auth_method == "client_secret_basic"
assert opts.oauth_session.scope == %w[all] 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 # from options, pointing to refresh
opts = HTTPX.plugin(:oauth).oauth_auth( opts = HTTPX.plugin(:oauth).oauth_auth(
@ -90,26 +75,6 @@ module Requests
end end
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 def test_plugin_oauth_client_credentials
with_oauth_metadata do |server| with_oauth_metadata do |server|
session = HTTPX.plugin(:oauth).oauth_auth( session = HTTPX.plugin(:oauth).oauth_auth(

View File

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

View File

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