Skip to content

Commit 995ce03

Browse files
committed
.
1 parent d3f5899 commit 995ce03

File tree

19 files changed

+521
-538
lines changed

19 files changed

+521
-538
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,6 @@ jobs:
7474
- name: Build frontend
7575
run: npm run build
7676

77-
- name: Verify build output
78-
run: |
79-
grep -q "html2rss-web" dist/index.html
80-
grep -q "Feed Gallery" dist/gallery/index.html
81-
8277
docker-test:
8378
if: github.event_name == 'push'
8479
needs:

app.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ def development? = self.class.development?
4242
opts[:check_dynamic_arity] = false
4343
opts[:check_arity] = :warn
4444
use Rack::Cache, metastore: 'file:./tmp/rack-cache-meta', entitystore: 'file:./tmp/rack-cache-body',
45-
verbose: false
45+
verbose: development?
4646

4747
plugin :content_security_policy do |csp|
4848
csp.default_src :none
4949
csp.style_src :self, "'unsafe-inline'"
5050
csp.script_src :self, "'unsafe-inline'"
5151
csp.connect_src :self
52-
csp.img_src :self, 'data:', 'blob:'
52+
csp.img_src :self
5353
csp.font_src :self, 'data:'
5454
csp.form_action :self
5555
csp.base_uri :none

app/account_manager.rb

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,38 @@ module Web
77
##
88
# Account management functionality
99
module AccountManager
10-
module_function
10+
class << self
11+
# @param token [String]
12+
# @return [Hash, nil]
13+
def get_account(token)
14+
return nil unless token && token_index.key?(token)
1115

12-
# @param token [String]
13-
# @return [Hash, nil]
14-
def get_account(token)
15-
return nil unless token && token_index.key?(token)
16+
token_index[token]
17+
end
1618

17-
token_index[token]
18-
end
19+
# @return [Array<Hash>]
20+
def accounts
21+
@accounts ||= begin # rubocop:disable ThreadSafety/ClassInstanceVariable
22+
auth_config = LocalConfig.global[:auth]
1923

20-
# @return [Hash] token to account mapping
21-
def token_index
22-
@token_index ||= build_token_index # rubocop:disable ThreadSafety/ClassInstanceVariable
23-
end
24+
auth_config&.dig(:accounts) ? auth_config[:accounts].transform_keys(&:to_sym) : []
25+
end
26+
end
2427

25-
# @return [Hash]
26-
def build_token_index
27-
accounts.each_with_object({}) { |account, hash| hash[account[:token]] = account }
28-
end
28+
# @param username [String]
29+
# @return [Hash, nil]
30+
def get_account_by_username(username)
31+
return nil unless username
2932

30-
# @return [Array<Hash>]
31-
def accounts
32-
auth_config = LocalConfig.global[:auth]
33-
return [] unless auth_config&.dig(:accounts)
34-
35-
auth_config[:accounts].map do |account|
36-
{
37-
username: account[:username].to_s,
38-
token: account[:token].to_s,
39-
allowed_urls: Array(account[:allowed_urls]).map(&:to_s)
40-
}
33+
accounts.find { |account| account[:username] == username }
4134
end
42-
end
4335

44-
# @param username [String]
45-
# @return [Hash, nil]
46-
def get_account_by_username(username)
47-
return nil unless username
36+
private
4837

49-
accounts.find { |account| account[:username] == username }
38+
# @return [Hash] token to account mapping
39+
def token_index
40+
@token_index ||= accounts.each_with_object({}) { |account, hash| hash[account[:token]] = account } # rubocop:disable ThreadSafety/ClassInstanceVariable
41+
end
5042
end
5143
end
5244
end

app/api/v1/strategies.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ def index(_request)
1616
{
1717
id: name.to_s,
1818
name: name.to_s,
19-
display_name: name.to_s.split('_').map(&:capitalize).join(' '),
20-
available: true
19+
display_name: name.to_s.split('_').map(&:capitalize).join(' ')
2120
}
2221
end
2322

app/auth_utils.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,24 @@ module Web
88
##
99
# Authentication utility functions
1010
module AuthUtils
11-
module_function
11+
class << self
12+
# @param username [String]
13+
# @param url [String]
14+
# @param token [String]
15+
# @return [String] 16-character hex feed ID
16+
def generate_feed_id(username, url, token)
17+
content = "#{username}:#{url}:#{token}"
18+
Digest::SHA256.hexdigest(content)[0..15]
19+
end
1220

13-
# @param username [String]
14-
# @param url [String]
15-
# @param token [String]
16-
# @return [String] 16-character hex feed ID
17-
def generate_feed_id(username, url, token)
18-
content = "#{username}:#{url}:#{token}"
19-
Digest::SHA256.hexdigest(content)[0..15]
20-
end
21-
22-
# Escapes XML special characters to prevent injection attacks
23-
# @param text [String]
24-
# @return [String]
25-
def sanitize_xml(text)
26-
return '' unless text
21+
# Escapes XML special characters to prevent injection attacks
22+
# @param text [String]
23+
# @return [String]
24+
def sanitize_xml(text)
25+
return '' unless text
2726

