final_redirect_url 2.0.0 → 2.1.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 +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +85 -26
- data/final_redirect_url.gemspec +5 -2
- data/lib/final_redirect_url/version.rb +1 -1
- data/lib/final_redirect_url.rb +120 -44
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c9fa9551dd7959832dfebc8b24e0cf5c5b326100385a2bf287dab1e50b98581
|
|
4
|
+
data.tar.gz: 2ebc2f9667d598f693fbf766c9eb55991477281cc8fff327ef8d2b693570e0f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54a6f3c70af375bf6743eb59f2c4441e4e0d072db4cd654ca94001daa0994638c896dcb2940a36f969004fd42e2f0675c2a278c64535ea6225e33e9a529ac237
|
|
7
|
+
data.tar.gz: 87c4b58fd877c878b109ad2f5ade9f802710e038cab50e9cf1c47ab89ef420c18cbe49570044313a26831f293176fb228df98a7fa78052c68da534f91b666b9e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.1.0] – 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Fork Contributions Integrated
|
|
13
|
+
Analysed all six public forks. Two contained work worth considering:
|
|
14
|
+
|
|
15
|
+
- **counterfind/final_redirect_url** (9 commits ahead): introduced Nokogiri meta-refresh parsing,
|
|
16
|
+
per-request timeouts, and HTTPS via `use_ssl`. The meta-refresh concept was adopted and
|
|
17
|
+
rewritten from scratch (removed `pry`, removed `KNOWN_SHORTENERS`, eliminated the
|
|
18
|
+
thread-unsafe module-level `@final_url`, fixed the deprecated `URI.regexp` call, replaced
|
|
19
|
+
`rescue Exception`, and switched `uri.host` → `uri.to_s` to preserve ports). Architecture
|
|
20
|
+
unchanged.
|
|
21
|
+
- **HowardSnowden/final_redirect_url** (4 commits ahead): extracted `get_response` as a named
|
|
22
|
+
method and added explicit SSL cipher configuration. The pattern was incorporated into the
|
|
23
|
+
new `fetch_response` method; the hardcoded cipher list was not adopted (unnecessary for
|
|
24
|
+
modern Ruby/OpenSSL).
|
|
25
|
+
- **pilot34, mbv, ymow**: trivial (limit logging, version bump, no changes) — discarded.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`FinalRedirectUrl.trace(url, options)`** — returns `Array<Hop>` documenting every URL and
|
|
29
|
+
HTTP status code visited in the redirect chain. Useful for debugging and link audits.
|
|
30
|
+
- **`FinalRedirectUrl::Hop`** struct with `url` (String) and `status` (Integer, nil when depth
|
|
31
|
+
exhausted) attributes.
|
|
32
|
+
- **Custom headers** (`options[:headers]`) — pass any HTTP headers (e.g. `User-Agent`, `Accept`,
|
|
33
|
+
`Authorization`) to bypass anti-bot blocks or authenticate requests.
|
|
34
|
+
- **Configurable timeouts** — `options[:open_timeout]` and `options[:read_timeout]` default to
|
|
35
|
+
**60 seconds** (up from the previous 10 s hard-coded value).
|
|
36
|
+
- **`verify_ssl` option** (`true` by default) — set `verify_ssl: false` to bypass strict SSL
|
|
37
|
+
certificate validation (e.g. for self-signed certificates in development).
|
|
38
|
+
- **Stateful cookie handling** — `Set-Cookie` headers from every 3xx response are captured and
|
|
39
|
+
injected as `Cookie` headers into subsequent requests, enabling cookie-gated redirect chains
|
|
40
|
+
to be followed correctly.
|
|
41
|
+
- **Meta-refresh parsing** (via Nokogiri, adapted from the `counterfind` fork) — when a 200 OK
|
|
42
|
+
response contains `<meta http-equiv="refresh">`, the gem follows the redirect URL. Handles
|
|
43
|
+
case-insensitive `HTTP-EQUIV`, quoted URLs, and malformed `content` attributes. Only fires
|
|
44
|
+
for `text/html` responses.
|
|
45
|
+
- **nokogiri** added as a runtime dependency.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Switched from `HEAD` to `GET` requests. `GET` is required so that the response body is
|
|
49
|
+
available for meta-refresh parsing; it also improves compatibility with servers that reject
|
|
50
|
+
`HEAD` with 405.
|
|
51
|
+
- `final_redirect_url` now delegates to `trace` internally, eliminating duplicated logic.
|
|
52
|
+
- Default timeouts raised from 10 s to **60 s**.
|
|
53
|
+
|
|
54
|
+
### No Open Issues
|
|
55
|
+
All issues were resolved in v2.0.0. No new open issues exist at the time of this release.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
10
59
|
## [2.0.0] – 2026-11-05
|
|
11
60
|
|
|
12
61
|
### Breaking Changes
|
|
@@ -54,6 +103,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
54
103
|
headless browser and are outside the scope of this gem. The gem returns the last HTTP-level
|
|
55
104
|
reachable URL in such cases.
|
|
56
105
|
|
|
106
|
+
[2.1.0]: https://github.com/indyarocks/final_redirect_url/compare/v2.0.0...v2.1.0
|
|
57
107
|
[2.0.0]: https://github.com/indyarocks/final_redirect_url/compare/v0.1.1...v2.0.0
|
|
58
108
|
[i2]: https://github.com/indyarocks/final_redirect_url/issues/2
|
|
59
109
|
[i3]: https://github.com/indyarocks/final_redirect_url/issues/3
|
data/README.md
CHANGED
|
@@ -3,69 +3,128 @@
|
|
|
3
3
|
[](https://github.com/indyarocks/final_redirect_url/actions/workflows/ci.yml)
|
|
4
4
|
[](https://badge.fury.io/rb/final_redirect_url)
|
|
5
5
|
|
|
6
|
-
Ruby gem that follows HTTP/HTTPS redirects
|
|
6
|
+
Ruby gem that follows HTTP/HTTPS redirects — including `<meta http-equiv="refresh">` and
|
|
7
|
+
cookie-gated chains — and returns the final destination URL.
|
|
7
8
|
|
|
8
9
|
## Requirements
|
|
9
10
|
|
|
10
11
|
- Ruby >= 3.0
|
|
12
|
+
- [Nokogiri](https://nokogiri.org) (bundled as a runtime dependency for meta-refresh parsing)
|
|
11
13
|
|
|
12
14
|
## Installation
|
|
13
15
|
|
|
14
|
-
Add this line to your application's Gemfile:
|
|
15
|
-
|
|
16
16
|
```ruby
|
|
17
17
|
gem 'final_redirect_url'
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Then run:
|
|
21
|
-
|
|
22
20
|
```
|
|
23
21
|
$ bundle install
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
or
|
|
27
25
|
|
|
28
26
|
```
|
|
29
27
|
$ gem install final_redirect_url
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
The input URL **must** include the `http://` or `https://` scheme.
|
|
30
|
+
## Basic usage
|
|
35
31
|
|
|
36
32
|
```ruby
|
|
37
33
|
require 'final_redirect_url'
|
|
38
34
|
|
|
39
|
-
# Simple redirect following
|
|
40
35
|
FinalRedirectUrl.final_redirect_url('http://google.com')
|
|
41
36
|
# => "https://www.google.com/"
|
|
42
37
|
|
|
43
|
-
# Ports are preserved
|
|
44
|
-
FinalRedirectUrl.final_redirect_url('http://localhost:3000/items/42')
|
|
45
|
-
# => "http://localhost:3000/items/42"
|
|
46
|
-
|
|
47
|
-
# Custom redirect depth (default: 10)
|
|
48
|
-
FinalRedirectUrl.final_redirect_url('http://example.com', depth: 5)
|
|
49
|
-
# => "http://final.example.com/"
|
|
50
|
-
|
|
51
38
|
# Invalid / missing protocol → empty string
|
|
52
39
|
FinalRedirectUrl.final_redirect_url('www.example.com')
|
|
53
40
|
# => ""
|
|
54
41
|
```
|
|
55
42
|
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
All options are passed as a second argument hash:
|
|
46
|
+
|
|
47
|
+
| Option | Type | Default | Description |
|
|
48
|
+
|----------------|---------|---------|-------------|
|
|
49
|
+
| `:depth` | Integer | `10` | Maximum redirect hops to follow |
|
|
50
|
+
| `:open_timeout` | Integer | `60` | TCP connect timeout in seconds |
|
|
51
|
+
| `:read_timeout` | Integer | `60` | Read timeout in seconds |
|
|
52
|
+
| `:verify_ssl` | Boolean | `true` | Enforce SSL certificate validation |
|
|
53
|
+
| `:headers` | Hash | `{}` | Extra HTTP headers sent with every request |
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# Limit redirect depth
|
|
57
|
+
FinalRedirectUrl.final_redirect_url('http://example.com', depth: 5)
|
|
58
|
+
|
|
59
|
+
# Custom timeouts (seconds)
|
|
60
|
+
FinalRedirectUrl.final_redirect_url('http://slow.example.com', open_timeout: 5, read_timeout: 30)
|
|
61
|
+
|
|
62
|
+
# Bypass SSL certificate verification (e.g. self-signed certs in development)
|
|
63
|
+
FinalRedirectUrl.final_redirect_url('https://self-signed.local', verify_ssl: false)
|
|
64
|
+
|
|
65
|
+
# Custom headers — override User-Agent or add Authorization / Accept
|
|
66
|
+
FinalRedirectUrl.final_redirect_url(
|
|
67
|
+
'http://bot-protected.example.com',
|
|
68
|
+
headers: {
|
|
69
|
+
'User-Agent' => 'Mozilla/5.0 (compatible; MyApp/1.0)',
|
|
70
|
+
'Accept' => 'text/html'
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Redirect trace
|
|
76
|
+
|
|
77
|
+
`FinalRedirectUrl.trace` returns every hop in the chain as an array of `Hop` structs.
|
|
78
|
+
Each `Hop` has a `url` (String) and a `status` (Integer, or `nil` when the depth limit is
|
|
79
|
+
reached before a request is made).
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
hops = FinalRedirectUrl.trace('http://hop1.example.com')
|
|
83
|
+
|
|
84
|
+
hops.each { |hop| puts "#{hop.status} #{hop.url}" }
|
|
85
|
+
# 301 http://hop1.example.com/
|
|
86
|
+
# 302 http://hop2.example.com/
|
|
87
|
+
# 200 https://final.example.com/
|
|
88
|
+
|
|
89
|
+
hops.last.url # => "https://final.example.com/"
|
|
90
|
+
hops.last.status # => 200
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`trace` accepts the same options as `final_redirect_url`.
|
|
94
|
+
|
|
95
|
+
## Meta-refresh support
|
|
96
|
+
|
|
97
|
+
When the gem reaches a `200 OK` response whose body contains a
|
|
98
|
+
`<meta http-equiv="refresh" content="N; url=...">` tag, it follows that URL as if it were an
|
|
99
|
+
HTTP redirect. Case-insensitive attribute matching and quoted URL values are both handled.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# Page at http://meta.example.com/ contains:
|
|
103
|
+
# <meta http-equiv="refresh" content="0; url=http://final.example.com/">
|
|
104
|
+
FinalRedirectUrl.final_redirect_url('http://meta.example.com/')
|
|
105
|
+
# => "http://final.example.com/"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Cookie-gated redirects
|
|
109
|
+
|
|
110
|
+
`Set-Cookie` headers from every `3xx` response are captured and re-sent as `Cookie` headers on
|
|
111
|
+
all subsequent requests in the same chain, allowing the gem to follow redirects that require an
|
|
112
|
+
active session cookie.
|
|
113
|
+
|
|
56
114
|
## Limitations
|
|
57
115
|
|
|
58
|
-
- **JavaScript / AMP redirects** (e.g. some Google News AMP URLs)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
116
|
+
- **JavaScript / AMP redirects** (e.g. some Google News AMP URLs) require a headless browser
|
|
117
|
+
and are outside the scope of this gem. The gem returns the last HTTP-reachable URL in such
|
|
118
|
+
cases.
|
|
119
|
+
- Servers that reject `HEAD` requests with `405 Method Not Allowed` are handled correctly
|
|
120
|
+
because the gem uses `GET` for all requests.
|
|
62
121
|
|
|
63
122
|
## Contributing
|
|
64
123
|
|
|
65
|
-
Bug reports and pull requests are welcome on GitHub at
|
|
66
|
-
|
|
67
|
-
|
|
124
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
125
|
+
<https://github.com/indyarocks/final_redirect_url>. Contributors are expected to adhere to the
|
|
126
|
+
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
68
127
|
|
|
69
128
|
## License
|
|
70
129
|
|
|
71
|
-
|
|
130
|
+
Available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/final_redirect_url.gemspec
CHANGED
|
@@ -9,8 +9,9 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ['chandan.jhun@gmail.com']
|
|
10
10
|
|
|
11
11
|
spec.summary = 'Get the final redirect URL for any given URL.'
|
|
12
|
-
spec.description = 'Follows HTTP redirects (including
|
|
13
|
-
'non-standard ports) to resolve the
|
|
12
|
+
spec.description = 'Follows HTTP/HTTPS redirects (including meta-refresh, cookie-gated ' \
|
|
13
|
+
'redirects, relative paths, and non-standard ports) to resolve the ' \
|
|
14
|
+
'final destination URL. Provides a trace method for full hop inspection.'
|
|
14
15
|
spec.homepage = 'https://github.com/indyarocks/final_redirect_url'
|
|
15
16
|
spec.license = 'MIT'
|
|
16
17
|
|
|
@@ -27,6 +28,8 @@ Gem::Specification.new do |spec|
|
|
|
27
28
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
28
29
|
spec.require_paths = ['lib']
|
|
29
30
|
|
|
31
|
+
spec.add_dependency 'nokogiri', '~> 1.0'
|
|
32
|
+
|
|
30
33
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
31
34
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
32
35
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
data/lib/final_redirect_url.rb
CHANGED
|
@@ -3,88 +3,164 @@ require 'net/http'
|
|
|
3
3
|
require 'net/https'
|
|
4
4
|
require 'uri'
|
|
5
5
|
require 'logger'
|
|
6
|
+
require 'nokogiri'
|
|
6
7
|
|
|
7
8
|
module FinalRedirectUrl
|
|
8
|
-
DEFAULT_DEPTH
|
|
9
|
+
DEFAULT_DEPTH = 10
|
|
10
|
+
DEFAULT_OPEN_TIMEOUT = 60
|
|
11
|
+
DEFAULT_READ_TIMEOUT = 60
|
|
9
12
|
USER_AGENT = "FinalRedirectUrl/#{VERSION} (+https://github.com/indyarocks/final_redirect_url)".freeze
|
|
10
13
|
|
|
14
|
+
# Value object representing one step in a redirect chain.
|
|
15
|
+
# hop.url # => "http://example.com/" (String)
|
|
16
|
+
# hop.status # => 301 (Integer, or nil when depth limit reached)
|
|
17
|
+
Hop = Struct.new(:url, :status, keyword_init: true)
|
|
18
|
+
|
|
11
19
|
class << self
|
|
12
20
|
# Resolves all HTTP/HTTPS redirects for the given URL and returns the final
|
|
13
|
-
# destination as a
|
|
14
|
-
# unrecoverable network errors.
|
|
21
|
+
# destination as a String. Returns "" for invalid input or unrecoverable errors.
|
|
15
22
|
#
|
|
16
|
-
# @param url
|
|
23
|
+
# @param url [String] starting URL (must include http:// or https://)
|
|
17
24
|
# @param options [Hash]
|
|
18
|
-
# @option options [Integer] :depth
|
|
25
|
+
# @option options [Integer] :depth max redirect hops (default: 10)
|
|
26
|
+
# @option options [Integer] :open_timeout TCP connect timeout in sec (default: 60)
|
|
27
|
+
# @option options [Integer] :read_timeout read timeout in sec (default: 60)
|
|
28
|
+
# @option options [Boolean] :verify_ssl enforce SSL cert validation (default: true)
|
|
29
|
+
# @option options [Hash] :headers extra HTTP request headers
|
|
19
30
|
# @return [String]
|
|
20
31
|
def final_redirect_url(url, options = {})
|
|
21
|
-
|
|
32
|
+
hops = trace(url, options)
|
|
33
|
+
hops.empty? ? '' : hops.last.url
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Follows all redirects and returns every hop as an Array of Hop structs.
|
|
37
|
+
# Useful for debugging, link audits, or building redirect-aware tooling.
|
|
38
|
+
#
|
|
39
|
+
# @param url [String]
|
|
40
|
+
# @param options [Hash] same keys as #final_redirect_url
|
|
41
|
+
# @return [Array<Hop>]
|
|
42
|
+
def trace(url, options = {})
|
|
43
|
+
return [] unless valid_url?(url)
|
|
22
44
|
|
|
23
|
-
limit
|
|
24
|
-
|
|
45
|
+
limit = positive_int(options[:depth], DEFAULT_DEPTH)
|
|
46
|
+
hops = []
|
|
47
|
+
cookies = {}
|
|
25
48
|
|
|
26
|
-
|
|
49
|
+
resolve(url, limit, options, hops, cookies)
|
|
50
|
+
hops
|
|
27
51
|
rescue StandardError => ex
|
|
28
52
|
Logger.new($stdout).error(ex.message)
|
|
29
|
-
|
|
53
|
+
hops
|
|
30
54
|
end
|
|
31
55
|
|
|
32
56
|
private
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
# Fixes issue #4: private via class << self so the method is genuinely
|
|
36
|
-
# inaccessible from outside the module.
|
|
37
|
-
def valid_url?(url)
|
|
38
|
-
uri = URI.parse(url.to_s)
|
|
39
|
-
uri.is_a?(URI::HTTP)
|
|
40
|
-
rescue URI::InvalidURIError
|
|
41
|
-
false
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Recursively follows redirects up to +limit+ hops.
|
|
45
|
-
# Fixes issue #2: when the hop limit is reached, returns the current URI
|
|
46
|
-
# (a proper URI object, not a bare string) so url_string_from_uri never
|
|
47
|
-
# receives a plain String.
|
|
48
|
-
# Fixes issue #6: uses URI.join to build absolute redirect targets, which
|
|
49
|
-
# preserves non-standard ports from the base URI (e.g. localhost:3000).
|
|
50
|
-
def resolve_redirects(url, limit)
|
|
58
|
+
def resolve(url, limit, options, hops, cookies)
|
|
51
59
|
uri = URI.parse(url)
|
|
52
|
-
return uri if limit <= 0
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
if limit <= 0
|
|
62
|
+
# Depth exhausted — record the pending URL without making a request.
|
|
63
|
+
hops << Hop.new(url: uri.to_s, status: nil)
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
response = fetch_response(uri, options, cookies)
|
|
68
|
+
collect_cookies(response, cookies)
|
|
55
69
|
|
|
56
70
|
case response
|
|
57
71
|
when Net::HTTPOK
|
|
58
|
-
uri
|
|
72
|
+
if (meta_url = meta_refresh_url(response, uri))
|
|
73
|
+
hops << Hop.new(url: uri.to_s, status: response.code.to_i)
|
|
74
|
+
warn "meta-refresh to #{meta_url}"
|
|
75
|
+
resolve(meta_url, limit - 1, options, hops, cookies)
|
|
76
|
+
else
|
|
77
|
+
hops << Hop.new(url: uri.to_s, status: response.code.to_i)
|
|
78
|
+
end
|
|
59
79
|
when Net::HTTPRedirection
|
|
60
|
-
location = response['location']
|
|
80
|
+
location = absolute_url(uri, response['location'])
|
|
81
|
+
hops << Hop.new(url: uri.to_s, status: response.code.to_i)
|
|
61
82
|
warn "redirected to #{location}"
|
|
62
|
-
|
|
83
|
+
resolve(location, limit - 1, options, hops, cookies)
|
|
63
84
|
else
|
|
64
|
-
# Non-
|
|
65
|
-
|
|
66
|
-
uri
|
|
85
|
+
# Non-200, non-redirect (403, 404, 500 …): record and stop.
|
|
86
|
+
hops << Hop.new(url: uri.to_s, status: response.code.to_i)
|
|
67
87
|
end
|
|
68
88
|
end
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def fetch_response(uri)
|
|
73
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
90
|
+
def fetch_response(uri, options, cookies)
|
|
91
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
74
92
|
http.use_ssl = (uri.scheme == 'https')
|
|
75
|
-
http.
|
|
76
|
-
http.
|
|
93
|
+
http.verify_mode = options.fetch(:verify_ssl, true) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
94
|
+
http.open_timeout = positive_int(options[:open_timeout], DEFAULT_OPEN_TIMEOUT)
|
|
95
|
+
http.read_timeout = positive_int(options[:read_timeout], DEFAULT_READ_TIMEOUT)
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
# GET is required so we can read the body for meta-refresh detection.
|
|
98
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
79
99
|
request['User-Agent'] = USER_AGENT
|
|
80
100
|
|
|
101
|
+
# Caller-supplied headers override defaults (including User-Agent).
|
|
102
|
+
(options[:headers] || {}).each { |k, v| request[k.to_s] = v }
|
|
103
|
+
|
|
104
|
+
# Inject accumulated cookies for cookie-gated redirect chains.
|
|
105
|
+
request['Cookie'] = cookies.map { |k, v| "#{k}=#{v}" }.join('; ') unless cookies.empty?
|
|
106
|
+
|
|
81
107
|
http.request(request)
|
|
82
108
|
end
|
|
83
109
|
|
|
84
|
-
#
|
|
85
|
-
|
|
110
|
+
# Harvests Set-Cookie values from a response into the shared cookie jar.
|
|
111
|
+
def collect_cookies(response, cookies)
|
|
112
|
+
Array(response.get_fields('set-cookie')).each do |cookie_str|
|
|
113
|
+
name, value = cookie_str.split(';', 2).first.to_s.split('=', 2)
|
|
114
|
+
cookies[name.to_s.strip] = value.to_s.strip
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Parses <meta http-equiv="refresh"> from an HTML body and returns the
|
|
119
|
+
# resolved absolute URL, or nil when no meta-refresh is present.
|
|
120
|
+
# Inspired by the counterfind fork; rewritten for correctness and safety.
|
|
121
|
+
def meta_refresh_url(response, base_uri)
|
|
122
|
+
content_type = response['content-type'].to_s
|
|
123
|
+
return nil unless content_type.empty? || content_type.include?('html')
|
|
124
|
+
return nil if response.body.to_s.empty?
|
|
125
|
+
|
|
126
|
+
doc = Nokogiri::HTML.parse(response.body)
|
|
127
|
+
meta = doc.at_xpath(
|
|
128
|
+
"//meta[translate(@http-equiv," \
|
|
129
|
+
"'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='refresh']/@content"
|
|
130
|
+
)
|
|
131
|
+
return nil unless meta
|
|
132
|
+
|
|
133
|
+
# Content examples:
|
|
134
|
+
# "0; url=https://final.example.com/"
|
|
135
|
+
# "5; URL='https://final.example.com/'"
|
|
136
|
+
# "0; https://final.example.com/" (some malformed pages omit "url=")
|
|
137
|
+
raw = meta.value.to_s
|
|
138
|
+
url_part = raw.split(/;\s*/i, 2).last.to_s
|
|
139
|
+
.sub(/\Aurl\s*=\s*/i, '')
|
|
140
|
+
.gsub(/['"]/, '')
|
|
141
|
+
.strip
|
|
142
|
+
return nil if url_part.empty?
|
|
143
|
+
|
|
144
|
+
absolute_url(base_uri, url_part)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def valid_url?(url)
|
|
150
|
+
uri = URI.parse(url.to_s)
|
|
151
|
+
uri.is_a?(URI::HTTP)
|
|
152
|
+
rescue URI::InvalidURIError
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
|
|
86
156
|
def absolute_url(base_uri, location)
|
|
87
157
|
URI.join(base_uri.to_s, location).to_s
|
|
88
158
|
end
|
|
159
|
+
|
|
160
|
+
def positive_int(value, default)
|
|
161
|
+
return default if value.nil?
|
|
162
|
+
v = value.to_i
|
|
163
|
+
v.positive? ? v : default
|
|
164
|
+
end
|
|
89
165
|
end
|
|
90
166
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: final_redirect_url
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chandan Jhunjhunwal
|
|
@@ -10,6 +10,20 @@ bindir: exe
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: nokogiri
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: bundler
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,8 +80,9 @@ dependencies:
|
|
|
66
80
|
- - "~>"
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
82
|
version: '3.0'
|
|
69
|
-
description: Follows HTTP redirects (including
|
|
70
|
-
ports) to resolve the final destination URL.
|
|
83
|
+
description: Follows HTTP/HTTPS redirects (including meta-refresh, cookie-gated redirects,
|
|
84
|
+
relative paths, and non-standard ports) to resolve the final destination URL. Provides
|
|
85
|
+
a trace method for full hop inspection.
|
|
71
86
|
email:
|
|
72
87
|
- chandan.jhun@gmail.com
|
|
73
88
|
executables: []
|