Skip to content

Commit 48d3950

Browse files
committed
preact
1 parent 10ed3ed commit 48d3950

35 files changed

+5227
-542
lines changed

.github/workflows/frontend.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,6 @@ jobs:
3636
cd frontend
3737
npm run build
3838
39-
- name: Verify build output
40-
run: |
41-
ls -la frontend/dist/
42-
test -f frontend/dist/index.html
43-
test -f frontend/dist/gallery/index.html
44-
test -f frontend/dist/styles.css
45-
4639
frontend-test:
4740
runs-on: ubuntu-latest
4841
steps:

.vscode/extensions.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"redhat.vscode-yaml",
44
"esbenp.prettier-vscode",
55
"github.copilot",
6-
"github.copilot-chat",
76
"shopify.ruby-lsp"
87
]
98
}

app/auth.rb

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,35 @@ module Auth
2323

2424
##
2525
# Authenticate a request and return account data if valid
26-
# @param request [Roda::Request] request object
26+
# @param request [Rack::Request] request object
2727
# @return [Hash, nil] account data if authenticated
2828
def authenticate(request)
2929
token = extract_token(request)
30-
return nil unless token
30+
return log_auth_failure(request, 'missing_token') unless token
3131

3232
account = get_account(token)
33-
if account
34-
SecurityLogger.log_auth_failure(request.ip, request.user_agent, 'success')
35-
else
36-
SecurityLogger.log_auth_failure(request.ip, request.user_agent, 'invalid_token')
37-
end
33+
return log_auth_success(account, request) if account
34+
35+
log_auth_failure(request, 'invalid_token')
36+
end
37+
38+
##
39+
# Log auth failure and return nil
40+
# @param request [Rack::Request] request object
41+
# @param reason [String] failure reason
42+
# @return [nil]
43+
def log_auth_failure(request, reason)
44+
SecurityLogger.log_auth_failure(request.ip, request.user_agent, reason)
45+
nil
46+
end
47+
48+
##
49+
# Log auth success and return account
50+
# @param account [Hash] account data
51+
# @param request [Rack::Request] request object
52+
# @return [Hash] account data
53+
def log_auth_success(account, request)
54+
SecurityLogger.log_auth_success(account[:username], request.ip)
3855
account
3956
end
4057

@@ -43,7 +60,7 @@ def authenticate(request)
4360
# @param token [String] authentication token
4461
# @return [Hash, nil] account data if found
4562
def get_account(token)
46-
return nil unless token
63+
return nil unless token && token_index.key?(token)
4764

4865
token_index[token]
4966
end
@@ -52,7 +69,14 @@ def get_account(token)
5269
# Get token index for O(1) lookups
5370
# @return [Hash] token to account mapping
5471
def token_index
55-
@token_index ||= accounts.each_with_object({}) { |account, hash| hash[account[:token]] = account } # rubocop:disable ThreadSafety/ClassInstanceVariable
72+
@token_index ||= build_token_index # rubocop:disable ThreadSafety/ClassInstanceVariable
73+
end
74+
75+
##
76+
# Build token index in a thread-safe manner
77+
# @return [Hash] token to account mapping
78+
def build_token_index
79+
accounts.each_with_object({}) { |account, hash| hash[account[:token]] = account }
5680
end
5781

5882
##
@@ -127,7 +151,7 @@ def validate_feed_token(feed_token, url)
127151
return nil unless valid
128152

129153
get_account_by_username(token_data[:payload][:username])
130-
rescue StandardError
154+
rescue JSON::ParserError, ArgumentError
131155
SecurityLogger.log_token_usage(feed_token, url, false)
132156
nil
133157
end
@@ -174,7 +198,7 @@ def token_valid?(token_data, url)
174198
# @param url [String] full URL with query parameters
175199
# @return [String, nil] feed token if found
176200
def extract_feed_token_from_url(url)
177-
URI.parse(url).then { |uri| URI.decode_www_form(uri.query || '').to_h['token'] }
201+
URI.parse(url).then { |uri| CGI.parse(uri.query || '')['token']&.first }
178202
rescue StandardError
179203
nil
180204
end
@@ -193,7 +217,7 @@ def feed_url_allowed?(feed_token, url)
193217

