55require 'json'
66
77require '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'
911require_relative 'app/auth'
1012require_relative 'app/auto_source'
1113require_relative 'app/feeds'
1618require_relative 'app/xml_builder'
1719require_relative 'app/auto_source_routes'
1820require_relative 'app/health_check_routes'
21+ require_relative 'app/security_logger'
1922
2023module 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
18254end
0 commit comments