Skip to content

Commit d79e990

Browse files
committed
feat: integrate request_service and use ssrf_filter strategy by default
1 parent 5030b56 commit d79e990

File tree

5 files changed

+78
-18
lines changed

5 files changed

+78
-18
lines changed

app.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
require 'rack/cache'
55
require_relative 'roda/roda_plugins/basic_auth'
66

7+
require 'html2rss'
8+
require_relative 'app/ssrf_filter_strategy'
9+
710
module Html2rss
811
module Web
912
##
@@ -13,6 +16,10 @@ module Web
1316
class App < Roda
1417
CONTENT_TYPE_RSS = 'application/xml'
1518

19+
Html2rss::RequestService.register_strategy(:ssrf_filter, SsrfFilterStrategy)
20+
Html2rss::RequestService.default_strategy = :ssrf_filter
21+
Html2rss::RequestService.unregister_strategy(:faraday)
22+
1623
def self.development? = ENV['RACK_ENV'] == 'development'
1724

1825
opts[:check_dynamic_arity] = false

app/ssrf_filter_strategy.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
require 'ssrf_filter'
4+
require 'html2rss'
5+
require_relative '../app/local_config'
6+
7+
module Html2rss
8+
module Web
9+
##
10+
# Strategy to fetch a URL using the SSRF filter.
11+
class SsrfFilterStrategy < Html2rss::RequestService::Strategy
12+
def execute
13+
headers = LocalConfig.global.fetch(:headers, {}).merge(
14+
ctx.headers.transform_keys(&:to_sym)
15+
)
16+
request = SsrfFilter.get(ctx.url, headers:)
17+
18+
Html2rss::RequestService::Response.new(body: request.body,
19+
headers: request.to_hash.transform_values(&:first))
20+
end
21+
end
22+
end
23+
end

helpers/auto_source.rb

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
require 'addressable'
44
require 'base64'
55
require 'html2rss'
6-
require 'ssrf_filter'
76

87
module Html2rss
98
module Web
@@ -20,20 +19,6 @@ def self.allowed_origins = ENV.fetch('AUTO_SOURCE_ALLOWED_ORIGINS', '')
2019
.reject(&:empty?)
2120
.to_set
2221

23-
# @param encoded_url [String] Base64 encoded URL
24-
# @return [RSS::Rss]
25-
def self.build_auto_source_from_encoded_url(encoded_url)
26-
url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
27-
request = SsrfFilter.get(url, headers: LocalConfig.global.fetch(:headers, {}))
28-
headers = request.to_hash.transform_values(&:first)
29-
30-
auto_source = Html2rss::AutoSource.new(url, body: request.body, headers:)
31-
32-
auto_source.channel.stylesheets << Html2rss::RssBuilder::Stylesheet.new(href: '/rss.xsl', type: 'text/xsl')
33-
34-
auto_source.build
35-
end
36-
3722
# @param rss [RSS::Rss]
3823
# @param default_in_minutes [Integer]
3924
# @return [Integer]

routes/auto_source.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
module Html2rss
77
module Web
88
class App
9+
# rubocop:disable Metrics/BlockLength
910
hash_branch 'auto_source' do |r|
1011
with_basic_auth(realm: 'Auto Source',
1112
username: AutoSource.username,
@@ -18,15 +19,29 @@ class App
1819
end
1920

2021
r.on String, method: :get do |encoded_url|
21-
rss = AutoSource.build_auto_source_from_encoded_url(encoded_url)
22+
strategy = request.params.fetch('strategy', :ssrf_filter).to_sym
23+
unless Html2rss::RequestService.strategy_registered?(strategy)
24+
raise Html2rss::RequestService::UnknownStrategy
25+
end
26+
27+
response['Content-Type'] = CONTENT_TYPE_RSS
28+
29+
url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
30+
rss = Html2rss.auto_source(url, strategy:)
2231

2332
HttpCache.expires response,
2433
AutoSource.ttl_in_seconds(rss),
2534
cache_control: 'private, must-revalidate'
2635

27-
response['Content-Type'] = CONTENT_TYPE_RSS
36+
# Unfortunately, Ruby's rss gem does not provide a direct method to
37+
# add an XML stylesheet to the RSS::RSS object itself.
38+
stylesheet = Html2rss::RssBuilder::Stylesheet.new(href: '/rss.xsl', type: 'text/xsl').to_xml
39+
40+
xml_content = rss.to_xml
41+
xml_content.sub!(/^<\?xml version="1.0" encoding="UTF-8"\?>/,
42+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n#{stylesheet}")
2843

29-
rss.to_s
44+
xml_content
3045
end
3146
else
3247
# auto_source feature is disabled
@@ -37,6 +52,7 @@ class App
3752
end
3853
end
3954
end
55+
# rubocop:enable Metrics/BlockLength
4056
end
4157
end
4258
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require_relative '../../../ssrf_filter_strategy'
5+
6+
RSpec.describe Html2rss::Web::SsrfFilterStrategy do
7+
subject(:instance) { described_class.new(ctx) }
8+
9+
let(:url) { 'http://example.com' }
10+
let(:headers) { { 'User-Agent': 'Mozilla/5.0' } }
11+
let(:ctx) { instance_double(Html2rss::RequestService::Context, url:, headers:) }
12+
13+
describe '#execute' do
14+
before do
15+
allow(SsrfFilter).to receive(:get).with(url, headers:).and_return(
16+
instance_double(Net::HTTPResponse, body: 'body', to_hash: { 'Content-Type' => ['text/html'] })
17+
)
18+
end
19+
20+
it 'returns a response', :aggregate_failures do
21+
response = instance.execute
22+
23+
expect(SsrfFilter).to haved_received(:get).with(url, headers:)
24+
expect(response).to be_a(Html2rss::RequestService::Response)
25+
expect(response.body).to eq('body')
26+
expect(response.headers).to eq('Content-Type' => 'text/html')
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)