Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,24 @@ services:
# AUTO_SOURCE_ALLOWED_ORIGINS: 127.0.0.1:3000
## to allow multiple origins, seperate those via comma:
# AUTO_SOURCE_ALLOWED_ORIGINS: example.com,h2r.host.tld
BROWSERLESS_IO_WEBSOCKET_URL: ws://browserless:3001
BROWSERLESS_IO_API_TOKEN: 6R0W53R135510

watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- "~/.docker/config.json:/config.json"
command: --cleanup --interval 7200

browserless:
image: "ghcr.io/browserless/chromium"
ports:
- "3001:3001"
environment:
PORT: 3001
CONCURRENT: 10
TOKEN: 6R0W53R135510
```

Start it up with: `docker compose up`.
Expand Down
9 changes: 9 additions & 0 deletions app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
require 'rack/cache'
require_relative 'roda/roda_plugins/basic_auth'

require 'html2rss'
require_relative 'app/ssrf_filter_strategy'

module Html2rss
module Web
##
Expand All @@ -13,6 +16,10 @@ module Web
class App < Roda
CONTENT_TYPE_RSS = 'application/xml'

Html2rss::RequestService.register_strategy(:ssrf_filter, SsrfFilterStrategy)
Html2rss::RequestService.default_strategy_name = :ssrf_filter
Html2rss::RequestService.unregister_strategy(:faraday)

def self.development? = ENV['RACK_ENV'] == 'development'

opts[:check_dynamic_arity] = false
Expand Down Expand Up @@ -64,6 +71,8 @@ def self.development? = ENV['RACK_ENV'] == 'development'
end
end

@show_backtrace = !ENV['CI'].to_s.empty? || development?

route do |r|
r.public
r.hash_branches('')
Expand Down
23 changes: 23 additions & 0 deletions app/ssrf_filter_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

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

module Html2rss
module Web
##
# Strategy to fetch a URL using the SSRF filter.
class SsrfFilterStrategy < Html2rss::RequestService::Strategy
def execute
headers = LocalConfig.global.fetch(:headers, {}).merge(
ctx.headers.transform_keys(&:to_sym)
)
response = SsrfFilter.get(ctx.url, headers:)

Html2rss::RequestService::Response.new(body: response.body,
headers: response.to_hash.transform_values(&:first))
end
end
end
end
15 changes: 0 additions & 15 deletions helpers/auto_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
require 'addressable'
require 'base64'
require 'html2rss'
require 'ssrf_filter'

module Html2rss
module Web
Expand All @@ -20,20 +19,6 @@ def self.allowed_origins = ENV.fetch('AUTO_SOURCE_ALLOWED_ORIGINS', '')
.reject(&:empty?)
.to_set

# @param encoded_url [String] Base64 encoded URL
# @return [RSS::Rss]
def self.build_auto_source_from_encoded_url(encoded_url)
url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
request = SsrfFilter.get(url, headers: LocalConfig.global.fetch(:headers, {}))
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

# @param rss [RSS::Rss]
# @param default_in_minutes [Integer]
# @return [Integer]
Expand Down
33 changes: 26 additions & 7 deletions public/auto_source.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
flex-direction: column;
}

.auto_source__wrapper {
display: flex;
flex-direction: column;
margin: 0 0 1em;
background: var(--background-alt);
padding: 0.75em 1em;
border-radius: 1em;
}
.auto_source input,
.auto_source button {
width: 100%;
Expand All @@ -14,27 +22,38 @@
flex-direction: column;
}

.auto_source > form input[type="submit"] {
flex: 1 1;
margin-right: 0;
.auto_source > form label {
display: flex;
justify-content: center;
align-items: center;
}

.auto_source > form label > span {
flex: 1;
}

.auto_source > form label > input {
flex: 0;
}

.auto_source > nav {
.auto_source nav {
display: flex;
flex-direction: column;
}

@media screen and (min-width: 768px) {
.auto_source > nav {
.auto_source nav {
justify-content: space-between;
flex-direction: row;
}
}

.auto_source > nav button {
.auto_source nav button {
margin-left: 0.25em;
margin-right: 0;
flex: 1;
font-size: 0.9em;
padding: 0.75em;
}

.auto_source__bookmarklet {
Expand All @@ -49,7 +68,7 @@
}

.auto_source iframe {
margin-top: 2em;
margin-top: 1em;
border: 2px groove transparent;
border-radius: 0.5em;
display: none; /* Hide by default */
Expand Down
42 changes: 30 additions & 12 deletions public/auto_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,19 @@ const autoSource = (function () {
initEventListeners() {
// Event listener for URL input change
this.urlInput.addEventListener("change", () => this.clearRssUrl());
this.urlInput.addEventListener("blur", (event) => this.handleFormSubmit(event));

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

const $radios = this.form?.querySelectorAll('input[type="radio"]');
Array.from($radios).forEach(($radio) => {
$radio.addEventListener("change", (event) => this.handleFormSubmit(event));
});

// Event listener for RSS URL input focus
this.rssUrlInput.addEventListener("focus", () => {
const strippedIframeSrc = this.iframe.src.replace("#items", "").trim();
const strippedIframeSrc = this.iframe.src.trim();
if (this.rssUrlInput.value.trim() !== strippedIframeSrc) {
this.updateIframeSrc(this.rssUrlInput.value.trim());
}
Expand Down Expand Up @@ -93,13 +99,20 @@ const autoSource = (function () {

if (this.isValidUrl(url)) {
const encodedUrl = this.encodeUrl(url);
const autoSourceUrl = this.generateAutoSourceUrl(encodedUrl);
const params = {};
const strategy = this.form?.querySelector('input[name="strategy"]:checked')?.value;
if (strategy) {
params["strategy"] = strategy;
}

const autoSourceUrl = this.generateAutoSourceUrl(encodedUrl, params);

this.rssUrlInput.value = autoSourceUrl;
this.rssUrlInput.select();

if (window.location.search !== `?url=${url}`) {
window.history.pushState({}, "", `?url=${url}`);
const targetSearch = `?url=${url}&strategy=${strategy}`;
if (window.location.search !== targetSearch) {
window.history.pushState({}, "", targetSearch);
}
}
}
Expand Down Expand Up @@ -132,17 +145,20 @@ const autoSource = (function () {
* @param {string} encodedUrl - The base64 encoded URL.
* @returns {string} The generated auto-source URL.
*/
generateAutoSourceUrl(encodedUrl) {
generateAutoSourceUrl(encodedUrl, params = {}) {
const baseUrl = new URL(window.location.origin);
return `${baseUrl}${BASE_PATH}/${encodedUrl}`;

const url = new URL(`${baseUrl}${BASE_PATH}/${encodedUrl}`);
url.search = new URLSearchParams(params).toString();
return url.toString();
}

/**
* Updates the iframe source.
* @param {string} rssUrlValue - The RSS URL value.
*/
updateIframeSrc(rssUrlValue) {
this.iframe.src = rssUrlValue === "" ? "" : `${rssUrlValue}#items`;
this.iframe.src = rssUrlValue === "" ? "about://blank" : `${rssUrlValue}`;
}
}

