Skip to content

Commit 65a7330

Browse files
committed
log for rack-attack
1 parent 0c301f8 commit 65a7330

File tree

12 files changed

+676
-156
lines changed

12 files changed

+676
-156
lines changed

app.rb

Lines changed: 11 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
require 'json'
66

77
require 'html2rss'
8-
require_relative 'app/ssrf_filter_strategy'
8+
require_relative 'app/environment_validator'
9+
require_relative 'app/roda_config'
10+
require_relative 'app/app_routes'
911
require_relative 'app/auth'
1012
require_relative 'app/auto_source'
1113
require_relative 'app/feeds'
@@ -16,6 +18,7 @@
1618
require_relative 'app/xml_builder'
1719
require_relative 'app/auto_source_routes'
1820
require_relative 'app/health_check_routes'
21+
require_relative 'app/security_logger'
1922

2023
module Html2rss
2124
module Web
@@ -33,150 +36,19 @@ class App < Roda
3336
CONTENT_TYPE_RSS = 'application/xml'
3437

3538
def self.development? = ENV['RACK_ENV'] == 'development'
36-
37-
# Validate required environment variables on startup
38-
def self.validate_environment!
39-
return if ENV['HTML2RSS_SECRET_KEY']
40-
41-
if development? || ENV['RACK_ENV'] == 'test'
42-
set_development_key
43-
else
44-
show_production_error
45-
end
46-
end
47-
48-
def self.set_development_key
49-
ENV['HTML2RSS_SECRET_KEY'] = 'development-default-key-not-for-production'
50-
puts '⚠️ WARNING: Using default secret key for development/testing only!'
51-
puts ' Set HTML2RSS_SECRET_KEY environment variable for production use.'
52-
end
53-
54-
def self.show_production_error
55-
puts production_error_message
56-
exit 1
57-
end
58-
59-
def self.production_error_message
60-
<<~ERROR
61-
❌ ERROR: HTML2RSS_SECRET_KEY environment variable is not set!
62-
63-
This application is designed to be used via Docker Compose only.
64-
Please read the project's README.md for setup instructions.
65-
66-
To generate a secure secret key and start the application:
67-
1. Generate a secret key: openssl rand -hex 32
68-
2. Edit docker-compose.yml and replace 'your-generated-secret-key-here' with your key
69-
3. Start with: docker-compose up
70-
71-
For more information, see: https://github.com/html2rss/html2rss-web#configuration
72-
ERROR
73-
end
74-
7539
def development? = self.class.development?
7640

7741
# Validate environment on class load
78-
validate_environment!
79-
80-
Html2rss::RequestService.register_strategy(:ssrf_filter, SsrfFilterStrategy)
81-
Html2rss::RequestService.default_strategy_name = :ssrf_filter
82-
Html2rss::RequestService.unregister_strategy(:faraday)
83-
84-
opts[:check_dynamic_arity] = false
85-
opts[:check_arity] = :warn
86-
87-
use Rack::Cache,
88-
metastore: 'file:./tmp/rack-cache-meta',
89-
entitystore: 'file:./tmp/rack-cache-body',
90-
verbose: false
91-
92-
plugin :content_security_policy do |csp|
93-
csp.default_src :none
94-
csp.style_src :self, "'unsafe-inline'" # Allow inline styles for Starlight
95-
csp.script_src :self, "'unsafe-inline'" # Allow inline scripts for progressive enhancement
96-
csp.connect_src :self
97-
csp.img_src :self, 'data:', 'blob:'
98-
csp.font_src :self, 'data:'
99-
csp.form_action :self
100-
csp.base_uri :none
101-
csp.frame_ancestors :none # More restrictive than :self
102-
csp.frame_src :none # More restrictive than :self
103-
csp.object_src :none # Prevent object/embed/applet
104-
csp.media_src :none # Prevent media sources
105-
csp.manifest_src :none # Prevent manifest
106-
csp.worker_src :none # Prevent workers
107-
csp.child_src :none # Prevent child contexts
108-
csp.block_all_mixed_content
109-
csp.upgrade_insecure_requests # Upgrade HTTP to HTTPS
110-
end
111-
112-
plugin :default_headers,
113-
'Content-Type' => 'text/html',
114-
'X-Content-Type-Options' => 'nosniff',
115-
'X-XSS-Protection' => '1; mode=block',
116-
'X-Frame-Options' => 'DENY',
117-
'X-Permitted-Cross-Domain-Policies' => 'none',
118-
'Referrer-Policy' => 'strict-origin-when-cross-origin',
119-
'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
120-
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload',
121-
'Cross-Origin-Embedder-Policy' => 'require-corp',
122-
'Cross-Origin-Opener-Policy' => 'same-origin',
123-
'Cross-Origin-Resource-Policy' => 'same-origin'
124-
125-
plugin :exception_page
126-
plugin :error_handler do |error|
127-
next exception_page(error) if development?
128-
129-
response.status = 500
130-
response['Content-Type'] = CONTENT_TYPE_RSS
131-
XmlBuilder.build_error_feed(message: error.message)
132-
end
133-
134-
plugin :public
135-
plugin :hash_branches
136-
137-
@show_backtrace = !ENV['CI'].to_s.empty? || development?
138-
139-
# API routes
140-
hash_branch 'api' do |r|
141-
response['Content-Type'] = 'application/json'
142-
143-
r.on 'feeds.json' do
144-
response['Cache-Control'] = 'public, max-age=300'
145-
JSON.generate(Feeds.list_feeds)
146-
end
147-
148-
r.on 'strategies.json' do
149-
response['Cache-Control'] = 'public, max-age=3600'
150-
JSON.generate(ApiRoutes.list_available_strategies)
151-
end
152-
153-
r.on String do |feed_name|
154-
ApiRoutes.handle_feed_generation(r, feed_name)
155-
end
156-
end
157-
158-
# Stable feed routes (new)
159-
hash_branch 'feeds' do |r|
160-
r.on String do |feed_id|
161-
AutoSourceRoutes.handle_stable_feed(r, feed_id)
162-
end
163-
end
42+
EnvironmentValidator.validate_environment!
43+
EnvironmentValidator.validate_production_security!
16444

