open_graph_fetcher 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46670fff3dd827c35482bd63b1c246aa876fd85f82b3fdff752144e4f07e537b
4
- data.tar.gz: a260be55faeb2038d4aeacfa9d419a1b7301ec023d327eb51fc2d5a1992ff260
3
+ metadata.gz: 48a6853f8f4665764d603a012713c1e8a5cd44c9ad649efc7626b2e210bf0931
4
+ data.tar.gz: 53ef6ecb11ab043f4d905a659a834254169721d6018b3a7428f535ceaad635fe
5
5
  SHA512:
6
- metadata.gz: f23ee76f381d04b9fcc1bb60eb2f1baf4a438647009987a072f7a23334008a18614f11bb9a9cf74065e5deb695f65a3d8bed786ea2e6f32137c61a4c14a4ae06
7
- data.tar.gz: 005b86f83da1f16ce55998bcefc1bceb93c0e0137bccab08e72ad1da2e749077fe2a0f5e85cd0bbf6abc005181cb16c2abab01adc1e2fa623344146743d198a7
6
+ metadata.gz: aa81fcbfff70cb71a13c599ceaf4be687e97e38d2889424290fd0b6c7487c967575fae28864d47cc46988d164ed18894df257c2a6838a2c55e7d40d7b07c3318
7
+ data.tar.gz: 75e7edea3e478392eccfd6fdde4f25e8b33d711d539746f09dd63543a15faaaae1dd4fbb621348e3cd7ec4ad7d1a5042dd6b7a88cda35e118203e19adeb25460
data/README.md CHANGED
@@ -3,10 +3,11 @@
3
3
  Fetch Open Graph metadata in a safer way.
4
4
 
5
5
  - Includes some mitigations for SSRF attacks
6
- - Blocks private and local IP ranges
6
+ - Blocks the direct usage of IP addresses in URLs
7
+ - Blocks private and local IP ranges (after DNS resolution)
7
8
  - Avoids TOC/TOU when connecting to the IP
8
9
  - Supports only HTTPS on the standard port (443)
9
- - Includes request timeouts
10
+ - Includes request timeouts (for DNS and HTTP)
10
11
  - Avoids redirects
11
12
  - Allows only text/html responses
12
13
  - Returns only known OG properties and nothing else
data/examples/fetch.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'open_graph_fetcher'
2
+
3
+ url = "https://ogp.me"
4
+ fetcher = OpenGraphFetcher::Fetcher.new(url)
5
+ og_data = fetcher.fetch
6
+ puts og_data
@@ -1,8 +1,10 @@
1
1
  module OpenGraphFetcher
2
2
  class Error < StandardError; end
3
3
 
4
+ class InvalidURIError < Error; end
4
5
  class InvalidSchemeError < Error; end
5
6
  class InvalidPortError < Error; end
7
+ class InvalidHostError < Error; end
6
8
  class IPResolutionError < Error; end
7
9
  class PrivateIPError < Error; end
8
10
  class FetchError < Error; end
@@ -5,8 +5,9 @@ require 'ipaddr'
5
5
 
6
6
  module OpenGraphFetcher
7
7
  class Fetcher
8
- OG_PROPERTIES = %w[title type image url description].freeze
8
+ OG_PROPERTIES = %w[title type image url description site_name].freeze
9
9
 
10
+ DNS_TIMEOUT = 3
10
11
  OPEN_TIMEOUT = 3
11
12
  READ_TIMEOUT = 3
12
13
 
@@ -19,9 +20,10 @@ module OpenGraphFetcher
19
20
  end
20
21
 
21
22
  def fetch
22
- uri = URI.parse(@url)
23
+ uri = parse_uri(@url)
23
24
  raise InvalidSchemeError, "Only HTTPS URLs are allowed" unless uri.scheme == "https"
