cli
1# frozen_string_literal: true
2
3module Scim
4 module Kit
5 class Http
6 Result = Struct.new(:status, :body, :unparsed) do
7 def ok?
8 !status.nil? && (200..299).cover?(status) && !unparsed
9 end
10 end
11
12 MAX_REDIRECTS = 3
13
14 attr_reader :driver, :retries
15
16 def initialize(driver: Http.default_driver, retries: 3)
17 @driver = driver
18 @retries = retries
19 end
20
21 def get(uri)
22 result = fetch(uri)
23 result.ok? ? result.body : {}
24 end
25
26 def fetch(uri, headers: {})
27 driver.with_retry(retries: retries) do |client|
28 result_for(get_following_redirects(client, uri, headers))
29 end
30 rescue *Net::Hippie::CONNECTION_ERRORS => error
31 Scim::Kit.logger.error(error)
32 Result.new(nil, { detail: error.message })
33 end
34
35 # No :logger here on purpose -- net-hippie hands it to
36 # Net::HTTP#set_debug_output, which dumps raw requests (credentials
37 # included) to the log.
38 def self.default_driver
39 @default_driver ||= Net::Hippie::Client.new(
40 follow_redirects: 0,
41 headers: headers,
42 open_timeout: 1,
43 read_timeout: 5
44 )
45 end
46
47 def self.headers
48 {
49 'Accept' => 'application/scim+json',
50 'Content-Type' => 'application/scim+json',
51 'User-Agent' => "scim/kit #{Scim::Kit::VERSION}"
52 }
53 end
54
55 private
56
57 # net-hippie rebuilds the redirected request without the per-request
58 # headers, so follow redirects here to keep them.
59 def get_following_redirects(client, uri, headers, limit: MAX_REDIRECTS)
60 uri = URI.parse(uri.to_s)
61 response = client.get(uri, headers: headers)
62 location = response['location'] if response.is_a?(Net::HTTPRedirection)
63 return response if limit.zero? || location.to_s.empty?
64
65 target = uri.merge(location)
66 get_following_redirects(
67 client, target, forwardable(headers, uri, target), limit: limit - 1
68 )
69 end
70
71 def forwardable(headers, from, to)
72 return headers if origin(from) == origin(to)
73
74 headers.reject { |name, _| name.to_s.casecmp?('authorization') }
75 end
76
77 def origin(uri)
78 [uri.scheme, uri.host, uri.port]
79 end
80
81 # An unparsed body is reported rather than raised, so the caller can
82 # show it, but it is never a successful result.
83 def result_for(response)
84 Result.new(response.code.to_i, parse(response.body))
85 rescue JSON::ParserError => error
86 Scim::Kit.logger.error(error)
87 Result.new(response.code.to_i, { detail: response.body }, true)
88 end
89
90 def parse(body)
91 return {} if body.nil?
92
93 JSON.parse(body, symbolize_names: true)
94 end
95 end
96 end
97end