http_mimic 0.3.0

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.
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module HttpMimic
6
+ class ResponseParser
7
+ HTTP_STATUS_LINE_REGEX = /\AHTTP\/(?<version>[\d\.]+)\s+(?<code>\d{3})(?:\s+(?<message>.*))?/i
8
+
9
+ attr_reader :raw_output, :exit_status, :stderr, :command, :request_url
10
+
11
+ def initialize(raw_output, exit_status: nil, stderr: nil, command: nil, request_url: nil)
12
+ @raw_output = raw_output.to_s
13
+ @exit_status = exit_status
14
+ @stderr = stderr.to_s
15
+ @command = command
16
+ @request_url = request_url
17
+ end
18
+
19
+ def parse
20
+ # Split header blocks and body
21
+ header_blocks, body = split_headers_and_body(@raw_output)
22
+
23
+ final_header_block = header_blocks.last || ''
24
+ history_header_blocks = header_blocks.size > 1 ? header_blocks[0...-1] : []
25
+
26
+ # Parse final status line and headers
27
+ code, http_version, status_message, headers, cookies = parse_header_block(final_header_block)
28
+
29
+ # Parse redirect history
30
+ history = history_header_blocks.map do |block|
31
+ h_code, h_version, h_msg, h_headers, h_cookies = parse_header_block(block)
32
+ {
33
+ code: h_code,
34
+ http_version: h_version,
35
+ status_message: h_msg,
36
+ headers: h_headers,
37
+ cookies: h_cookies,
38
+ raw_headers: block
39
+ }
40
+ end
41
+
42
+ # Parse body (auto-detect JSON)
43
+ parsed_body = parse_body(body, headers)
44
+
45
+ Response.new(
46
+ code: code,
47
+ http_version: http_version,
48
+ status_message: status_message,
49
+ headers: headers,
50
+ cookies: cookies,
51
+ body: body,
52
+ parsed_response: parsed_body,
53
+ raw_headers: final_header_block,
54
+ history: history,
55
+ request_url: request_url,
56
+ stderr: stderr,
57
+ exit_code: exit_status ? exit_status.exitstatus : 0,
58
+ command: command
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ # Split raw_output into header blocks and body by \r?\n\r?\n
65
+ def split_headers_and_body(text)
66
+ return [[], ''] if text.nil? || text.empty?
67
+
68
+ # Normalize line endings
69
+ normalized = text.gsub(/\r\n/, "\n")
70
+ parts = normalized.split("\n\n")
71
+
72
+ header_blocks = []
73
+ body_index = 0
74
+
75
+ parts.each_with_index do |part, idx|
76
+ trimmed = part.strip
77
+ if trimmed =~ HTTP_STATUS_LINE_REGEX
78
+ header_blocks << part
79
+ body_index = idx + 1
80
+ else
81
+ # Once a non-HTTP status block is encountered, the rest is body
82
+ break
83
+ end
84
+ end
85
+
86
+ # Combine remaining parts as body
87
+ body = parts[body_index..-1] ? parts[body_index..-1].join("\n\n") : ''
88
+
89
+ [header_blocks, body]
90
+ end
91
+
92
+ def parse_header_block(block)
93
+ return [0, nil, nil, Headers.new, Cookies.new] if block.nil? || block.empty?
94
+
95
+ lines = block.split(/\r?\n/).map(&:strip).reject(&:empty?)
96
+ status_line = lines.first || ''
97
+ header_lines = lines[1..-1] || []
98
+
99
+ code = 0
100
+ http_version = nil
101
+ status_message = nil
102
+
103
+ if match = status_line.match(HTTP_STATUS_LINE_REGEX)
104
+ code = match[:code].to_i
105
+ http_version = match[:version]
106
+ status_message = match[:message] ? match[:message].strip : ''
107
+ end
108
+
109
+ headers = Headers.new
110
+ set_cookies = []
111
+
112
+ header_lines.each do |line|
113
+ key, val = line.split(':', 2)
114
+ next unless key && val
115
+
116
+ key = key.strip
117
+ val = val.strip
118
+
119
+ if key.downcase == 'set-cookie'
120
+ set_cookies << val
121
+ headers.add(key, val)
122
+ else
123
+ headers[key] = val
124
+ end
125
+ end
126
+
127
+ cookies = Cookies.parse_set_cookie(set_cookies)
128
+
129
+ [code, http_version, status_message, headers, cookies]
130
+ end
131
+
132
+ def parse_body(body, headers)
133
+ return nil if body.nil? || body.empty?
134
+
135
+ content_type = headers['content-type'].to_s.downcase
136
+
137
+ if content_type.include?('json') || body.strip.start_with?('{', '[')
138
+ begin
139
+ JSON.parse(body)
140
+ rescue JSON::ParserError
141
+ body
142
+ end
143
+ else
144
+ body
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HttpMimic
4
+ VERSION = "0.3.0"
5
+ end
data/lib/http_mimic.rb ADDED
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ require 'http_mimic/version'
8
+ require 'http_mimic/exceptions'
9
+ require 'http_mimic/configuration'
10
+ require 'http_mimic/downloader'
11
+ require 'http_mimic/headers'
12
+ require 'http_mimic/cookies'
13
+ require 'http_mimic/response'
14
+ require 'http_mimic/response_parser'
15
+ require 'http_mimic/command_builder'
16
+ require 'http_mimic/request'
17
+ require 'http_mimic/client'
18
+ require 'http_mimic/module_methods'
19
+
20
+ module HttpMimic
21
+ extend ModuleMethods
22
+
23
+ class << self
24
+ def configuration
25
+ @configuration ||= Configuration.new
26
+ end
27
+
28
+ def configure
29
+ yield(configuration) if block_given?
30
+ end
31
+
32
+ def reset_configuration!
33
+ @configuration = Configuration.new
34
+ end
35
+
36
+ # Manually download and install curl-impersonate binary driver
37
+ def download_driver!(version: nil, force: false)
38
+ Downloader.download!(version: version, force: force)
39
+ end
40
+
41
+ # Check if curl-impersonate driver is installed locally
42
+ def driver_installed?(version: nil)
43
+ Downloader.installed?(version: version)
44
+ end
45
+
46
+ def included(base)
47
+ base.extend(ModuleMethods)
48
+ base.send(:include, InstanceMethods)
49
+ end
50
+ end
51
+
52
+ module InstanceMethods
53
+ def get(url, options = {})
54
+ self.class.get(url, options)
55
+ end
56
+
57
+ def post(url, options = {})
58
+ self.class.post(url, options)
59
+ end
60
+
61
+ def put(url, options = {})
62
+ self.class.put(url, options)
63
+ end
64
+
65
+ def patch(url, options = {})
66
+ self.class.patch(url, options)
67
+ end
68
+
69
+ def delete(url, options = {})
70
+ self.class.delete(url, options)
71
+ end
72
+
73
+ def head(url, options = {})
74
+ self.class.head(url, options)
75
+ end
76
+
77
+ def options(url, options = {})
78
+ self.class.options(url, options)
79
+ end
80
+
81
+ def request(method, url, options = {})
82
+ self.class.request(method, url, options)
83
+ end
84
+ end
85
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: http_mimic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - anxgang
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.17'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.17'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: Simplifies making HTTP requests using curl-impersonate to mimic real
55
+ browser TLS/HTTP2 fingerprints.
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - CHANGELOG.md
61
+ - LICENSE.txt
62
+ - README.md
63
+ - http_mimic.gemspec
64
+ - lib/http_mimic.rb
65
+ - lib/http_mimic/client.rb
66
+ - lib/http_mimic/command_builder.rb
67
+ - lib/http_mimic/configuration.rb
68
+ - lib/http_mimic/cookies.rb
69
+ - lib/http_mimic/downloader.rb
70
+ - lib/http_mimic/exceptions.rb
71
+ - lib/http_mimic/headers.rb
72
+ - lib/http_mimic/module_methods.rb
73
+ - lib/http_mimic/request.rb
74
+ - lib/http_mimic/response.rb
75
+ - lib/http_mimic/response_parser.rb
76
+ - lib/http_mimic/version.rb
77
+ homepage: https://github.com/anxgang/http_mimic
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ homepage_uri: https://github.com/anxgang/http_mimic
82
+ source_code_uri: https://github.com/anxgang/http_mimic
83
+ bug_tracker_uri: https://github.com/anxgang/http_mimic/issues
84
+ changelog_uri: https://github.com/anxgang/http_mimic/blob/main/CHANGELOG.md
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 2.3.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 4.0.8
100
+ specification_version: 4
101
+ summary: A HTTParty-like Ruby client wrapping curl-impersonate via Open3.
102
+ test_files: []