Module: Conversant::V3::HttpClient

Included in:
Base
Defined in:
lib/conversant/v3/http_client.rb,
sig/conversant/v3.rbs

Overview

HTTP client module

Constant Summary collapse

PORTAL_SESSION_REDIS_KEY =

Redis key for storing portal SESSION cookie

Returns:

  • (String)

Since:

  • 1.0.0

'CONVERSANT.V3.PORTAL.SESSION'
SSO_GW_SESSION2_REDIS_KEY =

Redis key for storing SSO_GW_SESSION2 cookie

Returns:

  • (String)

Since:

  • 1.0.0

'CONVERSANT.V3.PORTAL.SSO_GW_SESSION2'

Instance Method Summary collapse

Instance Method Details

#add_cookies_to_headers(headers) ⇒ Hash{String => String}

Adds cookies from cookie jar to request headers

Parameters:

  • headers (Hash{String => String})

    Request headers

Returns:

  • (Hash{String => String})

    Headers with Cookie header added if cookies exist

Since:

  • 1.0.0



357
358
359
360
361
362
# File 'lib/conversant/v3/http_client.rb', line 357

def add_cookies_to_headers(headers)
  return headers if cookie_jar.nil? || cookie_jar.empty?

  cookie_string = cookie_jar.map { |name, value| "#{name}=#{value}" }.join('; ')
  headers.merge('Cookie' => cookie_string)
end

#authenticateHash{Symbol => String}?

Authenticates and returns cached SSO sessions

Wrapper around sso_login that ensures sessions are cached in Redis. This is the primary method used by service classes for authentication.

Examples:

sessions = authenticate
if sessions
  headers['Cookie'] = "SESSION=#{sessions[:session]}; SSO_GW_SESSION2=#{sessions[:sso_gw_session2]}"
end

Returns:

  • (Hash{Symbol => String}, nil)

    Hash with :session and :sso_gw_session2 keys, or nil on failure

Since:

  • 1.0.0



187
188
189
190
191
192
193
194
195
# File 'lib/conversant/v3/http_client.rb', line 187

def authenticate
  result = 

  return unless result && result[:session]

  redis.set(PORTAL_SESSION_REDIS_KEY, result[:session], ex: configuration.cache_ttl)
  redis.set(SSO_GW_SESSION2_REDIS_KEY, result[:sso_gw_session2], ex: configuration.cache_ttl) if result[:sso_gw_session2]
  result
end

#configurationConversant::Configuration

Returns the global Conversant configuration

Returns:

Since:

  • 1.0.0



442
443
444
# File 'lib/conversant/v3/http_client.rb', line 442

def configuration
  Conversant.configuration
end

Gets the thread-local cookie jar for storing cookies during authentication

The cookie jar is used to maintain cookies across multiple HTTP requests during the SSO login flow. Each thread has its own isolated cookie jar.

Examples:

cookie_jar['SESSION'] = 'abc123'
puts cookie_jar['SESSION'] # => 'abc123'

Returns:

  • (Hash{String => String})

    Cookie name-value pairs

Since:

  • 1.0.0



41
42
43
# File 'lib/conversant/v3/http_client.rb', line 41

def cookie_jar
  Thread.current[:conversant_cookie_jar] ||= {}
end

Sets the thread-local cookie jar

Examples:

self.cookie_jar = { 'SESSION' => 'abc123' }
self.cookie_jar = nil # Clear cookies

Parameters:

  • value (Hash, nil)

    Cookie name-value pairs, or nil to clear

Returns:

  • (Hash, nil)

    The assigned value

Since:

  • 1.0.0



55
56
57
# File 'lib/conversant/v3/http_client.rb', line 55

def cookie_jar=(value)
  Thread.current[:conversant_cookie_jar] = value
end

#debug_log(message) ⇒ void

This method returns an undefined value.

Logs a debug message if debug mode is enabled

Examples:

debug_log "Starting authentication"

Parameters:

  • message (String)

    The message to log

Since:

  • 1.0.0



68
69
70
71
72
# File 'lib/conversant/v3/http_client.rb', line 68

def debug_log(message)
  return unless configuration.debug_mode

  configuration.logger.debug "Conversant::V3 - #{message}"
end

#extract_form_action(html_body) ⇒ String?

Extracts form action URL from HTML login page

Searches for the Keycloak login form action URL in the HTML.

Parameters:

  • html_body (String)

    HTML content of login page

Returns:

  • (String, nil)

    Form action URL, or nil if not found

Since:

  • 1.0.0



415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/conversant/v3/http_client.rb', line 415

