aspera-cli 4.27.2 → 4.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/rest/parameters'
4
+ require 'aspera/mime'
5
+ require 'aspera/assert'
6
+ require 'aspera/log'
7
+
8
+ module Aspera
9
+ module Rest
10
+ # List and lookup methods for Rest::Client
11
+ # To be included in classes inheriting Rest::Client that require those methods.
12
+ module List
13
+ # `max`: special query parameter: max number of items for list command
14
+ MAX_ITEMS = 'max'
15
+ # `pmax`: special query parameter: max number of pages for list command
16
+ MAX_PAGES = 'pmax'
17
+
18
+ # Query entity by general search (read with parameter `q`)
19
+ #
20
+ # @param entity [String] Path of entity in API
21
+ # @param value [String] Value of field of searched entity
22
+ # @param field [String] Field of searched entity
23
+ # @param query [Hash] Optional additional search query parameters
24
+ # @return [Hash] A single entity matching the search, or an exception if not found or multiple found
25
+ def lookup_with_q(entity, value:, field: 'name', query: {})
26
+ Aspera.assert_type(query, Hash) { 'query' }
27
+ Aspera.assert_type(field, String) { 'field' }
28
+ # returns entities matching the query (it matches against several fields in case insensitive way)
29
+ # We don't do paging, as anyway, we look for only one match
30
+ matching_items = read(entity, query.merge({'q' => value}))
31
+ # API style: {totalcount:, ...} cspell: disable-line: TODO: is that total_count ?
32
+ # @type [Array<Hash{String => String}>]
33
+ matching_items = matching_items[entity] if matching_items.is_a?(Hash)
34
+ Aspera.assert_type(matching_items, Array)
35
+ case matching_items.length
36
+ when 1 then return matching_items.first
37
+ when 0 then raise EntityNotFound, %Q{No such #{entity}: "#{value}"}
38
+ else
39
+ # multiple case insensitive partial matches, try case insensitive full match
40
+ # (AoC does not allow 2 entities with same case insensitive name, except for some, e.g. packages)
41
+ value_matches = matching_items.select { |i| i[field].casecmp?(value) }
42
+ case value_matches.length
43
+ when 1 then return value_matches.first
44
+ when 0 then raise Error, "#{entity}: Multiple case insensitive partial match for: \"#{value}\" in #{matching_items.map { |i| i[field] }.join(', ')} but no case insensitive full match. Please be more specific or give exact #{field}."
45
+ else raise Error, "#{entity}: Multiple entities with #{field}: \"#{value}\". Please use the identifier instead."
46
+ end
47
+ end
48
+ end
49
+
50
+ # Get a (full or partial) list of all entities of a given type.
51
+ # Using query: `offset` + `limit`
52
+ # And response `total_count`
53
+ # @param entity [String,Symbol] API endpoint of entity to list
54
+ # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
55
+ # @param query [Hash,nil] Additional query parameters
56
+ # @return [Array<(Array<Hash>, Integer)>] items, total_count
57
+ def list_entities_limit_offset_total_count(
58
+ entity:,
59
+ operation: 'GET',
60
+ items_key: nil,
61
+ query: nil
62
+ )
63
+ entity = entity.to_s if entity.is_a?(Symbol)
64
+ items_key = entity.split('/').last if items_key.nil?
65
+ query = {} if query.nil?
66
+ Aspera.assert_type(entity, String)
67
+ Aspera.assert_type(items_key, String)
68
+ Aspera.assert_type(query, Hash)
69
+ Log.log.debug { "list_entities t=#{entity} k=#{items_key}" }
70
+ Log.dump(:query, query)
71
+ result = []
72
+ offset = 0
73
+ # Special parameters are not sent to the API (the caller's query is not modified)
74
+ query = query.dup
75
+ max_items = query.delete(MAX_ITEMS)&.to_i
76
+ remain_pages = query.delete(MAX_PAGES)&.to_i
77
+ # Merge default parameters
78
+ query = {'limit'=> PER_PAGE_DEFAULT}.merge(query)
79
+ total_count = nil
80
+ call_args = {
81
+ operation: operation,
82
+ subpath: entity,
83
+ query: query,
84
+ headers: {'Accept' => Mime::JSON}
85
+ }
86
+ loop do
87
+ query['offset'] = offset
88
+ page_result = call(**call_args)
89
+ page_items = page_result[items_key]
90
+ Aspera.assert_type(page_items, Array)
91
+ result.concat(page_items)
92
+ # Reach the limit set by user ?
93
+ if !max_items.nil? && (result.length >= max_items)
94
+ result = result.slice(0, max_items)
95
+ break
96
+ end
97
+ total_count ||= page_result['total_count']
98
+ # An empty page ends the list, even if the total count is not reached (e.g. items deleted meanwhile)
99
+ break if page_items.empty?
100
+ # Without total count, a partial page is the last one
101
+ break if total_count.nil? ? page_items.length < query['limit'].to_i : result.length >= total_count
102
+ remain_pages -= 1 unless remain_pages.nil?
103
+ break if !remain_pages.nil? && remain_pages <= 0
104
+ offset += page_items.length
105
+ Parameters.instance.spinner_cb.call("#{result.length} / #{total_count || '?'}")
106
+ end
107
+ Parameters.instance.spinner_cb.call(action: :success)
108
+ return result, total_count
109
+ end
110
+
111
+ # Lookup an entity id from its name.
112
+ # Uses query `q` if `query` is `:default` and `field` is `name`.
113
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
114
+ # @param value [String] Value to lookup
115
+ # @param field [String] Field to match, by default it is `'name'`
116
+ # @param items_key [String] Key in the result to get the list of items (override entity)
117
+ # @param query [Hash] Additional query parameters (Default: `:default`)
118
+ def lookup_entity_by_field(entity:, value:, field: 'name', items_key: nil, query: :default)
119
+ if query.eql?(:default)
120
+ Aspera.assert_values(field, ['name']) { 'Default query field' }
121
+ query = {'q'=> value}
122
+ end
123
+ lookup_entity_generic(entity: entity, field: field, value: value) { list_entities_limit_offset_total_count(entity: entity, items_key: items_key, query: query).first }
124
+ end
125
+
126
+ # Lookup entity by field and value.
127
+ # Extracts a single result from the list returned by the block.
128
+ #
129
+ # @param entity [String] Type of entity to lookup (path, and by default it is also the field name in result)
130
+ # @param value [String] Value to match against the field.
131
+ # @param field [String] Field to match in the hashes (defaults to 'name').
132
+ # @yield [] A mandatory block that returns an Array of Hashes.
133
+ # @return [Hash] The unique matching object.
134
+ # @raise [Cli::BadIdentifier] If 0 or >1 matches are found.
135
+ def lookup_entity_generic(entity:, value:, field: 'name')
136
+ Aspera.assert(block_given?, 'block required for lookup_entity_generic')
137
+ found = yield
138
+ Aspera.assert_array_all(found, Hash)
139
+ found = found.select { |i| i[field].eql?(value) }
140
+ return found.first if found.length.eql?(1)
141
+ raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
142
+ end
143
+
144
+ PER_PAGE_DEFAULT = 1000
145
+ private_constant :PER_PAGE_DEFAULT
146
+ module_function :lookup_entity_generic
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Aspera
6
+ module Rest
7
+ # Global settings for Rest::Client
8
+ # For example to remove certificate verification globally:
9
+ # `Parameters.instance.session_cb = lambda{|http|http.verify_mode=OpenSSL::SSL::VERIFY_NONE}`
10
+ #
11
+ # @!method self.instance
12
+ # Returns the singleton instance of Parameters
13
+ # @return [Parameters] the singleton instance
14
+ class Parameters
15
+ include Singleton
16
+
17
+ # @return [String] HTTP request header: `User-Agent`
18
+ attr_accessor :user_agent
19
+ # @return [String] Suffix of file being downloaded, removed when download is complete
20
+ attr_accessor :download_partial_suffix
21
+ # @return [Boolean] Retry on any error (network or HTTP)
22
+ attr_accessor :retry_on_error
23
+ # @return [Boolean] Retry on connection timeout
24
+ attr_accessor :retry_on_timeout
25
+ # @return [Boolean] Retry on HTTP code 503 (service unavailable)
26
+ attr_accessor :retry_on_unavailable
27
+ # @return [Integer] Maximum number of retries on error (first call not included)
28
+ attr_accessor :retry_max
29
+ # @return [Integer] Seconds to wait before retry
30
+ attr_accessor :retry_sleep
31
+ # @return [Proc, nil] Called on new HTTP session, with the `Net::HTTP` as argument, e.g. to set timeouts or certificate verification
32
+ attr_accessor :session_cb
33
+ # @return [Object, nil] Progress bar, receives `event` calls during download
34
+ attr_accessor :progress_bar
35
+ # @return [Proc] Called with `(title = nil, action: :spin)` to display progress of long operations (default: no display)
36
+ attr_accessor :spinner_cb
37
+
38
+ private
39
+
40
+ # Set default values
41
+ def initialize
42
+ @user_agent = 'RubyAsperaRest'
43
+ @download_partial_suffix = '.http_partial'
44
+ @retry_on_error = false
45
+ @retry_on_timeout = true
46
+ @retry_on_unavailable = true
47
+ @retry_max = 1
48
+ @retry_sleep = 4
49
+ @session_cb = nil
50
+ @progress_bar = nil
51
+ @spinner_cb = ->(_title = nil, action: :spin) {}
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/rest/parameters'
4
+ require 'aspera/log'
5
+ require 'aspera/assert'
6
+ require 'net/http'
7
+ require 'net/https'
8
+ require 'uri'
9
+ require 'base64'
10
+
11
+ module Aspera
12
+ # HTTP REST client and helpers
13
+ module Rest
14
+ class << self
15
+ # Build a Basic authentication header value
16
+ # @param user [String] Username
17
+ # @param pass [String] Password
18
+ # @return [String] Basic auth token
19
+ def basic_authorization(user, pass) = "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"
20
+
21
+ # Indicate that the given Hash query uses php style for array parameters
22
+ # @param query [Hash] A key can have Array value and result will use PHP format: a[]=1&a[]=2
23
+ # @return [Hash] The query parameters.
24
+ def php_style(query)
25
+ Aspera.assert_type(query, Hash) { 'query' }
26
+ query[:x_array_php_style] = true
27
+ query
28
+ end
29
+
30
+ # Build URI from URL and parameters and check it is `http` or `https`.
31
+ # Check if php style is specified.
32
+ # `nil` values in query result in key without value, e.g. `?a`, while empty string values result in `?a=`.
33
+ # @param url [String] The URL without query.
34
+ # @param query [Hash,Array,String] The query parameters.
35
+ # @return [URI] The built URI.
36
+ def build_uri(url, query)
37
+ uri = URI.parse(url)
38
+ Aspera.assert_values(uri.scheme, %w[http https]) { 'URI scheme' }
39
+ return uri if query.nil? || query.respond_to?(:empty?) && query.empty?
40
+ Log.dump(:query, query)
41
+ uri.query =
42
+ case query
43
+ when String
44
+ query
45
+ when Hash
46
+ URI.encode_www_form(h_to_query_array(query))
47
+ when Array
48
+ Aspera.assert(query.all? { |i| i.is_a?(Array) && i.length.eql?(2) }, 'Query must be array of arrays of 2 elements')
49
+ URI.encode_www_form(query) # remove nil values
50
+ else Aspera.error_unexpected_value(query.class) { 'query type' }
51
+ end.gsub('%5B%5D=', '[]=')
52
+ # [] is allowed in url parameters
53
+ uri
54
+ end
55
+
56
+ # Support array for query parameter, there is no standard.
57
+ # Either p=1&p=2 (default)
58
+ # or p[]=1&p[]=2 (if `:x_array_php_style` is set to true in query)
59
+ # @param query [Hash] HTTP query as hash
60
+ # @return [Array<Array>] Array of [key, value] pairs suitable for URI.encode_www_form
61
+ def h_to_query_array(query)
62
+ Aspera.assert_type(query, Hash)
63
+ suffix = query[:x_array_php_style] ? '[]' : nil
64
+ query.each_with_object([]) do |(k, v), query_array|
65
+ next if k.eql?(:x_array_php_style)
66
+ case v
67
+ when Array
68
+ v.each do |e|
69
+ query_array.push(["#{k}#{suffix}", e])
70
+ end
71
+ else
72
+ query_array.push([k, v])
73
+ end
74
+ end
75
+ end
76
+
77
+ # Decode query string as Hash
78
+ # if parameter is only once, then it's a scalar
79
+ # if a parameter is several, then it's array
80
+ # if parameter has [] then it's an array, and [] is removed
81
+ # Support arrays in query string, e.g. PHP's way is p[]=1&p[]=2
82
+ # @param query [String] query string as in URI.query
83
+ # @return [Hash] decoded query
84
+ def query_to_h(query)
85
+ URI.decode_www_form(query).each_with_object({}) do |(key, value), h|
86
+ if key.end_with?('[]')
87
+ key = key[..-3]
88
+ h[key] = [] unless h.key?(key)
89
+ end
90
+ if h.key?(key)
91
+ h[key] = [h[key]] if !h[key].is_a?(Array)
92
+ h[key].push(value)
93
+ else
94
+ h[key] = value
95
+ end
96
+ end
97
+ end
98
+
99
+ # Start a HTTP/S session, also used for web sockets
100
+ # @param base_url [String] Base url of HTTP/S session
101
+ # @return [Net::HTTP] A started HTTP session
102
+ def start_http_session(base_url)
103
+ uri = URI.parse(base_url)
104
+ Aspera.assert_values(uri.scheme, %w[http https]) { 'URI scheme' }
105
+ # This honors http_proxy env var
106
+ http_session = Net::HTTP.new(uri.host, uri.port)
107
+ http_session.use_ssl = uri.scheme.eql?('https')
108
+ # Set http options in callback, such as timeout and cert. verification
109
+ Parameters.instance.session_cb&.call(http_session)
110
+ # Manually start session for keep alive (if supported by server, else, session is closed every time)
111
+ http_session.start
112
+ return http_session
113
+ end
114
+
115
+ # get Net::HTTP underlying socket i/o
116
+ # little hack, handy because HTTP debug, proxy, etc... will be available
117
+ # used implement web sockets after `start_http_session`
118
+ # @param http_session [Net::HTTP] the session object
119
+ # @return [Net::BufferedIO] The underlying socket i/o
120
+ def io_http_session(http_session)
121
+ Aspera.assert_type(http_session, Net::HTTP)
122
+ # Net::BufferedIO in net/protocol.rb
123
+ result = http_session.instance_variable_get(:@socket)
124
+ Aspera.assert(!result.nil?) { "no socket for #{http_session}" }
125
+ return result
126
+ end
127
+
128
+ # Get certificate chain of remote server
129
+ # @param url [String] URL of server
130
+ # @param as_string [Boolean] `true` to return PEM string, `false` for certificate objects
131
+ # @return [String, Array<OpenSSL::X509::Certificate>] Certificates of remote server
132
+ def remote_certificate_chain(url, as_string: true)
133
+ result = []
134
+ # initiate a session to retrieve remote certificate
135
+ http_session = Rest.start_http_session(url)
136
+ begin
137
+ # retrieve underlying openssl socket
138
+ result = Rest.io_http_session(http_session).io.peer_cert_chain
139
+ rescue
140
+ result = http_session.peer_cert
141
+ ensure
142
+ http_session.finish
143
+ end
144
+ result = result.map(&:to_pem).join("\n") if as_string
145
+ return result
146
+ end
147
+
148
+ # Parses an HTTP Content-Type header string into its media type and parameters
149
+ # according to RFC 9110 and RFC 6838.
150
+ # TODO: use gem: content_type
151
+ #
152
+ # @param header [String] The Content-Type header string, e.g., "application/json; charset=utf-8"
153
+ # @return [Hash] A hash with :type and :parameters keys.
154
+ # Example:
155
+ # {
156
+ # type: "application/json",
157
+ # parameters: {
158
+ # charset: "utf-8",
159
+ # version: "1.0"
160
+ # }
161
+ # }
162
+ def parse_header(header)
163
+ parts = header.split(';').map(&:strip)
164
+ media_type = parts.shift.downcase
165
+ parameters = parts.filter_map do |param|
166
+ key, value = param.split('=', 2)
167
+ next unless key && value
168
+ key = key.strip.downcase.to_sym
169
+ value = value.strip.gsub(/\A"|"\z/, '')
170
+ [key, value]
171
+ end.to_h
172
+ {type: media_type, parameters: parameters}
173
+ end
174
+ end
175
+ end
176
+ end