24
- raise InvalidPortError, "Only the default HTTPS port (443) is allowed" if uri.port && uri.port != 443
25
+ raise InvalidPortError, "Only the default HTTPS port (443) is allowed" unless uri.port == 443
26
+ raise InvalidHostError, "Using an IP as host is not allowed" if ip_address?(uri.hostname)
25
27
 
26
28
  ip_address = resolve_ip(uri)
27
29
  raise PrivateIPError, "Resolved IP address is in a private or reserved range" if private_ip?(ip_address)
@@ -34,12 +36,25 @@ module OpenGraphFetcher
34
36
  end
35
37
 
36
38
  private
39
+
40
+ def parse_uri(url)
41
+ URI.parse(url)
42
+ rescue URI::InvalidURIError => e
43
+ raise InvalidURIError, "Could not parse URI: #{e.message}"
44
+ end
37
45
 
38
46
  def resolve_ip(uri)
39
- Resolv.getaddress(uri.host)
47
+ Resolv::DNS.open do |dns|
48
+ dns.timeouts = DNS_TIMEOUT
49
+ dns.getaddress(uri.hostname).to_s
50
+ end
40
51
  rescue Resolv::ResolvError => e
41
52
  raise IPResolutionError, "Could not resolve IP: #{e.message}"
42
53
  end
54
+
55
+ def ip_address?(host)
56
+ host =~ Resolv::IPv4::Regex || host =~ Resolv::IPv6::Regex
57
+ end
43
58
 
44
59
  def private_ip?(ip)
45
60
  ip_addr = IPAddr.new(ip)
@@ -48,9 +63,12 @@ module OpenGraphFetcher
48
63
 
49
64
  def fetch_data(uri, ip)
50
65
  request = Net::HTTP::Get.new(uri.request_uri)
51
- Net::HTTP.start(uri.hostname, uri.port, ipaddr: ip, use_ssl: true, open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT) do |http|
52
- http.request(request)
53
- end
66
+ http = Net::HTTP.new(uri.hostname, 443)
67
+ http.ipaddr = ip
68
+ http.use_ssl = true
69
+ http.open_timeout = OPEN_TIMEOUT
70
+ http.read_timeout = READ_TIMEOUT
71
+ http.request(request)
54
72
  rescue Net::OpenTimeout, Net::ReadTimeout => e
55
73
  raise FetchError, "Request timed out: #{e.message}"
56
74
  rescue StandardError => e
@@ -1,3 +1,3 @@
1
1
  module OpenGraphFetcher
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
data/spec/fetcher_spec.rb CHANGED
@@ -4,7 +4,7 @@ require 'webmock/rspec'
4
4
  RSpec.describe OpenGraphFetcher::Fetcher do
5
5
 
6
6
  before do
7
- allow(Resolv).to receive(:getaddress).and_return("203.0.113.0")
7
+ allow(Resolv::DNS).to receive(:open).and_return("203.0.113.0")
8
8
  end
9
9
 
10
10
  describe ".fetch" do
@@ -38,6 +38,28 @@ RSpec.describe OpenGraphFetcher::Fetcher do
38
38
  "description" => "Example description"
39
39
  })
40
40
  end
41
+
42
+ it 'sets the ipaddr and other options on the Net::HTTP instance' do
43
+ http_instance = instance_double(Net::HTTP)
44
+ expect(Net::HTTP).to receive(:new).and_return(http_instance)
45
+
46
+ expect(http_instance).to receive(:ipaddr=).with("203.0.113.0")
47
+ expect(http_instance).to receive(:use_ssl=).with(true)
48
+ expect(http_instance).to receive(:open_timeout=).with(3)
49
+ expect(http_instance).to receive(:read_timeout=).with(3)
50
+
51
+ response_double = double("Net::HTTPResponse", code: "200", body: "<!DOCTYPE html><title>OK</title>")
52
+ expect(response_double).to receive(:[]).with("Content-Type").and_return("text/html")
53
+ expect(http_instance).to receive(:request).and_return(response_double)
54
+
55
+ OpenGraphFetcher::Fetcher.fetch("https://example.com")
56
+ end
57
+ end
58
+
59
+ context "when given an invalid URL" do
60
+ it "raises an InvalidURIError" do
61
+ expect { OpenGraphFetcher::Fetcher.fetch("# test") }.to raise_error(OpenGraphFetcher::InvalidURIError)
62
+ end
41
63
  end
