Skip to content

Commit b7516fd

Browse files
authored
feat: integrate request_service and use ssrf_filter strategy by default (#707)
* feat: integrate request_service and use ssrf_filter strategy by default Signed-off-by: Gil Desmarais <[email protected]> * feat: add strategy selection to form * feat(auto_source): enhance UI and improve form handling * docs(readme): add browserless to docker-compose example --------- Signed-off-by: Gil Desmarais <[email protected]>
1 parent 77dbd3f commit b7516fd

File tree

13 files changed

+232
-104
lines changed

13 files changed

+232
-104
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,24 @@ services:
5656
# AUTO_SOURCE_ALLOWED_ORIGINS: 127.0.0.1:3000
5757
## to allow multiple origins, seperate those via comma:
5858
# AUTO_SOURCE_ALLOWED_ORIGINS: example.com,h2r.host.tld
59+
BROWSERLESS_IO_WEBSOCKET_URL: ws://browserless:3001
60+
BROWSERLESS_IO_API_TOKEN: 6R0W53R135510
61+
5962
watchtower:
6063
image: containrrr/watchtower
6164
volumes:
6265
- /var/run/docker.sock:/var/run/docker.sock
6366
- "~/.docker/config.json:/config.json"
6467
command: --cleanup --interval 7200
68+
69+
browserless:
70+
image: "ghcr.io/browserless/chromium"
71+
ports:
72+
- "3001:3001"
73+
environment:
74+
PORT: 3001
75+
CONCURRENT: 10
76+
TOKEN: 6R0W53R135510
6577
```
6678
6779
Start it up with: `docker compose up`.

app.rb

Lines changed: 9 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_name = :ssrf_filter
21+
Html2rss::RequestService.unregister_strategy(:faraday)
22+
1623
def self.development? = ENV['RACK_ENV'] == 'development'
1724

1825
opts[:check_dynamic_arity] = false
@@ -64,6 +71,8 @@ def self.development? = ENV['RACK_ENV'] == 'development'
6471
end
6572
end
6673

74+
@show_backtrace = !ENV['CI'].to_s.empty? || development?
75+
6776
route do |r|
6877
r.public
6978
r.hash_branches('')

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+
response = SsrfFilter.get(ctx.url, headers:)
17+
18+
Html2rss::RequestService::Response.new(body: response.body,
19+
headers: response.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]

public/auto_source.css

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
flex-direction: column;
55
}
66

7+
.auto_source__wrapper {
8+
display: flex;
9+
flex-direction: column;
10+
margin: 0 0 1em;
11+
background: var(--background-alt);
12+
padding: 0.75em 1em;
13+
border-radius: 1em;
14+
}
715
.auto_source input,
816
.auto_source button {
917
width: 100%;
@@ -14,27 +22,38 @@
1422
flex-direction: column;
1523
}
1624

17-
.auto_source > form input[type="submit"] {
18-
flex: 1 1;
19-
margin-right: 0;
25+
.auto_source > form label {
26+
display: flex;
27+
justify-content: center;
28+
align-items: center;
29+
}
30+
31+
.auto_source > form label > span {
32+
flex: 1;
33+
}
34+
35+
.auto_source > form label > input {
36+
flex: 0;
2037
}
2138

22-
.auto_source > nav {
39+
.auto_source nav {
2340
display: flex;
2441
flex-direction: column;
2542
}
2643

2744
@media screen and (min-width: 768px) {
28-
.auto_source > nav {
45+
.auto_source nav {
2946
justify-content: space-between;
3047
flex-direction: row;
3148
}
3249
}
3350

34-
.auto_source > nav button {
51+
.auto_source nav button {
3552
margin-left: 0.25em;
3653
margin-right: 0;
3754
flex: 1;
55+
font-size: 0.9em;
56+
padding: 0.75em;
3857
}
3958

4059
.auto_source__bookmarklet {
@@ -49,7 +68,7 @@
4968
}
5069

5170
.auto_source iframe {
52-
margin-top: 2em;
71+
margin-top: 1em;
5372
border: 2px groove transparent;
5473
border-radius: 0.5em;
5574
display: none; /* Hide by default */

public/auto_source.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,19 @@ const autoSource = (function () {
4747
initEventListeners() {
4848
// Event listener for URL input change
4949
this.urlInput.addEventListener("change", () => this.clearRssUrl());
50+
this.urlInput.addEventListener("blur", (event) => this.handleFormSubmit(event));
5051

5152
// Event listener for form submit
5253
this.form.addEventListener("submit", (event) => this.handleFormSubmit(event));
5354

55+
const $radios = this.form?.querySelectorAll('input[type="radio"]');
56+
Array.from($radios).forEach(($radio) => {
57+
$radio.addEventListener("change", (event) => this.handleFormSubmit(event));
58+
});
59+
5460
// Event listener for RSS URL input focus
5561
this.rssUrlInput.addEventListener("focus", () => {
56-
const strippedIframeSrc = this.iframe.src.replace("#items", "").trim();
62+
const strippedIframeSrc = this.iframe.src.trim();
5763
if (this.rssUrlInput.value.trim() !== strippedIframeSrc) {
5864
this.updateIframeSrc(this.rssUrlInput.value.trim());
5965
}
@@ -93,13 +99,20 @@ const autoSource = (function () {
9399

94100
if (this.isValidUrl(url)) {
95101
const encodedUrl = this.encodeUrl(url);
96-
const autoSourceUrl = this.generateAutoSourceUrl(encodedUrl);
102+
const params = {};
103+
const strategy = this.form?.querySelector('input[name="strategy"]:checked')?.value;
104+
if (strategy) {
105+
params["strategy"] = strategy;
106+
}
107+
108+
const autoSourceUrl = this.generateAutoSourceUrl(encodedUrl, params);
97109

98110
this.rssUrlInput.value = autoSourceUrl;
99111
this.rssUrlInput.select();
100112

101-
if (window.location.search !== `?url=${url}`) {
102-
window.history.pushState({}, "", `?url=${url}`);
113+
const targetSearch = `?url=${url}&strategy=${strategy}`;
114+
if (window.location.search !== targetSearch) {
115+
window.history.pushState({}, "", targetSearch);
103116
}
104117
}
105118
}
@@ -132,17 +145,20 @@ const autoSource = (function () {
132145
* @param {string} encodedUrl - The base64 encoded URL.
133146
* @returns {string} The generated auto-source URL.
134147
*/
135-
generateAutoSourceUrl(encodedUrl) {
148+
generateAutoSourceUrl(encodedUrl, params = {}) {
136149
const baseUrl = new URL(window.location.origin);
137-
return `${baseUrl}${BASE_PATH}/${encodedUrl}`;
150+
151+
const url = new URL(`${baseUrl}${BASE_PATH}/${encodedUrl}`);
152+
url.search = new URLSearchParams(params).toString();
153+
return url.toString();
138154
}
139155

140156
/**
141157
* Updates the iframe source.
142158
* @param {string} rssUrlValue - The RSS URL value.
143159
*/
144160
updateIframeSrc(rssUrlValue) {
145-
this.iframe.src = rssUrlValue === "" ? "" : `${rssUrlValue}#items`;
161+
this.iframe.src = rssUrlValue === "" ? "about://blank" : `${rssUrlValue}`;
146162
}
147163
}
148164

