2424module Html2rss
2525 module Web
2626 ##
27- # This app uses html2rss and serves the feeds via HTTP.
28- #
29- # It is built with [Roda](https://roda.jeremyevans.net/).
27+ # Roda app serving RSS feeds via html2rss
3028 class App < Roda
3129 CONTENT_TYPE_RSS = 'application/xml'
3230
@@ -54,7 +52,7 @@ def development? = self.class.development?
5452 csp . font_src :self , 'data:'
5553 csp . form_action :self
5654 csp . base_uri :none
57- csp . frame_ancestors :none
55+ csp . frame_ancestors development? ? [ 'http://localhost:*' , 'https://localhost:*' ] : :none
5856 csp . frame_src :self
5957 csp . object_src :none
6058 csp . media_src :none
@@ -66,12 +64,18 @@ def development? = self.class.development?
6664 end
6765
6866 plugin :default_headers , {
69- 'X-Content-Type-Options' => 'nosniff' , 'X-XSS-Protection' => '1; mode=block' ,
70- 'X-Frame-Options' => 'SAMEORIGIN' , 'X-Permitted-Cross-Domain-Policies' => 'none' ,
71- 'Referrer-Policy' => 'strict-origin-when-cross-origin' , 'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()' ,
67+ 'X-Content-Type-Options' => 'nosniff' ,
68+ 'X-XSS-Protection' => '1; mode=block' ,
69+ 'X-Frame-Options' => 'SAMEORIGIN' ,
70+ 'X-Permitted-Cross-Domain-Policies' => 'none' ,
71+ 'Referrer-Policy' => 'strict-origin-when-cross-origin' ,
72+ 'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()' ,
7273 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload' ,
73- 'Cross-Origin-Embedder-Policy' => 'require-corp' , 'Cross-Origin-Opener-Policy' => 'same-origin' ,
74- 'Cross-Origin-Resource-Policy' => 'same-origin' , 'X-DNS-Prefetch-Control' => 'off' , 'X-Download-Options' => 'noopen'
74+ 'Cross-Origin-Embedder-Policy' => 'require-corp' ,
75+ 'Cross-Origin-Opener-Policy' => 'same-origin' ,
76+ 'Cross-Origin-Resource-Policy' => 'same-origin' ,
77+ 'X-DNS-Prefetch-Control' => 'off' ,
78+ 'X-Download-Options' => 'noopen'
7579 }
7680
7781 plugin :json_parser
@@ -112,18 +116,6 @@ def development? = self.class.development?
112116 route do |r |
113117 r . public
114118
115- r . get 'feeds.json' do
116- r . response [ 'Cache-Control' ] = 'public, max-age=300'
117- JSON . generate ( Feeds . list_feeds )
118- end
119-
120- r . get 'strategies.json' do
121- r . response [ 'Cache-Control' ] = 'public, max-age=3600'
122- JSON . generate ( { strategies : Html2rss ::RequestService . strategy_names . map do |name |
123- { name : name . to_s , display_name : name . to_s . split ( '_' ) . map ( &:capitalize ) . join ( ' ' ) }
124- end } )
125- end
126-
127119 r . on 'api' , 'v1' do
128120 r . response [ 'Content-Type' ] = 'application/json'
129121
@@ -149,8 +141,8 @@ def development? = self.class.development?
149141 end
150142
151143 r . on 'feeds' do
152- r . get String do |feed_id |
153- result = Api ::V1 ::Feeds . show ( r , feed_id )
144+ r . get String do |token |
145+ result = Api ::V1 ::Feeds . show ( r , token )
154146 result . is_a? ( Hash ) ? JSON . generate ( result ) : result
155147 end
156148 r . post do
@@ -179,25 +171,13 @@ def development? = self.class.development?
179171 end
180172 end
181173
182- r . on 'auto_source' do
183- if AutoSource . enabled?
184- r . post 'create' do
185- handle_create_feed ( r )
186- end
174+ # Backward compatibility: /{feed_name} (no auth required)
175+ r . get String do |feed_name |
176+ # Skip static file requests
177+ next if feed_name . include? ( '.' ) && !feed_name . end_with? ( '.xml' , '.rss' )
187178
188- r . get String do |encoded_url |
189- handle_legacy_feed ( r , encoded_url )
190- end
191- else
192- r . response . status = 400
193- 'Auto source feature is disabled'
194- end
195- end
196-
197- r . on 'feeds' do
198- r . get String do |_feed_id |
199- handle_feed_with_auth ( r , r . params [ 'token' ] )
200- end
179+ # Route to feed generation without auth for backward compatibility
180+ handle_feed_generation ( r , feed_name )
201181 end
202182 r . get 'health_check.txt' do
203183 handle_health_check ( r )
@@ -210,7 +190,25 @@ def development? = self.class.development?
210190 end
211191
212192 def fallback_html
213- '<!DOCTYPE html><html><head><title>html2rss-web</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{font-family:system-ui,sans-serif;max-width:800px;margin:0 auto;padding:2rem;line-height:1.6}h1{color:#111827}code{background:#f3f4f6;padding:0.2rem 0.4rem;border-radius:0.25rem}</style></head><body><h1>html2rss-web</h1><p>Convert websites to RSS feeds</p><p>API available at <code>/api/</code></p></body></html>'
193+ <<~HTML
194+ <!DOCTYPE html>
195+ < html >
196+ < head >
197+ < title > html2rss-web</ title >
198+ < meta name ="viewport " content ="width=device-width,initial-scale=1 ">
199+ < style >
200+ body {font-family : system-ui, sans-serif;max-width : 800px ;margin : 0 auto;padding : 2rem ;line-height : 1.6 }
201+ h1 {color : # 111827 }
202+ code {background : # f3f4f6 ;padding : 0.2rem 0.4rem ;border-radius : 0.25rem }
203+ </ style >
204+ </ head >
205+ < body >
206+ < h1 > html2rss-web</ h1 >
207+ < p > Convert websites to RSS feeds</ p >
208+ < p > API available at < code > /api/</ code > </ p >
209+ </ body >
210+ </ html >
211+ HTML
214212 end
215213
216214 private
@@ -223,65 +221,6 @@ def handle_feed_generation(router, feed_name)
223221 rss_content
224222 end
225223
226- def handle_create_feed ( router )
227- account = Auth . authenticate ( router )
228- unless account
229- router . response . status = 401
230- return 'Unauthorized'
231- end
232-
233- url = router . params [ 'url' ]
234- unless url && Auth . valid_url? ( url )
235- router . response . status = 400
236- return 'URL parameter required'
237- end
238-
239- unless Auth . url_allowed? ( account , url )
240- router . response . status = 403
241- return 'Access Denied'
242- end
243-
244- strategy = router . params [ 'strategy' ] || 'ssrf_filter'
245- name = router . params [ 'name' ] || "Auto-generated feed for #{ url } "
246- feed_data = AutoSource . create_stable_feed ( name , url , account , strategy )
247- unless feed_data
248- router . response . status = 500
249- return 'Internal Server Error'
250- end
251-
252- router . response [ 'Content-Type' ] = 'application/json'
253- JSON . generate ( feed_data )
254- end
255-
256- def handle_legacy_feed ( router , encoded_url )
257- account = Auth . authenticate ( router )
258- return error_response ( router , 401 , 'Unauthorized' ) unless account
259- return error_response ( router , 403 , 'Access Denied' ) unless AutoSource . allowed_origin? ( router )
260-
261- decoded_url = Base64 . urlsafe_decode64 ( encoded_url )
262- return error_response ( router , 400 , 'Invalid URL' ) unless decoded_url && Auth . valid_url? ( decoded_url )
263- return error_response ( router , 403 , 'Access Denied' ) unless AutoSource . url_allowed_for_token? ( account ,
264- decoded_url )
265-
266- generate_rss_response ( router , decoded_url )
267- rescue ArgumentError
268- error_response ( router , 400 , 'Invalid encoded URL' )
269- end
270-
271- def handle_feed_with_auth ( router , feed_token = nil )
272- url = router . params [ 'url' ]
273- return error_response ( router , 400 , 'url parameter required' ) unless url && Auth . valid_url? ( url )
274-
275- if feed_token
276- return error_response ( router , 403 , 'Access Denied' ) unless Auth . feed_url_allowed? ( feed_token , url )
277- else
278- account = Auth . authenticate ( router )
279- return error_response ( router , 401 , 'Unauthorized' ) unless account
280- return error_response ( router , 403 , 'Access Denied' ) unless Auth . url_allowed? ( account , url )
281- end
282- generate_rss_response ( router , url )
283- end
284-
285224 def generate_rss_response ( router , url )
286225 router . response [ 'Content-Type' ] = 'application/xml'
287226 HttpCache . expires ( router . response , 600 , cache_control : 'public' )
0 commit comments