28-
CGI.escapeHTML(text.to_s)
27+
CGI.escapeHTML(text.to_s)
28+
end
2929
end
3030
end
3131
end

app/auto_source.rb

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,68 +11,70 @@ module Web
1111
##
1212
# Auto source functionality for generating RSS feeds from any website
1313
module AutoSource
14-
module_function
15-
16-
# @return [Boolean]
17-
def enabled?
18-
if EnvironmentValidator.development?
19-
ENV.fetch('AUTO_SOURCE_ENABLED', nil) != 'false'
20-
else
21-
ENV.fetch('AUTO_SOURCE_ENABLED', nil) == 'true'
14+
class << self
15+
# @return [Boolean]
16+
def enabled?
17+
if EnvironmentValidator.development?
18+
ENV.fetch('AUTO_SOURCE_ENABLED', nil) != 'false'
19+
else
20+
ENV.fetch('AUTO_SOURCE_ENABLED', nil) == 'true'
21+
end
2222
end
23-
end
2423

25-
def create_stable_feed(name, url, token_data, strategy = 'ssrf_filter')
26-
return nil unless url_allowed_for_token?(token_data, url)
24+
def create_stable_feed(name, url, token_data, strategy = 'ssrf_filter')
25+
return nil unless url_allowed_for_token?(token_data, url)
2726

28-
feed_id = AuthUtils.generate_feed_id(token_data[:username], url, token_data[:token])
29-
feed_token = Auth.generate_feed_token(token_data[:username], url)
30-
return nil unless feed_token
27+
feed_id = AuthUtils.generate_feed_id(token_data[:username], url, token_data[:token])
28+
feed_token = Auth.generate_feed_token(token_data[:username], url)
29+
return nil unless feed_token
3130

32-
identifiers = { feed_id: feed_id, feed_token: feed_token }
31+
identifiers = { feed_id: feed_id, feed_token: feed_token }
3332

34-
build_feed_data(name, url, token_data, strategy, identifiers)
35-
end
33+
build_feed_data(name, url, token_data, strategy, identifiers)
34+
end
3635

37-
# @param token_data [Hash]
38-
# @param url [String]
39-
# @return [Boolean]
40-
def url_allowed_for_token?(token_data, url)
41-
account = AccountManager.get_account_by_username(token_data[:username])
42-
return false unless account
36+
def generate_feed_from_stable_id(feed_id, token_data)
37+
return nil unless token_data
4338

44-
UrlValidator.url_allowed?(account, url)
45-
end
39+
# Reconstruct feed data from token and feed_id
40+
# Stateless operation
41+
{
42+
id: feed_id,
43+
url: nil, # Will be provided in request
44+
username: token_data[:username],
45+
strategy: 'ssrf_filter'
46+
}
47+
end
4648

47-
def generate_feed_from_stable_id(feed_id, token_data)
48-
return nil unless token_data
49+
def generate_feed_content(url, strategy = 'ssrf_filter')
50+
feed_content = FeedGenerator.call_strategy(url, strategy)
51+
FeedGenerator.process_feed_content(url, strategy, feed_content)
52+
end
4953

50-
# Reconstruct feed data from token and feed_id
51-
# Stateless operation
52-
{
53-
id: feed_id,
54-
url: nil, # Will be provided in request
55-
username: token_data[:username],
56-
strategy: 'ssrf_filter'
57-
}
58-
end
54+
private
5955

60-
def generate_feed_content(url, strategy = 'ssrf_filter')
61-
feed_content = FeedGenerator.call_strategy(url, strategy)
62-
FeedGenerator.process_feed_content(url, strategy, feed_content)
63-
end
56+
# @param token_data [Hash]
57+
# @param url [String]
58+
# @return [Boolean]
59+
def url_allowed_for_token?(token_data, url)
60+
account = AccountManager.get_account_by_username(token_data[:username])
61+
return false unless account
6462

65-
def build_feed_data(name, url, token_data, strategy, identifiers)
66-
public_url = "/api/v1/feeds/#{identifiers[:feed_token]}"
63+
UrlValidator.url_allowed?(account, url)
64+
end
65+
66+
def build_feed_data(name, url, token_data, strategy, identifiers)
67+
public_url = "/api/v1/feeds/#{identifiers[:feed_token]}"
6768

68-
{
69-
id: identifiers[:feed_id],
70-
name: name,
71-
url: url,
72-
username: token_data[:username],
73-
strategy: strategy,
74-
public_url: public_url
75-
}
69+
{
70+
id: identifiers[:feed_id],
71+
name: name,
72+
url: url,
73+
username: token_data[:username],
74+
strategy: strategy,
75+
public_url: public_url
76+
}
77+
end
7678
end
7779
end
7880
end

0 commit comments

Comments
 (0)