@@ -18,6 +18,7 @@ defmodule RetWeb.PageController do
1818 alias Plug.Conn
1919 import Ret.ConnUtils
2020 import Ret.HttpUtils
21+ require Logger
2122
2223 ##
2324 # NOTE: In addition to adding a route, you must add static html pages to the page_origin_warmer.ex
@@ -745,17 +746,23 @@ defmodule RetWeb.PageController do
745746 do: cors_proxy ( conn , "#{ url } ?#{ qs } " )
746747
747748 defp cors_proxy ( conn , url ) do
749+ cors_proxy_with_redirects ( conn , url , 0 )
750+ end
751+
752+ defp cors_proxy_with_redirects ( conn , url , redirect_count ) when redirect_count > 5 do
753+ Logger . error ( "CORS Proxy: Too many redirects (#{ redirect_count } ) for URL: #{ url } " )
754+ conn |> send_resp ( 400 , "Too many redirects" )
755+ end
756+
757+ defp cors_proxy_with_redirects ( conn , url , redirect_count ) do
748758 % URI { authority: authority , host: host } = uri = URI . parse ( url )
749759
750760 resolved_ip = HttpUtils . resolve_ip ( host )
751761
752762 if HttpUtils . internal_ip? ( resolved_ip ) do
763+ Logger . warning ( "CORS Proxy: Blocking internal IP #{ inspect ( resolved_ip ) } for host #{ host } " )
753764 conn |> send_resp ( 401 , "Bad request." )
754765 else
755- # We want to ensure that the URL we request hits the same IP that we verified above,
756- # so we replace the host with the IP address here and use this url to make the proxy request.
757- ip_url = URI . to_string ( HttpUtils . replace_host ( uri , resolved_ip ) )
758-
759766 # Disallow CORS proxying unless request was made to the cors proxy url
760767 cors_proxy_url = Application . get_env ( :ret , RetWeb.Endpoint ) [ :cors_proxy_url ]
761768
@@ -780,7 +787,7 @@ defmodule RetWeb.PageController do
780787 upstream: url ,
781788 allowed_origins: allowed_origins ,
782789 proxy_url: "#{ cors_scheme } ://#{ cors_host } :#{ cors_port } " ,
783- # Since we replaced the host with the IP address in ip_url above, we need to force the host
790+ # We need to force the host
784791 # used for ssl verification here so that the connection isn't rejected.
785792 # Note that we have to convert the authority to a charlist, since this uses Erlang's `ssl` module
786793 # internally, which expects a charlist.
@@ -789,33 +796,91 @@ defmodule RetWeb.PageController do
789796 { :server_name_indication , to_charlist ( authority ) } ,
790797 { :versions , [ :"tlsv1.2" , :"tlsv1.3" ] }
791798 ]
792- ]
793- # preserve_host_header: true
799+ ] ,
800+ error_callback: fn error -> Logger . error ( "CORS-proxy error: #{ inspect ( error ) } " ) end
794801 )
795802
796803 body = ReverseProxyPlug . read_body ( conn )
797804 is_head = conn |> Conn . get_req_header ( "x-original-method" ) == [ "HEAD" ]
798805
799- % Conn { }
800- |> Map . merge ( conn )
801- |> Map . put (
802- :method ,
803- if is_head do
804- "HEAD"
805- else
806- conn . method
806+ try do
807+ # First, make a HEAD request to check for redirects using HTTPoison
808+ case HTTPoison . head ( url , [ ] ,
809+ follow_redirect: false ,
810+ ssl: [
811+ { :server_name_indication , to_charlist ( authority ) } ,
812+ { :versions , [ :"tlsv1.2" , :"tlsv1.3" ] }
813+ ] ,
814+ timeout: 15_000 ,
815+ recv_timeout: 15_000
816+ ) do
817+ { :ok , % HTTPoison.Response { status_code: status_code , headers: headers } }
818+ when status_code in [ 301 , 302 , 303 , 307 , 308 ] ->
819+ # Found a redirect
820+ location_header =
821+ headers
822+ |> Enum . find ( fn { k , _v } -> String . downcase ( k ) == "location" end )
823+ |> elem ( 1 )
824+
825+ if location_header do
826+ # Resolve relative URLs against the current URL
827+ redirect_url = URI . merge ( uri , location_header ) |> URI . to_string ( )
828+ cors_proxy_with_redirects ( conn , redirect_url , redirect_count + 1 )
829+ else
830+ Logger . warning ( "CORS Proxy: Redirect response missing location header" )
831+ # Fall back to ReverseProxyPlug for this response
832+ make_reverse_proxy_request ( conn , url , body , is_head , opts )
833+ end
834+
835+ { :ok , % HTTPoison.Response { } } ->
836+ # Not a redirect, use ReverseProxyPlug for the actual request
837+ make_reverse_proxy_request ( conn , url , body , is_head , opts )
838+
839+ { :error , reason } ->
840+ Logger . error ( "CORS Proxy: HEAD request failed: #{ inspect ( reason ) } " )
841+ # Fall back to ReverseProxyPlug anyway
842+ make_reverse_proxy_request ( conn , url , body , is_head , opts )
807843 end
808- )
809- # Need to strip path_info since proxy plug reads it
810- |> Map . put ( :path_info , [ ] )
811- |> ReverseProxyPlug . request ( body , opts )
812- |> ReverseProxyPlug . response ( conn , opts )
844+ rescue
845+ error ->
846+ Logger . error ( "CORS Proxy: Request failed with exception: #{ inspect ( error ) } " )
847+ conn |> send_resp ( 500 , "Proxy request failed: #{ inspect ( error ) } " )
848+ catch
849+ :exit , reason ->
850+ Logger . error ( "CORS Proxy: Request exited with reason: #{ inspect ( reason ) } " )
851+ conn |> send_resp ( 500 , "Proxy request timed out or failed" )
852+
853+ kind , reason ->
854+ Logger . error ( "CORS Proxy: Request failed with #{ kind } : #{ inspect ( reason ) } " )
855+ conn |> send_resp ( 500 , "Proxy request failed" )
856+ end
813857 else
858+ Logger . warning ( "CORS Proxy: Request rejected - invalid host or scheme" )
814859 conn |> send_resp ( 401 , "Bad request." )
815860 end
816861 end
817862 end
818863
864+ defp make_reverse_proxy_request ( conn , _url , body , is_head , opts ) do
865+ proxy_conn =
866+ % Conn { }
867+ |> Map . merge ( conn )
868+ |> Map . put (
869+ :method ,
870+ if is_head do
871+ "HEAD"
872+ else
873+ conn . method
874+ end
875+ )
876+ # Need to strip path_info since proxy plug reads it
877+ |> Map . put ( :path_info , [ ] )
878+ |> ReverseProxyPlug . request ( body , opts )
879+ |> ReverseProxyPlug . response ( conn , opts )
880+
881+ proxy_conn
882+ end
883+
819884 defp render_static_asset ( conn ) do
820885 static_options = Plug.Static . init ( at: "/" , from: :ret , gzip: true , brotli: true )
821886 Plug.Static . call ( conn , static_options )
0 commit comments