mechanize 2.13.0 → 2.14.1
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 +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/upstream.yml +2 -2
- data/CHANGELOG.md +20 -0
- data/Gemfile +2 -2
- data/README.md +7 -0
- data/Rakefile +1 -1
- data/SECURITY.md +11 -0
- data/lib/mechanize/http/agent.rb +90 -29
- data/lib/mechanize/version.rb +1 -1
- data/lib/mechanize.rb +27 -2
- data/test/htdocs/frame_test.html +8 -6
- data/test/htdocs/relative/tc_relative_links.html +1 -1
- data/test/test_mechanize.rb +9 -1
- data/test/test_mechanize_http_agent.rb +235 -5
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01e9dae7534053b14ad6ab963f43aaf4fd6475bae450ddbe78d2dc690ab0d541
|
|
4
|
+
data.tar.gz: ab9674181ce7e7a023b1e58f72c30cb84898358aa09294612b4aeee26cc5057e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc6571d503c3c2cbf2e22a6c9af5b75b3479bceaf008e3480e55d7a6ebaf9d5673c3cb61ec64fc29b6081e802afa6b2f5ec321d6247119bda822825a32f4248b
|
|
7
|
+
data.tar.gz: d2188a092148a8ea10865d1cf8e07a8f8a08c6c30760e191efa3a6af3a756821e95e525c116396000c063ba0ec670325900af4b5cb099c222cbfc6d71c5b26b3
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
- uses: actions/checkout@v4
|
|
24
24
|
- uses: ruby/setup-ruby@v1
|
|
25
25
|
with:
|
|
26
|
-
ruby-version: "
|
|
26
|
+
ruby-version: "4.0"
|
|
27
27
|
bundler-cache: true
|
|
28
28
|
- run: bundle exec rake rubocop
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
strategy:
|
|
33
33
|
fail-fast: false
|
|
34
34
|
matrix:
|
|
35
|
-
ruby-version: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "
|
|
35
|
+
ruby-version: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "4.0"]
|
|
36
36
|
runs-on: ubuntu-latest
|
|
37
37
|
steps:
|
|
38
38
|
- uses: actions/checkout@v4
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
strategy:
|
|
24
24
|
fail-fast: false
|
|
25
25
|
matrix:
|
|
26
|
-
ruby-version: ["head"]
|
|
26
|
+
ruby-version: ["head", "truffleruby-head"]
|
|
27
27
|
runs-on: ubuntu-latest
|
|
28
28
|
steps:
|
|
29
29
|
- uses: actions/checkout@v4
|
|
@@ -44,7 +44,7 @@ jobs:
|
|
|
44
44
|
steps:
|
|
45
45
|
- uses: actions/checkout@v4
|
|
46
46
|
- uses: ruby/setup-ruby@v1
|
|
47
|
-
with: { ruby-version: "
|
|
47
|
+
with: { ruby-version: "4.0" }
|
|
48
48
|
- run: |
|
|
49
49
|
bundle add ${{matrix.name}} --git="${{matrix.git}}"
|
|
50
50
|
bundle show
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Mechanize CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.14.1 / 2026-08-22
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
* Strip both `Proxy-Authorization` and `Cookie2` headers on a cross-origin redirect. [GHSA-2mwr-xjcg-37j7](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-2mwr-xjcg-37j7)
|
|
8
|
+
* Headers set through `Mechanize#request_headers=` now obey the same cross-origin redirect rules as per-request headers, instead of being re-applied unconditionally to every request. [GHSA-2mwr-xjcg-37j7](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-2mwr-xjcg-37j7)
|
|
9
|
+
* A redirect is now considered cross-origin when the scheme, the host or the port changes, per [RFC 6454](https://datatracker.ietf.org/doc/html/rfc6454). Previously only a host change discarded cookies, and an `https:` to `http:` redirect to the same host carried credentials in cleartext. [GHSA-5jgv-wc2m-xv99](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-5jgv-wc2m-xv99)
|
|
10
|
+
* A `meta` refresh that points cross-origin now discards sensitive request headers, the same as an HTTP redirect. Previously headers set through `Mechanize#request_headers=` followed it unconditionally. [GHSA-c6rp-p8xm-4q9f](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-c6rp-p8xm-4q9f)
|
|
11
|
+
|
|
12
|
+
### Improved
|
|
13
|
+
|
|
14
|
+
* A `meta` refresh now carries the original request's headers, minus any credential withheld for crossing an origin and minus the entity headers of the original request. Previously it carried only the headers set through `Mechanize#request_headers=`.
|
|
15
|
+
* `robots.txt` is fetched without any caller-supplied request headers, on any origin. Previously it received the headers set through `Mechanize#request_headers=`. It is a request mechanize makes on its own behalf.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## 2.14.0 / 2025-01-05
|
|
19
|
+
|
|
20
|
+
* `Mechanize` exposes a `write_timeout` attribute, which is set on the connection if it's supported (e.g., `Net::HTTP::Persistent.write_timeout`). (#586) @maurycy
|
|
21
|
+
|
|
22
|
+
|
|
3
23
|
## 2.13.0 / 2025-01-02
|
|
4
24
|
|
|
5
25
|
* Quash frozen string warnings in Ruby 3.4. (#661) @simpl1g
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -56,6 +56,13 @@ bundle exec rake test
|
|
|
56
56
|
See also Mechanize::TestCase to read about the built-in testing infrastructure.
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
### Security and Vulnerability Reporting
|
|
60
|
+
|
|
61
|
+
Please report vulnerabilities at https://github.com/sparklemotion/mechanize/security
|
|
62
|
+
|
|
63
|
+
See [SECURITY.md](SECURITY.md) for full information and description of our security policy.
|
|
64
|
+
|
|
65
|
+
|
|
59
66
|
## Authors
|
|
60
67
|
|
|
61
68
|
* Eric Hodel
|
data/Rakefile
CHANGED
data/SECURITY.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Security and Vulnerability Reporting
|
|
2
|
+
|
|
3
|
+
The Mechanize Core Contributors take security very seriously and investigate all reported vulnerabilities.
|
|
4
|
+
|
|
5
|
+
If you would like to report a vulnerability or have a security concern regarding Mechanize or how Mechanize is using any of its underlying platform-specific libraries, please [report it via Github](https://github.com/sparklemotion/mechanize/security).
|
|
6
|
+
|
|
7
|
+
Your report should be acknowledged within a week, and you'll receive a more detailed response within two weeks indicating next steps in handling your report.
|
|
8
|
+
|
|
9
|
+
If you have not received a reply to your submission within a week, contact the current security coordinator (Mike Dalessio <mike.dalessio@gmail.com>).
|
|
10
|
+
|
|
11
|
+
If a vulnerability is first reported by you, we will credit you with the discovery in the public disclosure.
|
data/lib/mechanize/http/agent.rb
CHANGED
|
@@ -9,10 +9,14 @@ require 'webrobots'
|
|
|
9
9
|
|
|
10
10
|
class Mechanize::HTTP::Agent
|
|
11
11
|
|
|
12
|
-
CREDENTIAL_HEADERS = ['Authorization']
|
|
13
|
-
COOKIE_HEADERS = ['Cookie']
|
|
12
|
+
CREDENTIAL_HEADERS = ['Authorization', 'Proxy-Authorization']
|
|
13
|
+
COOKIE_HEADERS = ['Cookie', 'Cookie2']
|
|
14
14
|
POST_HEADERS = ['Content-Length', 'Content-MD5', 'Content-Type']
|
|
15
15
|
|
|
16
|
+
# Symbols accepted as header names in the per-request headers hash, and the
|
|
17
|
+
# header each one names.
|
|
18
|
+
SYMBOL_HEADERS = { :etag => 'ETag', :if_modified_since => 'If-Modified-Since' }.freeze
|
|
19
|
+
|
|
16
20
|
# :section: Headers
|
|
17
21
|
|
|
18
22
|
# Disables If-Modified-Since conditional requests (enabled by default)
|
|
@@ -21,7 +25,9 @@ class Mechanize::HTTP::Agent
|
|
|
21
25
|
# Is gzip compression of requests enabled?
|
|
22
26
|
attr_accessor :gzip_enabled
|
|
23
27
|
|
|
24
|
-
# A hash of request headers to be used for every request
|
|
28
|
+
# A hash of request headers to be used for every request. Headers named in
|
|
29
|
+
# CREDENTIAL_HEADERS or COOKIE_HEADERS are withheld from a request that
|
|
30
|
+
# follows a redirect across an origin; see #crosses_origin?.
|
|
25
31
|
attr_accessor :request_headers
|
|
26
32
|
|
|
27
33
|
# The User-Agent header to send
|
|
@@ -106,6 +112,9 @@ class Mechanize::HTTP::Agent
|
|
|
106
112
|
# Length of time to attempt to read data from the server
|
|
107
113
|
attr_accessor :read_timeout
|
|
108
114
|
|
|
115
|
+
# Length of time to attempt to write data to the server
|
|
116
|
+
attr_accessor :write_timeout
|
|
117
|
+
|
|
109
118
|
# :section:
|
|
110
119
|
|
|
111
120
|
# The cookies for this agent
|
|
@@ -161,6 +170,7 @@ class Mechanize::HTTP::Agent
|
|
|
161
170
|
@robots_mutex = Mutex.new
|
|
162
171
|
@user_agent = nil
|
|
163
172
|
@webrobots = nil
|
|
173
|
+
@write_timeout = nil
|
|
164
174
|
|
|
165
175
|
# HTTP Authentication
|
|
166
176
|
@auth_store = Mechanize::HTTP::AuthStore.new
|
|
@@ -236,9 +246,20 @@ class Mechanize::HTTP::Agent
|
|
|
236
246
|
#
|
|
237
247
|
# +redirects+ tracks the number of redirects experienced when retrieving the
|
|
238
248
|
# page. If it is over the redirection_limit an error will be raised.
|
|
249
|
+
#
|
|
250
|
+
# +apply_request_headers+ merges #request_headers into +headers+. It is true
|
|
251
|
+
# for a request the caller initiated and false for one continuing an
|
|
252
|
+
# operation already under way, such as following a redirect, where +headers+
|
|
253
|
+
# already carries the merged set minus anything withheld along the way.
|
|
239
254
|
|
|
240
255
|
def fetch uri, method = :get, headers = {}, params = [],
|
|
241
|
-
referer = current_page, redirects = 0
|
|
256
|
+
referer = current_page, redirects = 0, apply_request_headers = true
|
|
257
|
+
|
|
258
|
+
# Fold the agent defaults into the caller's headers once, at the start of
|
|
259
|
+
# the operation, so that from here on a single hash carries every header
|
|
260
|
+
# and #response_redirect can drop a credential from it for good. Requests
|
|
261
|
+
# continuing an operation pass false and supply the hash they were given.
|
|
262
|
+
headers = merge_request_headers headers if apply_request_headers
|
|
242
263
|
|
|
243
264
|
referer_uri = referer ? referer.uri : nil
|
|
244
265
|
uri = resolve uri, referer
|
|
@@ -274,6 +295,9 @@ class Mechanize::HTTP::Agent
|
|
|
274
295
|
if @read_timeout && connection.respond_to?(:read_timeout=)
|
|
275
296
|
connection.read_timeout = @read_timeout
|
|
276
297
|
end
|
|
298
|
+
if @write_timeout && connection.respond_to?(:write_timeout=)
|
|
299
|
+
connection.write_timeout = @write_timeout
|
|
300
|
+
end
|
|
277
301
|
|
|
278
302
|
request_log request
|
|
279
303
|
|
|
@@ -306,7 +330,7 @@ class Mechanize::HTTP::Agent
|
|
|
306
330
|
|
|
307
331
|
response_cookies response, uri, page
|
|
308
332
|
|
|
309
|
-
meta = response_follow_meta_refresh response, uri, page, redirects
|
|
333
|
+
meta = response_follow_meta_refresh response, uri, page, redirects, headers
|
|
310
334
|
return meta if meta
|
|
311
335
|
|
|
312
336
|
if robots && page.is_a?(Mechanize::Page)
|
|
@@ -578,23 +602,58 @@ class Mechanize::HTTP::Agent
|
|
|
578
602
|
end
|
|
579
603
|
end
|
|
580
604
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
605
|
+
# The agent defaults, with +headers+ applied on top. A header in +headers+
|
|
606
|
+
# replaces an agent default naming the same header, whatever the spelling,
|
|
607
|
+
# because header names are case-insensitive and the agent's keys may be
|
|
608
|
+
# symbols.
|
|
609
|
+
def merge_request_headers headers
|
|
610
|
+
merged = {}
|
|
611
|
+
|
|
612
|
+
@request_headers.each { |field, value| merged[field.to_s] = value }
|
|
613
|
+
|
|
614
|
+
headers.each do |field, value|
|
|
615
|
+
name = canonical_header_name field
|
|
616
|
+
merged.delete_if { |existing, _| canonical_header_name(existing) == name }
|
|
617
|
+
merged[field] = value
|
|
584
618
|
end
|
|
585
619
|
|
|
620
|
+
merged
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def canonical_header_name field
|
|
624
|
+
(SYMBOL_HEADERS[field] || field).to_s.downcase
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def request_add_headers request, headers = {}
|
|
586
628
|
headers.each do |field, value|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
raise ArgumentError, "unknown header symbol #{field}"
|
|
629
|
+
if Symbol === field then
|
|
630
|
+
name = SYMBOL_HEADERS[field] or
|
|
631
|
+
raise ArgumentError, "unknown header symbol #{field}"
|
|
632
|
+
request[name] = value
|
|
592
633
|
else
|
|
593
634
|
request[field] = value
|
|
594
635
|
end
|
|
595
636
|
end
|
|
596
637
|
end
|
|
597
638
|
|
|
639
|
+
# Does moving from +from_uri+ to +to_uri+ cross an origin? An origin is the
|
|
640
|
+
# scheme, host and port together; see RFC 6454.
|
|
641
|
+
def crosses_origin? from_uri, to_uri
|
|
642
|
+
to_uri.scheme != from_uri.scheme ||
|
|
643
|
+
to_uri.host != from_uri.host ||
|
|
644
|
+
to_uri.port != from_uri.port
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Should the header named +name+ be withheld from a request issued after a
|
|
648
|
+
# redirect that crossed an origin?
|
|
649
|
+
def drop_after_redirect? name, crossed_origin
|
|
650
|
+
crossed_origin && match_header?(name, CREDENTIAL_HEADERS + COOKIE_HEADERS)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def match_header? name, candidates
|
|
654
|
+
candidates.any? { |candidate| name.to_s.casecmp?(candidate) }
|
|
655
|
+
end
|
|
656
|
+
|
|
598
657
|
def request_auth request, uri
|
|
599
658
|
base_uri = uri + '/'
|
|
600
659
|
base_uri.user &&= nil
|
|
@@ -867,7 +926,7 @@ class Mechanize::HTTP::Agent
|
|
|
867
926
|
raise Mechanize::UnauthorizedError.new(page, challenges, message)
|
|
868
927
|
end
|
|
869
928
|
|
|
870
|
-
fetch uri, request.method.downcase.to_sym, headers, params, referer
|
|
929
|
+
fetch uri, request.method.downcase.to_sym, headers, params, referer, 0, false
|
|
871
930
|
end
|
|
872
931
|
|
|
873
932
|
def response_content_encoding response, body_io
|
|
@@ -944,7 +1003,7 @@ class Mechanize::HTTP::Agent
|
|
|
944
1003
|
end
|
|
945
1004
|
end
|
|
946
1005
|
|
|
947
|
-
def response_follow_meta_refresh response, uri, page, redirects
|
|
1006
|
+
def response_follow_meta_refresh response, uri, page, redirects, headers = {}
|
|
948
1007
|
delay, new_url = get_meta_refresh(response, uri, page)
|
|
949
1008
|
return nil unless delay
|
|
950
1009
|
new_url = new_url ? secure_resolve!(new_url, page) : uri
|
|
@@ -954,8 +1013,16 @@ class Mechanize::HTTP::Agent
|
|
|
954
1013
|
|
|
955
1014
|
sleep delay
|
|
956
1015
|
@history.push(page, page.uri)
|
|
957
|
-
|
|
958
|
-
|
|
1016
|
+
|
|
1017
|
+
headers = headers.dup
|
|
1018
|
+
|
|
1019
|
+
# The refresh is fetched with GET, so the original request's entity headers
|
|
1020
|
+
# must not describe it.
|
|
1021
|
+
POST_HEADERS.each { |key| headers.delete_if { |h, _| h.to_s.casecmp?(key) } }
|
|
1022
|
+
headers.delete_if { |h, _| drop_after_redirect? h, crosses_origin?(uri, new_url) }
|
|
1023
|
+
|
|
1024
|
+
fetch new_url, :get, headers, [],
|
|
1025
|
+
Mechanize::Page.new, redirects + 1, false
|
|
959
1026
|
end
|
|
960
1027
|
|
|
961
1028
|
def response_log response
|
|
@@ -1058,18 +1125,12 @@ class Mechanize::HTTP::Agent
|
|
|
1058
1125
|
headers.delete_if { |h| h.casecmp?(key) }
|
|
1059
1126
|
end
|
|
1060
1127
|
|
|
1061
|
-
#
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
# cookies are OK to be shared across ports on the same host
|
|
1066
|
-
CREDENTIAL_HEADERS.each { |ch| headers.delete_if { |h| h.casecmp?(ch) } }
|
|
1067
|
-
end
|
|
1068
|
-
else
|
|
1069
|
-
(COOKIE_HEADERS + CREDENTIAL_HEADERS).each { |ch| headers.delete_if { |h| h.casecmp?(ch) } }
|
|
1070
|
-
end
|
|
1128
|
+
# Sensitive headers must not follow a redirect across an origin.
|
|
1129
|
+
crossed_origin = crosses_origin? page.uri, new_uri
|
|
1130
|
+
|
|
1131
|
+
headers.delete_if { |h, _| drop_after_redirect? h, crossed_origin }
|
|
1071
1132
|
|
|
1072
|
-
fetch new_uri, redirect_method, headers, [], referer, redirects + 1
|
|
1133
|
+
fetch new_uri, redirect_method, headers, [], referer, redirects + 1, false
|
|
1073
1134
|
end
|
|
1074
1135
|
|
|
1075
1136
|
# :section: Robots
|
|
@@ -1080,7 +1141,7 @@ class Mechanize::HTTP::Agent
|
|
|
1080
1141
|
robots_mutex.synchronize do
|
|
1081
1142
|
Thread.current[RobotsKey] = true
|
|
1082
1143
|
begin
|
|
1083
|
-
fetch(uri).body
|
|
1144
|
+
fetch(uri, :get, {}, [], current_page, 0, false).body
|
|
1084
1145
|
rescue Mechanize::ResponseCodeError => e
|
|
1085
1146
|
case e.response_code
|
|
1086
1147
|
when /\A4\d\d\z/
|
data/lib/mechanize/version.rb
CHANGED
data/lib/mechanize.rb
CHANGED
|
@@ -951,6 +951,21 @@ Use of #auth and #basic_auth are deprecated due to a security vulnerability.
|
|
|
951
951
|
@agent.read_timeout = read_timeout
|
|
952
952
|
end
|
|
953
953
|
|
|
954
|
+
##
|
|
955
|
+
# Length of time to wait for data to be sent to the server
|
|
956
|
+
|
|
957
|
+
def write_timeout
|
|
958
|
+
@agent.write_timeout
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
##
|
|
962
|
+
# Sets the timeout for each chunk of data to be sent to the server to
|
|
963
|
+
# +write_timeout+. A single request may write many chunks of data.
|
|
964
|
+
|
|
965
|
+
def write_timeout= write_timeout
|
|
966
|
+
@agent.write_timeout = write_timeout
|
|
967
|
+
end
|
|
968
|
+
|
|
954
969
|
##
|
|
955
970
|
# Controls how mechanize deals with redirects. The following values are
|
|
956
971
|
# allowed:
|
|
@@ -996,7 +1011,16 @@ Use of #auth and #basic_auth are deprecated due to a security vulnerability.
|
|
|
996
1011
|
end
|
|
997
1012
|
|
|
998
1013
|
##
|
|
999
|
-
# A hash of custom request headers that will be sent on every request
|
|
1014
|
+
# A hash of custom request headers that will be sent on every request.
|
|
1015
|
+
#
|
|
1016
|
+
# Headers named in Mechanize::HTTP::Agent::CREDENTIAL_HEADERS or
|
|
1017
|
+
# COOKIE_HEADERS are withheld from a request that follows a redirect to
|
|
1018
|
+
# another origin, where an origin is the scheme, host and port together, and
|
|
1019
|
+
# stay withheld for the rest of that operation. A meta refresh to another
|
|
1020
|
+
# origin is treated the same way.
|
|
1021
|
+
#
|
|
1022
|
+
# The robots.txt lookup carries none of these headers, on any origin. It is
|
|
1023
|
+
# a request mechanize makes on its own behalf.
|
|
1000
1024
|
|
|
1001
1025
|
def request_headers
|
|
1002
1026
|
@agent.request_headers
|
|
@@ -1004,7 +1028,8 @@ Use of #auth and #basic_auth are deprecated due to a security vulnerability.
|
|
|
1004
1028
|
|
|
1005
1029
|
##
|
|
1006
1030
|
# Replaces the custom request headers that will be sent on every request
|
|
1007
|
-
# with +request_headers
|
|
1031
|
+
# with +request_headers+. See #request_headers for the credentials
|
|
1032
|
+
# withheld after a redirect to another origin.
|
|
1008
1033
|
|
|
1009
1034
|
def request_headers= request_headers
|
|
1010
1035
|
@agent.request_headers = request_headers
|
data/test/htdocs/frame_test.html
CHANGED
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
</IFRAME>
|
|
19
19
|
</FRAMESET>
|
|
20
20
|
<NOFRAMES>
|
|
21
|
-
|
|
22
|
-
<UL>
|
|
23
|
-
<LI><A href="/google.html">Some neat contents</A>
|
|
24
|
-
<LI><A href="/form_test.html" class="bar">Form Test</A>
|
|
25
|
-
<LI><A href="/file_upload.html">Some other neat contents</A>
|
|
26
|
-
</UL>
|
|
21
|
+
Your user agent does not support frames.
|
|
27
22
|
</NOFRAMES>
|
|
23
|
+
<P>This frameset document contains:
|
|
24
|
+
<UL>
|
|
25
|
+
<LI><A href="/google.html">Some neat contents</A>
|
|
26
|
+
<LI><A href="/form_test.html" class="bar">Form Test</A>
|
|
27
|
+
<LI><A href="/file_upload.html">Some other neat contents</A>
|
|
28
|
+
</UL>
|
|
29
|
+
</P>
|
|
28
30
|
</FRAMESET>
|
|
29
31
|
</HTML>
|
|
30
32
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<body>
|
|
3
3
|
<a href="../tc_relative_links.html">dot dot slash</a>
|
|
4
4
|
<a href="../../../../../tc_relative_links.html">too many dots</a>
|
|
5
|
+
<a href="?a=b">just the query string</A>
|
|
5
6
|
<FRAMESET cols="20%, 80%">
|
|
6
7
|
<FRAMESET rows="100, 200, 200">
|
|
7
8
|
<FRAME name="frame1" src="../tc_relative_links.html">
|
|
@@ -13,7 +14,6 @@
|
|
|
13
14
|
[Your user agent does not support frames or is currently configured
|
|
14
15
|
not to display frames. However, you may visit
|
|
15
16
|
<A href="foo.html">the related document.</A>]
|
|
16
|
-
<a href="?a=b">just the query string</A>
|
|
17
17
|
</IFRAME>
|
|
18
18
|
</FRAMESET>
|
|
19
19
|
</FRAMESET>
|
data/test/test_mechanize.rb
CHANGED
|
@@ -1016,8 +1016,16 @@ but not <a href="/" rel="me nofollow">this</a>!
|
|
|
1016
1016
|
|
|
1017
1017
|
def test_read_timeout_equals
|
|
1018
1018
|
@mech.read_timeout = 5
|
|
1019
|
-
|
|
1020
1019
|
assert_equal 5, @mech.read_timeout
|
|
1020
|
+
assert @mech.get('http://localhost/response_code?code=200')
|
|
1021
|
+
assert_equal 5, @mech.agent.http.read_timeout
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
def test_write_timeout_equals
|
|
1025
|
+
@mech.write_timeout = 7
|
|
1026
|
+
assert_equal 7, @mech.write_timeout
|
|
1027
|
+
assert @mech.get('http://localhost/response_code?code=200')
|
|
1028
|
+
assert_equal 7, @mech.agent.http.write_timeout
|
|
1021
1029
|
end
|
|
1022
1030
|
|
|
1023
1031
|
def test_timeouts_for_file_connection
|
|
@@ -464,12 +464,50 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
464
464
|
assert_equal @headers, @req.to_hash.keys.sort
|
|
465
465
|
end
|
|
466
466
|
|
|
467
|
-
def
|
|
467
|
+
def test_fetch_accepts_symbol_names_in_request_headers
|
|
468
|
+
@agent.request_headers = { Authorization: 'Bearer tokensecret' }
|
|
469
|
+
|
|
470
|
+
page = @agent.fetch 'http://example/http_headers'
|
|
471
|
+
|
|
472
|
+
assert_match 'authorization|Bearer tokensecret', page.body
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def test_fetch_lets_a_request_header_override_any_spelling_of_an_agent_header
|
|
476
|
+
@agent.request_headers = { 'Authorization' => 'agent-one', 'authorization' => 'agent-two' }
|
|
477
|
+
|
|
478
|
+
page = @agent.fetch 'http://example/http_headers', :get,
|
|
479
|
+
{ 'Authorization' => 'per-request' }
|
|
480
|
+
|
|
481
|
+
assert_match 'authorization|per-request', page.body
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def test_meta_refresh_drops_entity_headers
|
|
485
|
+
@agent.follow_meta_refresh = true
|
|
486
|
+
|
|
487
|
+
uri = URI 'http://example/session'
|
|
488
|
+
response = Net::HTTPOK.new '1.1', '200', 'OK'
|
|
489
|
+
response['Refresh'] = '0;url=/http_headers'
|
|
490
|
+
page = Mechanize::Page.new uri, response, '', 200, @mech
|
|
491
|
+
|
|
492
|
+
@agent.response_follow_meta_refresh(response, uri, page, 0,
|
|
493
|
+
{ 'Content-Length' => '7',
|
|
494
|
+
'Content-Type' => 'text/plain',
|
|
495
|
+
'Content-MD5' => 'deadbeef' })
|
|
496
|
+
|
|
497
|
+
request = requests.last
|
|
498
|
+
|
|
499
|
+
assert_equal 'GET', request.method
|
|
500
|
+
assert_nil request['Content-Length']
|
|
501
|
+
assert_nil request['Content-Type']
|
|
502
|
+
assert_nil request['Content-MD5']
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def test_fetch_applies_request_headers
|
|
468
506
|
@agent.request_headers['X-Foo'] = 'bar'
|
|
469
507
|
|
|
470
|
-
@agent.
|
|
508
|
+
page = @agent.fetch 'http://example/http_headers'
|
|
471
509
|
|
|
472
|
-
|
|
510
|
+
assert_match 'x-foo|bar', page.body
|
|
473
511
|
end
|
|
474
512
|
|
|
475
513
|
def test_request_add_headers_symbol
|
|
@@ -1300,6 +1338,33 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
1300
1338
|
assert_equal uri, page.uri
|
|
1301
1339
|
end
|
|
1302
1340
|
|
|
1341
|
+
def test_response_follow_meta_refresh_to_cross_origin_drops_agent_request_headers
|
|
1342
|
+
uri = URI.parse 'http://example/'
|
|
1343
|
+
|
|
1344
|
+
body = <<-BODY
|
|
1345
|
+
<title></title>
|
|
1346
|
+
<meta http-equiv="refresh" content="0;url=http://trap/http_headers">
|
|
1347
|
+
BODY
|
|
1348
|
+
|
|
1349
|
+
page = Mechanize::Page.new(uri, nil, body, 200, @mech)
|
|
1350
|
+
|
|
1351
|
+
@agent.follow_meta_refresh = true
|
|
1352
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1353
|
+
|
|
1354
|
+
page = @agent.response_follow_meta_refresh @res, uri, page, 0
|
|
1355
|
+
|
|
1356
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
def test_meta_refresh_to_same_origin_keeps_agent_request_headers
|
|
1360
|
+
@agent.follow_meta_refresh = true
|
|
1361
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1362
|
+
|
|
1363
|
+
page = @agent.fetch 'http://example/http_refresh?refresh_url=/http_headers'
|
|
1364
|
+
|
|
1365
|
+
assert_match("authorization|Bearer tokensecret", page.body)
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1303
1368
|
def test_response_follow_meta_refresh_limit
|
|
1304
1369
|
uri = URI.parse 'http://example/#id+1'
|
|
1305
1370
|
|
|
@@ -1669,6 +1734,171 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
1669
1734
|
refute_match("cookie|name=value", page.body)
|
|
1670
1735
|
end
|
|
1671
1736
|
|
|
1737
|
+
def test_response_redirect_to_cross_site_strips_all_credential_headers
|
|
1738
|
+
@agent.redirect_ok = true
|
|
1739
|
+
|
|
1740
|
+
headers = {
|
|
1741
|
+
'Range' => 'bytes=0-9999',
|
|
1742
|
+
'Proxy-Authorization' => 'Basic proxysecret',
|
|
1743
|
+
'Cookie2' => 'version=1',
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
page = html_page ''
|
|
1747
|
+
page = @agent.response_redirect({ 'Location' => 'http://trap/http_headers' }, :get,
|
|
1748
|
+
page, 0, headers)
|
|
1749
|
+
|
|
1750
|
+
refute_includes(headers.keys, "Proxy-Authorization")
|
|
1751
|
+
refute_includes(headers.keys, "Cookie2")
|
|
1752
|
+
|
|
1753
|
+
refute_match("proxy-authorization|Basic proxysecret", page.body)
|
|
1754
|
+
refute_match("cookie2|version=1", page.body)
|
|
1755
|
+
end
|
|
1756
|
+
|
|
1757
|
+
def test_response_redirect_to_same_site_keeps_all_credential_headers
|
|
1758
|
+
@agent.redirect_ok = true
|
|
1759
|
+
|
|
1760
|
+
headers = {
|
|
1761
|
+
'Proxy-Authorization' => 'Basic proxysecret',
|
|
1762
|
+
'Cookie2' => 'version=1',
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
page = html_page ''
|
|
1766
|
+
page = @agent.response_redirect({ 'Location' => '/http_headers' }, :get,
|
|
1767
|
+
page, 0, headers)
|
|
1768
|
+
|
|
1769
|
+
assert_match("proxy-authorization|Basic proxysecret", page.body)
|
|
1770
|
+
assert_match("cookie2|version=1", page.body)
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1773
|
+
def test_response_redirect_to_cross_site_drops_agent_request_headers
|
|
1774
|
+
@agent.redirect_ok = true
|
|
1775
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret', 'Cookie' => 'name=value' }
|
|
1776
|
+
|
|
1777
|
+
page = html_page ''
|
|
1778
|
+
page = @agent.response_redirect({ 'Location' => 'http://trap/http_headers' }, :get,
|
|
1779
|
+
page, 0, {})
|
|
1780
|
+
|
|
1781
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1782
|
+
refute_match("cookie|name=value", page.body)
|
|
1783
|
+
end
|
|
1784
|
+
|
|
1785
|
+
def test_redirect_to_cross_site_keeps_insensitive_agent_request_headers
|
|
1786
|
+
@agent.redirect_ok = true
|
|
1787
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret', 'X-Api-Key' => 'apikey' }
|
|
1788
|
+
|
|
1789
|
+
page = @agent.fetch 'http://example/redirect', :get,
|
|
1790
|
+
{ 'X-Location' => 'http://trap/http_headers' }
|
|
1791
|
+
|
|
1792
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1793
|
+
assert_match("x-api-key|apikey", page.body)
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def test_redirect_to_same_site_keeps_agent_request_headers
|
|
1797
|
+
@agent.redirect_ok = true
|
|
1798
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1799
|
+
|
|
1800
|
+
page = @agent.fetch 'http://example/redirect', :get, { 'X-Location' => '/http_headers' }
|
|
1801
|
+
|
|
1802
|
+
assert_match("authorization|Bearer tokensecret", page.body)
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
def test_response_redirect_to_downgraded_scheme_strips_credential
|
|
1806
|
+
@agent.redirect_ok = true
|
|
1807
|
+
|
|
1808
|
+
headers = { 'AUTHORIZATION' => 'Basic xxx', 'cookie' => 'name=value' }
|
|
1809
|
+
|
|
1810
|
+
page = page 'https://example:8443/', 'text/html'
|
|
1811
|
+
page = @agent.response_redirect({ 'Location' => 'http://example:8443/http_headers' }, :get,
|
|
1812
|
+
page, 0, headers)
|
|
1813
|
+
|
|
1814
|
+
refute_includes(headers.keys, "AUTHORIZATION")
|
|
1815
|
+
refute_includes(headers.keys, "cookie")
|
|
1816
|
+
|
|
1817
|
+
refute_match("authorization|Basic xxx", page.body)
|
|
1818
|
+
refute_match("cookie|name=value", page.body)
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1821
|
+
def test_response_redirect_to_downgraded_scheme_drops_agent_request_headers
|
|
1822
|
+
@agent.redirect_ok = true
|
|
1823
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1824
|
+
|
|
1825
|
+
page = page 'https://example:8443/', 'text/html'
|
|
1826
|
+
page = @agent.response_redirect({ 'Location' => 'http://example:8443/http_headers' }, :get,
|
|
1827
|
+
page, 0, {})
|
|
1828
|
+
|
|
1829
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1830
|
+
end
|
|
1831
|
+
|
|
1832
|
+
def test_response_redirect_to_different_port_strips_cookie
|
|
1833
|
+
@agent.redirect_ok = true
|
|
1834
|
+
|
|
1835
|
+
headers = { 'cookie' => 'name=value' }
|
|
1836
|
+
|
|
1837
|
+
page = html_page ''
|
|
1838
|
+
page = @agent.response_redirect({ 'Location' => 'http://example:81/http_headers' }, :get,
|
|
1839
|
+
page, 0, headers)
|
|
1840
|
+
|
|
1841
|
+
refute_match("cookie|name=value", page.body)
|
|
1842
|
+
end
|
|
1843
|
+
|
|
1844
|
+
# Every request the agent made to a host other than the one the credential
|
|
1845
|
+
# was set for.
|
|
1846
|
+
def foreign_requests
|
|
1847
|
+
requests.reject { |request| request['host'] == 'example' }
|
|
1848
|
+
end
|
|
1849
|
+
|
|
1850
|
+
def test_second_redirect_hop_on_a_foreign_origin_does_not_restore_credentials
|
|
1851
|
+
@agent.redirect_ok = true
|
|
1852
|
+
@agent.redirection_limit = 2
|
|
1853
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1854
|
+
|
|
1855
|
+
assert_raises Mechanize::RedirectLimitReachedError do
|
|
1856
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1857
|
+
{ 'X-Location' => 'http://trap/redirect' }
|
|
1858
|
+
end
|
|
1859
|
+
|
|
1860
|
+
assert_operator foreign_requests.length, :>, 1
|
|
1861
|
+
assert_empty foreign_requests.select { |r| r['Authorization'] }
|
|
1862
|
+
end
|
|
1863
|
+
|
|
1864
|
+
def test_authentication_retry_on_a_foreign_origin_does_not_restore_credentials
|
|
1865
|
+
@agent.redirect_ok = true
|
|
1866
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1867
|
+
@agent.add_auth URI('http://trap/'), 'user', 'pass'
|
|
1868
|
+
|
|
1869
|
+
begin
|
|
1870
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1871
|
+
{ 'X-Location' => 'http://trap/basic_auth' }
|
|
1872
|
+
rescue Mechanize::UnauthorizedError
|
|
1873
|
+
# a leaked bearer token overwrites the Basic credential and the retry fails
|
|
1874
|
+
end
|
|
1875
|
+
|
|
1876
|
+
assert_operator foreign_requests.length, :>, 1
|
|
1877
|
+
assert_empty foreign_requests.select { |r| r['Authorization'] == 'Bearer tokensecret' }
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1880
|
+
def test_meta_refresh_to_a_second_foreign_origin_does_not_restore_credentials
|
|
1881
|
+
@agent.redirect_ok = true
|
|
1882
|
+
@agent.follow_meta_refresh = true
|
|
1883
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1884
|
+
|
|
1885
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1886
|
+
{ 'X-Location' => 'http://trap/http_refresh?refresh_url=/http_headers' }
|
|
1887
|
+
|
|
1888
|
+
assert_nil requests.find { |r| r.path == '/http_headers' }['Authorization']
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
def test_robots_lookup_on_a_foreign_origin_does_not_send_credentials
|
|
1892
|
+
@agent.redirect_ok = true
|
|
1893
|
+
@agent.robots = true
|
|
1894
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1895
|
+
|
|
1896
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1897
|
+
{ 'X-Location' => 'http://trap/http_headers' }
|
|
1898
|
+
|
|
1899
|
+
assert_nil requests.find { |r| r.path == '/robots.txt' }['Authorization']
|
|
1900
|
+
end
|
|
1901
|
+
|
|
1672
1902
|
def test_response_redirect_to_same_site_with_credential
|
|
1673
1903
|
@agent.redirect_ok = true
|
|
1674
1904
|
|
|
@@ -1704,11 +1934,11 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
1704
1934
|
page, 0, headers)
|
|
1705
1935
|
|
|
1706
1936
|
refute_includes(headers.keys, "AUTHORIZATION")
|
|
1707
|
-
|
|
1937
|
+
refute_includes(headers.keys, "cookie")
|
|
1708
1938
|
|
|
1709
1939
|
assert_match("range|bytes=0-9999", page.body)
|
|
1710
1940
|
refute_match("authorization|Basic xxx", page.body)
|
|
1711
|
-
|
|
1941
|
+
refute_match("cookie|name=value", page.body)
|
|
1712
1942
|
end
|
|
1713
1943
|
|
|
1714
1944
|
def test_response_redirect_not_ok
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mechanize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.14.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Hodel
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
- Lee Jarvis
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date:
|
|
14
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: addressable
|
|
@@ -232,10 +232,11 @@ email:
|
|
|
232
232
|
executables: []
|
|
233
233
|
extensions: []
|
|
234
234
|
extra_rdoc_files:
|
|
235
|
+
- CHANGELOG.md
|
|
235
236
|
- EXAMPLES.rdoc
|
|
236
237
|
- GUIDE.rdoc
|
|
237
|
-
- CHANGELOG.md
|
|
238
238
|
- README.md
|
|
239
|
+
- SECURITY.md
|
|
239
240
|
files:
|
|
240
241
|
- ".autotest"
|
|
241
242
|
- ".github/dependabot.yml"
|
|
@@ -250,6 +251,7 @@ files:
|
|
|
250
251
|
- LICENSE.txt
|
|
251
252
|
- README.md
|
|
252
253
|
- Rakefile
|
|
254
|
+
- SECURITY.md
|
|
253
255
|
- examples/flickr_upload.rb
|
|
254
256
|
- examples/latest_user_agents.rb
|
|
255
257
|
- examples/mech-dump.rb
|
|
@@ -474,7 +476,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
474
476
|
- !ruby/object:Gem::Version
|
|
475
477
|
version: '0'
|
|
476
478
|
requirements: []
|
|
477
|
-
rubygems_version:
|
|
479
|
+
rubygems_version: 4.0.16
|
|
478
480
|
specification_version: 4
|
|
479
481
|
summary: The Mechanize library is used for automating interaction with websites
|
|
480
482
|
test_files:
|