Skip to content

Commit

Permalink
feat(auto_source): add specs and re-structure files
Browse files Browse the repository at this point in the history
  • Loading branch information
gildesmarais committed Oct 20, 2024
1 parent 574ed4b commit 31a7b4e
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 57 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ group :development do
end

group :test do
gem 'rack-test'
gem 'rspec'
gem 'simplecov', require: false
gem 'vcr'
gem 'webmock'
end
14 changes: 13 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
base64 (0.2.0)
bigdecimal (3.1.8)
byebug (11.1.3)
concurrent-ruby (1.3.4)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
diff-lcs (1.5.1)
docile (1.4.1)
Expand All @@ -26,6 +30,7 @@ GEM
faraday (>= 1, < 3)
faraday-net_http (3.3.0)
net-http
hashdiff (1.1.1)
html2rss (0.14.0)
addressable (~> 2.7)
faraday (> 2.0.1, < 3.0)
Expand Down Expand Up @@ -76,6 +81,8 @@ GEM
rack (3.1.7)
rack-cache (1.17.0)
rack (>= 0.4)
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.7.0)
rack-unreloader (2.1.0)
rainbow (3.1.1)
Expand Down Expand Up @@ -134,14 +141,17 @@ GEM
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
ssrf_filter (1.1.2)
strscan (3.1.0)
thor (1.3.2)
tilt (2.4.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
uri (0.13.1)
vcr (6.2.0)
webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
yard (0.9.36)
zeitwerk (2.6.18)

Expand All @@ -162,6 +172,7 @@ DEPENDENCIES
parallel
puma
rack-cache
rack-test
rack-timeout
rack-unreloader
rake
Expand All @@ -176,6 +187,7 @@ DEPENDENCIES
ssrf_filter
tilt
vcr
webmock
yard

BUNDLED WITH
Expand Down
49 changes: 49 additions & 0 deletions helpers/auto_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require 'addressable'
require 'base64'
require 'html2rss'
require 'ssrf_filter'

module Html2rss
module Web
##
# Helper methods for handling auto source feature.
class AutoSource
def self.enabled? = ENV['AUTO_SOURCE_ENABLED'].to_s == 'true'
def self.username = ENV.fetch('AUTO_SOURCE_USERNAME')
def self.password = ENV.fetch('AUTO_SOURCE_PASSWORD')
def self.allowed_origins = ENV.fetch('AUTO_SOURCE_ALLOWED_ORIGINS', '').split(',').to_set

def self.build_auto_source_from_encoded_url(encoded_url)
url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
request = SsrfFilter.get(url)
headers = request.to_hash.transform_values(&:first)

auto_source = Html2rss::AutoSource.new(url, body: request.body, headers:)

auto_source.channel.stylesheets << Html2rss::RssBuilder::Stylesheet.new(href: '/rss.xsl', type: 'text/xsl')

auto_source.build
end

def self.ttl_in_seconds(rss, default_in_minutes: 60)
(rss.channel.ttl || default_in_minutes) * 60
end

def self.check_request_origin!(request, response, allowed_origins = AutoSource.allowed_origins)
if allowed_origins.empty?
response.write 'No allowed origins are configured. Please set AUTO_SOURCE_ALLOWED_ORIGINS.'
else
origin = Set[request.env['HTTP_HOST'], request.env['HTTP_X_FORWARDED_HOST']].delete(nil)
return if allowed_origins.intersect?(origin)

response.write "Origin '#{origin}' is not allowed."
end

response.status = 403
request.halt
end
end
end
end
3 changes: 3 additions & 0 deletions helpers/handle_error.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# frozen_string_literal: true

require 'html2rss/configs'
require_relative '../app/local_config'

module Html2rss
module Web
class App
Expand Down
72 changes: 17 additions & 55 deletions routes/auto_source.rb
Original file line number Diff line number Diff line change
@@ -1,77 +1,39 @@
# frozen_string_literal: true

require 'addressable'
require 'base64'
require 'ssrf_filter'
require 'html2rss'
require_relative '../app/http_cache'
require_relative '../helpers/auto_source'

module Html2rss
module Web
class App
if ENV['AUTO_SOURCE_ENABLED'].to_s == 'true'
hash_branch 'auto_source' do |r|
with_basic_auth(realm: 'Auto Source',
username: AutoSource.username,
password: AutoSource.password) do
AutoSource.check_request_origin!(request, response)

hash_branch 'auto_source' do |r|
with_basic_auth(realm: 'Auto Source',
username: ENV.fetch('AUTO_SOURCE_USERNAME'),
password: ENV.fetch('AUTO_SOURCE_PASSWORD')) do
if AutoSource.enabled?
r.root do
view 'index', layout: '/layout'
end

r.on String, method: :get do |encoded_url|
assert_allowed_request_origin!
rss = AutoSource.build_auto_source_from_encoded_url(encoded_url)

rss = build_auto_source_from_encoded_url(encoded_url)

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

response['Content-Type'] = CONTENT_TYPE_RSS

rss.to_s
end
end
end

private

def assert_allowed_request_origin!
allowed_origins = ENV.fetch('AUTO_SOURCE_ALLOWED_ORIGINS', '').split(',').to_set

if allowed_origins.empty?
response.write 'No allowed origins are configured. Please set AUTO_SOURCE_ALLOWED_ORIGINS.'
else
origin = request.env['HTTP_HOST']

return if allowed_origins.include?(origin)

response.write "Origin '#{origin}' is not allowed."
end

request.halt
end

def build_auto_source_from_encoded_url(encoded_url)
url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
request = SsrfFilter.get(url)
headers = request.to_hash.transform_values(&:first)

auto_source = Html2rss::AutoSource.new(url, body: request.body, headers:)

auto_source.channel.stylesheets << Html2rss::RssBuilder::Stylesheet.new(href: '/rss.xsl', type: 'text/xsl')

auto_source.build
end

def ttl_in_seconds(rss, default_in_minutes: 60)
(rss.channel.ttl || default_in_minutes) * 60
end

else
# auto_source feature is disabled
hash_branch 'auto_source' do |r|
r.on do
response.status = 403
'The auto source feature is disabled.'
# auto_source feature is disabled
r.on do
response.status = 400
'The auto source feature is disabled.'
end
end
end
end
Expand Down
102 changes: 102 additions & 0 deletions spec/fixtures/vcr_cassettes/auto_source-github-h2r-web.yml

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions spec/html2rss/web/app_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,31 @@

RSpec.describe Html2rss::Web::App do
it { expect(described_class).to be < Roda }

context 'with Rack::Test' do
include Rack::Test::Methods

def app = described_class

it 'responds to /' do
get '/'
expect(last_response).to be_ok
end
end

describe '.development?' do
subject { described_class.development? }

context 'when RACK_ENV is development' do
before { ENV['RACK_ENV'] = 'development' }

it { is_expected.to be true }
end

context 'when RACK_ENV is not development' do
before { ENV['RACK_ENV'] = 'test' }

it { is_expected.to be false }
end
end
end
74 changes: 74 additions & 0 deletions spec/routes/auto_source_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require 'spec_helper'

require_relative '../../app'

describe Html2rss::Web::App do # rubocop:disable RSpec/SpecFilePathFormat
include Rack::Test::Methods
def app = described_class

let(:request_headers) do
{ 'HTTP_HOST' => 'localhost' }
end

let(:username) { 'username' }
let(:password) { 'password' }

before do
allow(Html2rss::Web::AutoSource).to receive_messages(enabled?: true,
username:,
password:,
allowed_origins: Set['localhost'])
end

describe "GET '/auto_source/'" do
context 'without provided basic auth' do
it 'sets header "www-authenticate" in response', :aggregate_failures do
get '/auto_source/', {}, request_headers

expect(last_response.has_header?('www-authenticate')).to be true
expect(last_response).to be_unauthorized
end
end

context 'with provided basic auth' do
it 'responds successfully to /auto_source/', :aggregate_failures do
get '/auto_source/', {},
request_headers.merge('HTTP_AUTHORIZATION' => basic_authorize(username, password))

expect(last_response).to be_ok
expect(last_response.body).to include('Automatically sources') &
include('<iframe loading="lazy"></iframe>')
end
end
end

describe "GET '/auto_source/:encoded_url'" do
context 'with provided basic auth' do
subject(:response) do
VCR.use_cassette('auto_source-github-h2r-web') do
get "/auto_source/#{Base64.urlsafe_encode64('https://github.com/html2rss/html2rss-web')}",
{},
request_headers.merge('HTTP_AUTHORIZATION' => basic_authorize(username, password))
end
end

it 'responds successfully', :aggregate_failures do
expect(response).to be_ok
end

it do
expect(response.body).to start_with '<?xml version="1.0" encoding="UTF-8"?>'
end

it do
expect(response.get_header('cache-control')).to eq 'must-revalidate, private, max-age=0'
end

it 'sets content type' do
expect(response.get_header('content-type')).to eq described_class::CONTENT_TYPE_RSS
end
end
end
end
3 changes: 2 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
end
end

require 'rack/test'
require 'vcr'

VCR.configure do |config|
config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
config.hook_into :faraday
config.hook_into :faraday, :webmock
end

# This file was generated by the `rspec --init` command. Conventionally, all
Expand Down

0 comments on commit 31a7b4e

Please sign in to comment.