42
64
 
43
65
  context "when given an HTTP URL" do
@@ -51,20 +73,26 @@ RSpec.describe OpenGraphFetcher::Fetcher do
51
73
  expect { OpenGraphFetcher::Fetcher.fetch("https://example.com:8443") }.to raise_error(OpenGraphFetcher::InvalidPortError)
52
74
  end
53
75
  end
76
+
77
+ context "when given a URL with an IP address" do
78
+ it "raises an InvalidHostError" do
79
+ expect { OpenGraphFetcher::Fetcher.fetch("https://203.0.113.0/test") }.to raise_error(OpenGraphFetcher::InvalidHostError)
80
+ end
81
+ end
54
82
 
55
83
  context "when the IP address cannot be resolved" do
56
84
  it "raises an IPResolutionError with an appropriate error message" do
57
- allow(Resolv).to receive(:getaddress).and_raise(Resolv::ResolvError, "DNS resolution failed")
85
+ allow(Resolv::DNS).to receive(:open).and_raise(Resolv::ResolvError, "DNS resolution failed")
58
86
 
59
- expect { OpenGraphFetcher::Fetcher.fetch("https://nonexistent-domain.com") }.to raise_error(OpenGraphFetcher::IPResolutionError, /Could not resolve IP: DNS resolution failed/)
87
+ expect { OpenGraphFetcher::Fetcher.fetch("https://nonexistent.example.com") }.to raise_error(OpenGraphFetcher::IPResolutionError, /Could not resolve IP: DNS resolution failed/)
60
88
  end
61
89
  end
62
90
 
63
91
  context "when the resolved IP address is private" do
64
92
  it "raises a PrivateIPError" do
65
- allow(Resolv).to receive(:getaddress).with("10.0.0.1").and_return("10.0.0.1")
93
+ allow(Resolv::DNS).to receive(:open).and_return("10.0.0.1")
66
94
 
67
- expect { OpenGraphFetcher::Fetcher.fetch("https://10.0.0.1") }.to raise_error(OpenGraphFetcher::PrivateIPError)
95
+ expect { OpenGraphFetcher::Fetcher.fetch("https://ssrf.example.com") }.to raise_error(OpenGraphFetcher::PrivateIPError)
68
96
  end
69
97
  end
70
98
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open_graph_fetcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Colli
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-04 00:00:00.000000000 Z
11
+ date: 2024-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -52,8 +52,8 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- description:
56
- email:
55
+ description:
56
+ email:
57
57
  executables: []
58
58
  extensions: []
59
59
  extra_rdoc_files: []
@@ -63,6 +63,7 @@ files:
63
63
  - Gemfile
64
64
  - LICENSE
65
65
  - README.md
66
+ - examples/fetch.rb
66
67
  - lib/open_graph_fetcher.rb
67
68
  - lib/open_graph_fetcher/errors.rb
68
69
  - lib/open_graph_fetcher/fetcher.rb
@@ -73,7 +74,7 @@ homepage: https://github.com/collimarco/open_graph_fetcher
73
74
  licenses:
74
75
  - MIT
75
76
  metadata: {}
76
- post_install_message:
77
+ post_install_message:
77
78
  rdoc_options: []
78
79
  require_paths:
79
80
  - lib
@@ -88,8 +89,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
89
  - !ruby/object:Gem::Version
89
90
  version: '0'
90
91
  requirements: []
91
- rubygems_version: 3.5.16
92
- signing_key:
92
+ rubygems_version: 3.0.3.1
93
+ signing_key:
93
94
  specification_version: 4
94
95
  summary: Fetch Open Graph metadata in a safer way.
95
96
  test_files: []