Expand Down Expand Up @@ -187,7 +203,7 @@ const autoSource = (function () {
*/
async copyText() {
try {
const textToCopy = this.rssUrlField.value;
const textToCopy = this.rssUrlWithAuth;
await navigator.clipboard.writeText(textToCopy);
} catch (error) {
console.error("Failed to copy text to clipboard:", error);
Expand All @@ -198,7 +214,7 @@ const autoSource = (function () {
* Opens the link specified in the text field.
*/
openLink() {
const linkToOpen = this.rssUrlField?.value;
const linkToOpen = this.rssUrlWithAuth;

if (typeof linkToOpen === "string" && linkToOpen.trim() !== "") {
window.open(linkToOpen, "_blank", "noopener,noreferrer");
Expand All @@ -209,6 +225,10 @@ const autoSource = (function () {
* Subscribes to the feed specified in the text field.
*/
async subscribeToFeed() {
window.open(this.rssUrlWithAuth);
}

get rssUrlWithAuth() {
const feedUrl = this.rssUrlField.value;
const storedUser = LocalStorageFacade.getOrAsk("username");
const storedPassword = LocalStorageFacade.getOrAsk("password");
Expand All @@ -217,9 +237,7 @@ const autoSource = (function () {
url.username = storedUser;
url.password = storedPassword;

const feedUrlWithAuth = `feed:${url.toString()}`;

window.open(feedUrlWithAuth);
return `feed:${url.toString()}`;
}

resetCredentials() {
Expand Down
11 changes: 11 additions & 0 deletions public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ html {

body {
scroll-behavior: smooth;
margin: 0 auto;
}

/* General Styles */

h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1rem 0;
}

label {
font-weight: bold;
cursor: pointer;
Expand Down
25 changes: 21 additions & 4 deletions routes/auto_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

require_relative '../app/http_cache'
require_relative '../helpers/auto_source'
require 'html2rss'

module Html2rss
module Web
class App
# rubocop:disable Metrics/BlockLength
hash_branch 'auto_source' do |r|
with_basic_auth(realm: 'Auto Source',
username: AutoSource.username,
Expand All @@ -18,15 +20,29 @@ class App
end

r.on String, method: :get do |encoded_url|
rss = AutoSource.build_auto_source_from_encoded_url(encoded_url)
strategy = (request.params['strategy'] || :ssrf_filter).to_sym
unless Html2rss::RequestService.strategy_registered?(strategy)
raise Html2rss::RequestService::UnknownStrategy
end

response['Content-Type'] = CONTENT_TYPE_RSS

url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
rss = Html2rss.auto_source(url, strategy:)

# Unfortunately, Ruby's rss gem does not provide a direct method to
# add an XML stylesheet to the RSS::RSS object itself.
stylesheet = Html2rss::RssBuilder::Stylesheet.new(href: '/rss.xsl', type: 'text/xsl').to_xml

xml_content = rss.to_xml
xml_content.sub!(/^<\?xml version="1.0" encoding="UTF-8"\?>/,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n#{stylesheet}")

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

response['Content-Type'] = CONTENT_TYPE_RSS

rss.to_s
xml_content
end
else
# auto_source feature is disabled
Expand All @@ -37,6 +53,7 @@ class App
end
end
end
# rubocop:enable Metrics/BlockLength
end
end
end
Loading