33require 'roda'
44require 'rack/cache'
55require 'json'
6+ require 'base64'
67
78require 'html2rss'
89require_relative 'app/environment_validator'
1112require_relative 'app/auto_source'
1213require_relative 'app/feeds'
1314require_relative 'app/health_check'
15+ require_relative 'app/local_config'
1416require_relative 'app/response_context'
1517require_relative 'app/xml_builder'
1618require_relative 'app/security_logger'
@@ -33,17 +35,73 @@ def development? = self.class.development?
3335 RodaConfig . configure ( self )
3436
3537 plugin :hash_branches
38+ plugin :json_parser
39+ plugin :public
40+ plugin :exception_page
41+ plugin :error_handler do |error |
42+ next exception_page ( error ) if development?
3643
37- Dir [ 'routes/*.rb' ] . each { |f | require_relative f }
44+ response . status = 500
45+ response [ 'Content-Type' ] = 'application/xml'
46+ require_relative 'app/xml_builder'
47+ XmlBuilder . build_error_feed ( message : error . message )
48+ end
3849
39- @show_backtrace = development? && ! ENV [ 'CI' ]
50+ # Routes are now defined directly in this file for better clarity
4051
41- AppRoutes . load_routes ( self )
52+ @show_backtrace = development? && ! ENV [ 'CI' ]
4253
4354 route do |r |
4455 r . public
45- r . hash_branches ( '' )
4656
57+ # API routes
58+ r . on 'api' do
59+ r . response [ 'Content-Type' ] = 'application/json'
60+
61+ r . get 'feeds.json' do
62+ r . response [ 'Cache-Control' ] = 'public, max-age=300'
63+ JSON . generate ( Feeds . list_feeds )
64+ end
65+
66+ r . get 'strategies.json' do
67+ r . response [ 'Cache-Control' ] = 'public, max-age=3600'
68+ JSON . generate ( list_available_strategies )
69+ end
70+
71+ r . get String do |feed_name |
72+ handle_feed_generation ( r , feed_name )
73+ end
74+ end
75+
76+ # Auto source routes
77+ r . on 'auto_source' do
78+ if AutoSource . enabled?
79+ r . post 'create' do
80+ handle_create_feed ( r )
81+ end
82+
83+ r . get String do |encoded_url |
84+ handle_legacy_feed ( r , encoded_url )
85+ end
86+ else
87+ r . response . status = 400
88+ 'Auto source feature is disabled'
89+ end
90+ end
91+
92+ # Feed routes
93+ r . on 'feeds' do
94+ r . get String do |feed_id |
95+ handle_stable_feed ( r , feed_id )
96+ end
97+ end
98+
99+ # Health check
100+ r . get 'health_check.txt' do
101+ handle_health_check ( r )
102+ end
103+
104+ # Root route
47105 r . root do
48106 index_path = 'public/frontend/index.html'
49107 response [ 'Content-Type' ] = 'text/html'
@@ -77,6 +135,161 @@ def fallback_html
77135 </ html >
78136 HTML
79137 end
138+
139+ private
140+
141+ def list_available_strategies
142+ strategies = Html2rss ::RequestService . strategy_names . map do |name |
143+ {
144+ name : name . to_s ,
145+ display_name : name . to_s . split ( '_' ) . map ( &:capitalize ) . join ( ' ' )
146+ }
147+ end
148+
149+ { strategies : strategies }
150+ end
151+
152+ def handle_feed_generation ( router , feed_name )
153+ rss_content = Feeds . generate_feed ( feed_name , router . params )
154+ config = LocalConfig . find ( feed_name )
155+ ttl = config . dig ( :channel , :ttl ) || 3600
156+
157+ router . response [ 'Content-Type' ] = 'application/xml'
158+ router . response [ 'Cache-Control' ] = "public, max-age=#{ ttl } "
159+ rss_content
160+ end
161+
162+ def handle_create_feed ( router )
163+ account = Auth . authenticate ( router )
164+ unless account
165+ router . response . status = 401
166+ return 'Unauthorized'
167+ end
168+
169+ url = router . params [ 'url' ]
170+ unless url && Auth . valid_url? ( url )
171+ router . response . status = 400
172+ return 'Bad Request'
173+ end
174+
175+ unless Auth . url_allowed? ( account , url )
176+ router . response . status = 403
177+ return 'Forbidden'
178+ end
179+
180+ strategy = router . params [ 'strategy' ] || 'ssrf_filter'
181+ feed_data = AutoSource . create_stable_feed ( 'Generated Feed' , url , account , strategy )
182+ unless feed_data
183+ router . response . status = 500
184+ return 'Internal Server Error'
185+ end
186+
187+ router . response [ 'Content-Type' ] = 'application/json'
188+ JSON . generate ( feed_data )
189+ end
190+
191+ def handle_legacy_feed ( router , encoded_url )
192+ account = Auth . authenticate ( router )
193+ unless account
194+ router . response . status = 401
195+ return 'Unauthorized'
196+ end
197+
198+ unless AutoSource . allowed_origin? ( router )
199+ router . response . status = 403
200+ return 'Forbidden'
201+ end
202+
203+ decoded_url = Base64 . urlsafe_decode64 ( encoded_url )
204+ unless decoded_url && Auth . valid_url? ( decoded_url )
205+ router . response . status = 400
206+ return 'Bad Request'
207+ end
208+
209+ unless AutoSource . url_allowed_for_token? ( account , decoded_url )
210+ router . response . status = 403
211+ return 'Forbidden'
212+ end
213+
214+ strategy = router . params [ 'strategy' ] || 'ssrf_filter'
215+ rss_content = AutoSource . generate_feed_content ( decoded_url , strategy )
216+
217+ router . response [ 'Content-Type' ] = 'application/xml'
218+ rss_content . to_s
219+ rescue ArgumentError
220+ router . response . status = 400
221+ 'Bad Request'
222+ end
223+
224+ def handle_stable_feed ( router , feed_id )
225+ feed_token = router . params [ 'token' ]
226+
227+ if feed_token
228+ handle_public_feed ( router , feed_id , feed_token )
229+ else
230+ handle_authenticated_feed ( router )
231+ end
232+ end
233+
234+ def handle_public_feed ( router , _feed_id , feed_token )
235+ url = router . params [ 'url' ]
236+ unless url && Auth . valid_url? ( url )
237+ router . response . status = 400
238+ return 'Bad Request'
239+ end
240+
241+ unless Auth . feed_url_allowed? ( feed_token , url )
242+ router . response . status = 403
243+ return 'Forbidden'
244+ end
245+
246+ strategy = router . params [ 'strategy' ] || 'ssrf_filter'
247+ rss_content = AutoSource . generate_feed_content ( url , strategy )
248+
249+ router . response [ 'Content-Type' ] = 'application/xml'
250+ rss_content . to_s
251+ end
252+
253+ def handle_authenticated_feed ( router )
254+ account = Auth . authenticate ( router )
255+ unless account
256+ router . response . status = 401
257+ return 'Unauthorized'
258+ end
259+
260+ url = router . params [ 'url' ]
261+ unless url && Auth . valid_url? ( url )
262+ router . response . status = 400
263+ return 'Bad Request'
264+ end
265+
266+ unless Auth . url_allowed? ( account , url )
267+ router . response . status = 403
268+ return 'Forbidden'
269+ end
270+
271+ strategy = router . params [ 'strategy' ] || 'ssrf_filter'
272+ rss_content = AutoSource . generate_feed_content ( url , strategy )
273+
274+ router . response [ 'Content-Type' ] = 'application/xml'
275+ rss_content . to_s
276+ end
277+
278+ def handle_health_check ( router )
279+ health_check_account = HealthCheck . find_health_check_account
280+ account = Auth . authenticate ( router )
281+
282+ if account && health_check_account && account [ :token ] == health_check_account [ :token ]
283+ router . response [ 'Content-Type' ] = 'text/plain'
284+ HealthCheck . run
285+ else
286+ router . response . status = 401
287+ router . response [ 'WWW-Authenticate' ] = 'Bearer realm="Health Check"'
288+ router . response [ 'Content-Type' ] = 'application/xml'
289+ require_relative 'app/xml_builder'
290+ XmlBuilder . build_error_feed ( message : 'Unauthorized' , title : 'Health Check Unauthorized' )
291+ end
292+ end
80293 end
81294 end
82295end
0 commit comments