def extract_form_action(html_body)
  return nil unless html_body

  # Parse HTML to find form action URL
  match = html_body.match(/form[^>]*id=["']kc-form-login["'][^>]*action=["']([^"']*)["']/)
  return CGI.unescapeHTML(match[1]) if match

  # Fallback: look for any form with action
  match = html_body.match(/form[^>]*action=["']([^"']*)["']/)
  return CGI.unescapeHTML(match[1]) if match

  nil
end

#http_get(url) ⇒ Hash{Symbol => untyped}?

Performs an HTTP GET request with cookie management

Handles redirects automatically and updates the cookie jar from responses. Used internally during SSO login flow.

Parameters:

  • url (String)

    URL to fetch

Returns:

  • (Hash{Symbol => untyped}, nil)

    Response hash with :status, :body, :headers keys, or nil on error

Since:

  • 1.0.0



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/conversant/v3/http_client.rb', line 251

def http_get(url)
  headers = {
    'User-Agent' => configuration.default_ua,
    'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
  }

  headers = add_cookies_to_headers(headers)

  response = RestClient::Request.execute(
    method: :get,
    url: url,
    headers: headers,
    verify_ssl: configuration.verify_ssl,
    max_redirects: 10
  ) do |resp, _request, _result|
    update_cookie_jar(resp)

    case resp.code
    when 200..299
      resp
    when 300..399
      resp.follow_redirection
    else
      resp
    end
  end

  update_cookie_jar(response)

  {
    status: response.code,
    body: response.body,
    headers: response.headers
  }
rescue StandardError => e
  configuration.logger.error "Conversant::V3 - HTTP GET error: #{e.message}"
  nil
end

#http_post(url, payload, extra_headers = {}) ⇒ Hash{Symbol => untyped}?

Performs an HTTP POST request with cookie management

Handles redirects automatically and updates the cookie jar from responses. Used internally during SSO login flow for form submissions.

Parameters:

  • url (String)

    URL to post to

  • payload (String)

    Request body

  • extra_headers (Hash{String => String}) (defaults to: {})

    Additional headers to merge

Returns:

  • (Hash{Symbol => untyped}, nil)

    Response hash with :status, :body, :headers keys, or nil on error

Since:

  • 1.0.0



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/conversant/v3/http_client.rb', line 299

def http_post(url, payload, extra_headers = {})
  headers = {
    'User-Agent' => configuration.default_ua,
    'Accept' => '*/*'
  }.merge(extra_headers)

  headers = add_cookies_to_headers(headers)

  response = RestClient::Request.execute(
    method: :post,
    url: url,
    payload: payload,
    headers: headers,
    verify_ssl: configuration.verify_ssl,
    max_redirects: 10
  ) do |resp, _request, _result|
    update_cookie_jar(resp)

    case resp.code
    when 200..299
      resp
    when 300..399
      location = resp.headers[:location]
      if location
        get_response = http_get(location)
        if get_response && get_response[:status] == 200
          OpenStruct.new(
            code: get_response[:status],
            body: get_response[:body],
            headers: get_response[:headers]
          )
        else
          resp.follow_redirection
        end
      else
        resp.follow_redirection
      end
    else
      resp
    end
  end

  update_cookie_jar(response)

  {
    status: response.code,
    body: response.body,
    headers: response.headers
  }
rescue StandardError => e
  configuration.logger.error "Conversant::V3 - HTTP POST error: #{e.message}"
  nil
end

#login_urlString

Returns the SSO login page URL (portal console root)

Resolved from configuration (ENV-driven) so the base domain is never hardcoded. Always normalized with a single trailing slash.

Returns:

Since:

  • 1.0.0



435
436
437
# File 'lib/conversant/v3/http_client.rb', line 435

def 
  "#{configuration.portal_endpoint.chomp('/')}/"
end

This method returns an undefined value.

Parses a Set-Cookie header string and stores it in the cookie jar

Parameters:

  • cookie_string (String)

    Set-Cookie header value

Since:

  • 1.0.0



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/conversant/v3/http_client.rb', line 393

def parse_cookie_to_jar(cookie_string)
  return unless cookie_string

  cookie_parts = cookie_string.split(';')
  return if cookie_parts.empty?

  first_part = cookie_parts.first
  parts = first_part.split('=', 2)
  return unless parts && parts.length == 2

  name, value = parts
  cookie_jar[name.strip] = value.strip

  debug_log "Parsed cookie: #{name.strip}" if configuration.debug_mode
end

#redisRedis

Returns the Redis client from configuration

Returns:

  • (Redis)

    Redis client instance

Since:

  • 1.0.0



449
450
451
# File 'lib/conversant/v3/http_client.rb', line 449

def redis
  configuration.redis
end

#request(method, url, payload, headers) ⇒ Array(Integer, RestClient::Response)

Makes an authenticated HTTP request

Handles JSON payload serialization and SSL verification based on configuration. All service classes use this method for making API calls.

Examples:

code, response = request(:get, 'https://api.example.com/data', nil, headers)
if code == 200
  data = JSON.parse(response.body)
end

Parameters:

  • method (Symbol)

    HTTP method (:get, :post, :put, :delete, etc.)

  • url (String)

    Full URL to request

  • payload (Hash, String, nil)

    Request payload (auto-converted to JSON if Content-Type matches)

  • headers (Hash{String => String})

    HTTP headers

Returns:

  • (Array(Integer, RestClient::Response))

    Status code and response object

Since:

  • 1.0.0



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/conversant/v3/http_client.rb', line 216

def request(method, url, payload, headers)
  debug_log "[Request] #{method} #{url}"
  debug_log "[Request] Headers: #{headers.inspect}" if configuration.debug_mode

  # Check Content-Type using string key (headers are now plain hash with string keys)
  payload = payload&.to_json if headers&.[]('Content-Type') == configuration.default_content_type

  request = RestClient::Request.new(
    method: method,
    url: url,
    payload: payload,
    headers: headers,
    verify_ssl: configuration.verify_ssl
  )

  response = request.execute do |response|
    response
  end

  debug_log "[Response] Status: #{response.code}" if configuration.debug_mode
  [response.code, response]
rescue StandardError => e
  configuration.logger.error "Conversant::V3 - Request exception: #{e.message}"
  [500, nil]
end

#sso_loginHash{Symbol => String}?

Performs SSO login to obtain root SESSION and SSO_GW_SESSION2 cookies

Executes a multi-step SSO authentication flow:

  1. Fetches the login page
  2. Extracts the form action URL
  3. Submits credentials
  4. Extracts and caches session cookies

Sessions are cached in Redis for reuse across all services and customers.

Examples:

sessions = 
if sessions
  puts "SESSION: #{sessions[:session]}"
  puts "SSO_GW_SESSION2: #{sessions[:sso_gw_session2]}"
end

Returns:

  • (Hash{Symbol => String}, nil)

    Hash with :session and :sso_gw_session2 keys, or nil on failure

Since:

  • 1.0.0



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/conversant/v3/http_client.rb', line 94

def 
  session_key = PORTAL_SESSION_REDIS_KEY
  sso_key = SSO_GW_SESSION2_REDIS_KEY

  # Check cache first
  cached_session = redis.get(session_key)
  cached_sso_gw = redis.get(sso_key)

  if cached_session && cached_sso_gw
    debug_log 'Using cached global SSO sessions'
    return {
      session: cached_session,
      sso_gw_session2: cached_sso_gw
    }
  end

  begin
    # Initialize thread-safe cookie jar
    self.cookie_jar = {}

    # Step 1: Get login page and extract form action
    debug_log "[SSO Login - Step 1] Fetching login page: #{}"
     = http_get()
    return nil unless 

    debug_log "Login page loaded, status: #{[:status]}"

    # Step 2: Parse login form to get action URL
    form_action = extract_form_action([:body])
    unless form_action
      debug_log 'Could not extract form action URL from login page'
      return nil
    end

    debug_log "[SSO Login - Step 2] Form action extracted: #{form_action}"

    # Step 3: Submit credentials
    form_data = URI.encode_www_form(
      username: configuration.swiftserve_identifier_id,
      password: configuration.swiftserve_identifier_hash,
      credentialId: ''
    )

    debug_log '[SSO Login - Step 3] Submitting login form...'
     = http_post(form_action, form_data, {
                                 'Content-Type' => 'application/x-www-form-urlencoded'
                               })

    return nil unless 

    debug_log "Login completed, status: #{[:status]}"

    # Step 4: Extract cookies
    session_cookie = cookie_jar['SESSION']
    sso_gw_session2_cookie = cookie_jar['SSO_GW_SESSION2']

    if session_cookie && sso_gw_session2_cookie
      # Cache the sessions globally
      redis.set(session_key, session_cookie, ex: configuration.cache_ttl)
      redis.set(sso_key, sso_gw_session2_cookie, ex: configuration.cache_ttl)

      configuration.logger.info 'Conversant::V3 - SSO login successful, sessions cached'

      return {
        session: session_cookie,
        sso_gw_session2: sso_gw_session2_cookie
      }
    end

    debug_log 'New SSO login failed - no valid sessions obtained'
    nil
  rescue StandardError => e
    configuration.logger.error "Conversant::V3 - SSO login error: #{e.message}"
    nil
  ensure
    self.cookie_jar = nil
  end
end

This method returns an undefined value.

Updates cookie jar from HTTP response

Extracts cookies from Set-Cookie headers and response.cookies hash.

Parameters:

  • response (RestClient::Response)

    HTTP response object

Since:

  • 1.0.0



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/conversant/v3/http_client.rb', line 370

def update_cookie_jar(response)
  return unless response&.headers

  set_cookie_headers = response.headers[:set_cookie]
  if set_cookie_headers
    if set_cookie_headers.is_a?(Array)
      set_cookie_headers.each { |cookie| parse_cookie_to_jar(cookie) }
    elsif set_cookie_headers.is_a?(String)
      parse_cookie_to_jar(set_cookie_headers)
    end
  end

  return unless response.respond_to?(:cookies) && response.cookies.is_a?(Hash)

  response.cookies.each do |name, value|
    cookie_jar[name.to_s] = value.to_s
  end
end