194218
##
195219
# Extract token from request (Authorization header only)
196-
# @param request [Roda::Request] request object
220+
# @param request [Rack::Request] request object
197221
# @return [String, nil] token if found
198222
def extract_token(request)
199223
auth_header = request.env['HTTP_AUTHORIZATION']
@@ -266,7 +290,8 @@ def url_matches_pattern?(url, pattern)
266290
escaped_pattern = Regexp.escape(pattern).gsub('\\*', '.*')
267291
url.match?(/\A#{escaped_pattern}\z/)
268292
else
269-
url.include?(pattern)
293+
# Exact match for non-wildcard patterns
294+
url == pattern
270295
end
271296
end
272297

@@ -282,38 +307,18 @@ def sanitize_xml(text)
282307
end
283308

284309
##
285-
# Validate URL format and scheme using Html2rss::Url.for_channel
310+
# Validate URL format and scheme
286311
# @param url [String] URL to validate
287-
# @return [Boolean] true if URL is valid and allowed
312+
# @return [Boolean] true if URL is valid
288313
def valid_url?(url)
289-
return false unless basic_url_valid?(url)
314+
return false unless url.is_a?(String) && !url.empty? && url.length <= 2048
290315

291-
validate_url_with_html2rss(url)
316+
uri = URI.parse(url)
317+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
292318
rescue StandardError
293319
false
294320
end
295321

296-
##
297-
# Basic URL format validation
298-
# @param url [String] URL to validate
299-
# @return [Boolean] true if basic format is valid
300-
def basic_url_valid?(url)
301-
url.is_a?(String) && !url.empty? && url.length <= 2048 && url.match?(%r{\Ahttps?://.+})
302-
end
303-
304-
##
305-
# Validate URL using Html2rss if available, otherwise basic validation
306-
# @param url [String] URL to validate
307-
# @return [Boolean] true if URL is valid
308-
def validate_url_with_html2rss(url)
309-
if defined?(Html2rss::Url) && Html2rss::Url.respond_to?(:for_channel)
310-
!Html2rss::Url.for_channel(url).nil?
311-
else
312-
# Fallback to basic URL validation for tests
313-
URI.parse(url).is_a?(URI::HTTP) || URI.parse(url).is_a?(URI::HTTPS)
314-
end
315-
end
316-
317322
##
318323
# Validate username format and length
319324
# @param username [String] username to validate

app/roda_config.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def configure_csp_sources(csp)
6666

6767
def configure_csp_security(csp)
6868
csp.frame_ancestors :none # More restrictive than :self
69-
csp.frame_src :none # More restrictive than :self
69+
csp.frame_src :self # Allow iframes for RSS feeds
7070
csp.object_src :none # Prevent object/embed/applet
7171
csp.media_src :none # Prevent media sources
7272
csp.manifest_src :none # Prevent manifest
@@ -85,7 +85,7 @@ def basic_security_headers
8585
'Content-Type' => 'text/html',
8686
'X-Content-Type-Options' => 'nosniff',
8787
'X-XSS-Protection' => '1; mode=block',
88-
'X-Frame-Options' => 'DENY',
88+
'X-Frame-Options' => 'SAMEORIGIN',
8989
'X-Permitted-Cross-Domain-Policies' => 'none',
9090
'Referrer-Policy' => 'strict-origin-when-cross-origin'
9191
}

app/security_logger.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ def log_auth_failure(ip, user_agent, reason)
4747
})
4848
end
4949

50+
##
51+
# Log authentication success
52+
# @param username [String] authenticated username
53+
# @param ip [String] client IP address
54+
def log_auth_success(username, ip)
55+
log_event('auth_success', {
56+
username: username,
57+
ip: ip
58+
})
59+
end
60+
5061
##
5162
# Log rate limit exceeded
5263
# @param ip [String] client IP address

config/feeds.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ auth:
44
token: "allow-any-urls-abcd"
55
allowed_urls:
66
- "*" # Full access
7-
- username: "limited"
8-
token: "limited-urls-token"
7+
- username: "demo"
8+
token: "self-host-for-full-access"
99
allowed_urls:
10-
- "https://example.com/*"
11-
- "https://news.ycombinator.com/*"
10+
- "https://www.chip.de/testberichte"
11+
- "https://news.ycombinator.com"
12+
- "https://github.com/trending"
1213
- username: "health-check"
1314
token: "health-check-token-xyz789"
1415
allowed_urls: [] # Health check doesn't need URL access

frontend/astro.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineConfig } from "astro/config";
22
import starlight from "@astrojs/starlight";
3+
import preact from "@astrojs/preact";
34

45
export default defineConfig({
56
output: "static",
@@ -30,6 +31,7 @@ export default defineConfig({
3031
},
3132
},
3233
integrations: [
34+
preact(),
3335
starlight({
3436
title: "html2rss-web",
3537
description: "Convert websites to RSS feeds instantly",

0 commit comments

Comments
 (0)