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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +263 -0
- data/http_mimic.gemspec +31 -0
- data/lib/http_mimic/client.rb +72 -0
- data/lib/http_mimic/command_builder.rb +333 -0
- data/lib/http_mimic/configuration.rb +41 -0
- data/lib/http_mimic/cookies.rb +111 -0
- data/lib/http_mimic/downloader.rb +272 -0
- data/lib/http_mimic/exceptions.rb +34 -0
- data/lib/http_mimic/headers.rb +101 -0
- data/lib/http_mimic/module_methods.rb +101 -0
- data/lib/http_mimic/request.rb +88 -0
- data/lib/http_mimic/response.rb +99 -0
- data/lib/http_mimic/response_parser.rb +148 -0
- data/lib/http_mimic/version.rb +5 -0
- data/lib/http_mimic.rb +85 -0
- metadata +102 -0
|
@@ -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
|
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: []
|