@@ -187,7 +203,7 @@ const autoSource = (function () {
187203
*/
188204
async copyText() {
189205
try {
190-
const textToCopy = this.rssUrlField.value;
206+
const textToCopy = this.rssUrlWithAuth;
191207
await navigator.clipboard.writeText(textToCopy);
192208
} catch (error) {
193209
console.error("Failed to copy text to clipboard:", error);
@@ -198,7 +214,7 @@ const autoSource = (function () {
198214
* Opens the link specified in the text field.
199215
*/
200216
openLink() {
201-
const linkToOpen = this.rssUrlField?.value;
217+
const linkToOpen = this.rssUrlWithAuth;
202218

203219
if (typeof linkToOpen === "string" && linkToOpen.trim() !== "") {
204220
window.open(linkToOpen, "_blank", "noopener,noreferrer");
@@ -209,6 +225,10 @@ const autoSource = (function () {
209225
* Subscribes to the feed specified in the text field.
210226
*/
211227
async subscribeToFeed() {
228+
window.open(this.rssUrlWithAuth);
229+
}
230+
231+
get rssUrlWithAuth() {
212232
const feedUrl = this.rssUrlField.value;
213233
const storedUser = LocalStorageFacade.getOrAsk("username");
214234
const storedPassword = LocalStorageFacade.getOrAsk("password");
@@ -217,9 +237,7 @@ const autoSource = (function () {
217237
url.username = storedUser;
218238
url.password = storedPassword;
219239

220-
const feedUrlWithAuth = `feed:${url.toString()}`;
221-
222-
window.open(feedUrlWithAuth);
240+
return `feed:${url.toString()}`;
223241
}
224242

225243
resetCredentials() {

public/styles.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,20 @@ html {
1919

2020
body {
2121
scroll-behavior: smooth;
22+
margin: 0 auto;
2223
}
2324

2425
/* General Styles */
26+
27+
h1,
28+
h2,
29+
h3,
30+
h4,
31+
h5,
32+
h6 {
33+
margin: 1rem 0;
34+
}
35+
2536
label {
2637
font-weight: bold;
2738
cursor: pointer;

routes/auto_source.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
require_relative '../app/http_cache'
44
require_relative '../helpers/auto_source'
5+
require 'html2rss'
56

67
module Html2rss
78
module Web
89
class App
10+
# rubocop:disable Metrics/BlockLength
911
hash_branch 'auto_source' do |r|
1012
with_basic_auth(realm: 'Auto Source',
1113
username: AutoSource.username,
@@ -18,15 +20,29 @@ class App
1820
end
1921

2022
r.on String, method: :get do |encoded_url|
21-
rss = AutoSource.build_auto_source_from_encoded_url(encoded_url)
23+
strategy = (request.params['strategy'] || :ssrf_filter).to_sym
24+
unless Html2rss::RequestService.strategy_registered?(strategy)
25+
raise Html2rss::RequestService::UnknownStrategy
26+
end
27+
28+
response['Content-Type'] = CONTENT_TYPE_RSS
29+
30+
url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
31+
rss = Html2rss.auto_source(url, strategy:)
32+
33+
# Unfortunately, Ruby's rss gem does not provide a direct method to
34+
# add an XML stylesheet to the RSS::RSS object itself.
35+
stylesheet = Html2rss::RssBuilder::Stylesheet.new(href: '/rss.xsl', type: 'text/xsl').to_xml
36+
37+
xml_content = rss.to_xml
38+
xml_content.sub!(/^<\?xml version="1.0" encoding="UTF-8"\?>/,
39+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n#{stylesheet}")
2240

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

27-
response['Content-Type'] = CONTENT_TYPE_RSS
28-
29-
rss.to_s
45+
xml_content
3046
end
3147
else
3248
# auto_source feature is disabled
@@ -37,6 +53,7 @@ class App
3753
end
3854
end
3955
end
56+
# rubocop:enable Metrics/BlockLength
4057
end
4158
end
4259
end

0 commit comments

Comments
 (0)