165-
# Auto source routes
166-
hash_branch 'auto_source' do |r|
167-
handle_auto_source_routes(r)
168-
end
45+
# Configure Roda app
46+
RodaConfig.configure(self)
16947

170-
# Health check route
171-
hash_branch 'health_check.txt' do |r|
172-
handle_health_check_routes(r)
173-
end
48+
@show_backtrace = development? && !ENV['CI']
17449

175-
route do |r|
176-
r.public
177-
r.hash_branches
178-
handle_static_files(r)
179-
end
50+
# Define all routes
51+
AppRoutes.define_routes(self)
18052
end
18153
end
18254
end

app/api_routes.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ module Web
99
module ApiRoutes
1010
module_function
1111

12+
##
13+
# List available request strategies
14+
# @return [Hash] hash with strategies array
1215
def list_available_strategies
1316
strategies = Html2rss::RequestService.strategy_names.map do |name|
1417
{
@@ -20,11 +23,15 @@ def list_available_strategies
2023
{ strategies: strategies }
2124
end
2225

26+
##
27+
# Handle feed generation request
28+
# @param router [Roda::Request] request router
29+
# @param feed_name [String] name of the feed to generate
30+
# @return [String] RSS content
2331
def handle_feed_generation(router, feed_name)
2432
params = router.params
2533
rss_content = Feeds.generate_feed(feed_name, params)
2634

27-
# Extract TTL from feed configuration
2835
config = LocalConfig.find(feed_name)
2936
ttl = config.dig(:channel, :ttl) || 3600
3037

@@ -36,6 +43,10 @@ def handle_feed_generation(router, feed_name)
3643
Feeds.error_feed(error.message)
3744
end
3845

46+
##
47+
# Set RSS response headers
48+
# @param router [Roda::Request] request router
49+
# @param ttl [Integer] time to live in seconds
3950
def rss_headers(router, ttl: 3600)
4051
router.response['Content-Type'] = 'application/xml'
4152
router.response['Cache-Control'] = "public, max-age=#{ttl}"

app/app_routes.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
5+
module Html2rss
6+
module Web
7+
##
8+
# Main application routes for html2rss-web
9+
# Handles all route definitions and routing logic
10+
module AppRoutes
11+
module_function
12+
13+
##
14+
# Define all application routes
15+
# @param app [Class] The Roda app class
16+
def define_routes(app)
17+
define_api_routes(app)
18+
define_feed_routes(app)
19+
define_auto_source_routes(app)
20+
define_health_check_routes(app)
21+
define_main_route(app)
22+
end
23+
24+
def define_api_routes(app)
25+
app.hash_branch 'api' do |r|
26+
r.response['Content-Type'] = 'application/json'
27+
28+
r.on 'feeds.json' do
29+
r.response['Cache-Control'] = 'public, max-age=300'
30+
JSON.generate(Feeds.list_feeds)
31+
end
32+
33+
r.on 'strategies.json' do
34+
r.response['Cache-Control'] = 'public, max-age=3600'
35+
JSON.generate(ApiRoutes.list_available_strategies)
36+
end
37+
38+
r.on String do |feed_name|
39+
ApiRoutes.handle_feed_generation(r, feed_name)
40+
end
41+
end
42+
end
43+
44+
def define_feed_routes(app)
45+
app.hash_branch 'feeds' do |r|
46+
r.on String do |feed_id|
47+
AutoSourceRoutes.handle_stable_feed(r, feed_id)
48+
end
49+
end
50+
end
51+
52+
def define_auto_source_routes(app)
53+
app.hash_branch 'auto_source' do |r|
54+
handle_auto_source_routes(r)
55+
end
56+
end
57+
58+
def define_health_check_routes(app)
59+
app.hash_branch 'health_check.txt' do |r|
60+
handle_health_check_routes(r)
61+
end
62+
end
63+
64+
def define_main_route(app)
65+
app.route do |r|
66+
r.public
67+
r.hash_branches
68+
handle_static_files(r)
69+
end
70+
end
71+
end
72+
end
73+
end

app/auth.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'json'
88
require 'cgi'
99
require_relative 'local_config'
10+
require_relative 'security_logger'
1011

1112
module Html2rss
1213
##
@@ -28,7 +29,13 @@ def authenticate(request)
2829
token = extract_token(request)
2930
return nil unless token
3031

31-
get_account(token)
32+
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
38+
account
3239
end
3340

3441
##
@@ -113,10 +120,15 @@ def validate_feed_token(feed_token, url)
113120
return nil unless feed_token && url
114121

115122
token_data = decode_feed_token(feed_token)
116-
return nil unless token_data && verify_token_signature(token_data) && token_valid?(token_data, url)
123+
valid = token_data && verify_token_signature(token_data) && token_valid?(token_data, url)
124+
125+
SecurityLogger.log_token_usage(feed_token, url, valid)
126+
127+
return nil unless valid
117128

118129
get_account_by_username(token_data[:payload][:username])
119130
rescue StandardError
131+
SecurityLogger.log_token_usage(feed_token, url, false)
120132
nil
121133
end
122134

app/auto_source.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,38 @@ module Web
1212
module AutoSource
1313
module_function
1414

15+
##
16+
# Check if auto source is enabled
17+
# @return [Boolean] true if enabled
1518
def enabled?
16-
# Enable by default in development, require explicit setting in production
1719
if development?
1820
ENV.fetch('AUTO_SOURCE_ENABLED', nil) != 'false'
1921
else
2022
ENV.fetch('AUTO_SOURCE_ENABLED', nil) == 'true'
2123
end
2224
end
2325

26+
##
27+
# Authenticate request with token
28+
# @param request [Roda::Request] request object
29+
# @return [Hash, nil] account data if authenticated
2430
def authenticate_with_token(request)
2531
Auth.authenticate(request)
2632
end
2733

34+
##
35+
# Check if origin is allowed
36+
# @param request [Roda::Request] request object
37+
# @return [Boolean] true if origin is allowed
2838
def allowed_origin?(request)
2939
origin = request.env['HTTP_HOST'] || request.env['HTTP_X_FORWARDED_HOST']
3040
origins = allowed_origins
3141
origins.empty? || origins.include?(origin)
3242
end
3343

44+
##
45+
# Get allowed origins from configuration
46+
# @return [Array<String>] list of allowed origins
3447
def allowed_origins
3548
if development?
3649
default_origins = 'localhost:3000,localhost:3001,127.0.0.1:3000,127.0.0.1:3001'
@@ -41,8 +54,12 @@ def allowed_origins
4154
origins.split(',').map(&:strip)
4255
end
4356

57+
##
58+
# Check if URL is allowed for token
59+
# @param token_data [Hash] token data
60+
# @param url [String] URL to check
61+
# @return [Boolean] true if URL is allowed
4462
def url_allowed_for_token?(token_data, url)
45-
# Get full account data from config
4663
account = Auth.get_account_by_username(token_data[:username])
4764
return false unless account
4865

0 commit comments

Comments
 (0)