refactor progress
This commit is contained in:
164
fluxer_relay/src/relay/fluxer_relay_http_handler.erl
Normal file
164
fluxer_relay/src/relay/fluxer_relay_http_handler.erl
Normal file
@@ -0,0 +1,164 @@
|
||||
%% Copyright (C) 2026 Fluxer Contributors
|
||||
%%
|
||||
%% This file is part of Fluxer.
|
||||
%%
|
||||
%% Fluxer is free software: you can redistribute it and/or modify
|
||||
%% it under the terms of the GNU Affero General Public License as published by
|
||||
%% the Free Software Foundation, either version 3 of the License, or
|
||||
%% (at your option) any later version.
|
||||
%%
|
||||
%% Fluxer is distributed in the hope that it will be useful,
|
||||
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
%% GNU Affero General Public License for more details.
|
||||
%%
|
||||
%% You should have received a copy of the GNU Affero General Public License
|
||||
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
-module(fluxer_relay_http_handler).
|
||||
-behaviour(cowboy_handler).
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
-spec init(cowboy_req:req(), term()) -> {ok, cowboy_req:req(), term()}.
|
||||
init(Req, State) ->
|
||||
Method = cowboy_req:method(Req),
|
||||
Path = cowboy_req:path(Req),
|
||||
Headers = cowboy_req:headers(Req),
|
||||
case validate_relay_request(Headers) of
|
||||
{ok, OriginInstance} ->
|
||||
proxy_request(Method, Path, Headers, Req, State, OriginInstance);
|
||||
{error, Reason} ->
|
||||
error_response(401, Reason, Req, State)
|
||||
end.
|
||||
|
||||
-spec validate_relay_request(map()) -> {ok, binary()} | {error, binary()}.
|
||||
validate_relay_request(Headers) ->
|
||||
case maps:get(<<"x-relay-origin">>, Headers, undefined) of
|
||||
undefined ->
|
||||
{error, <<"Missing X-Relay-Origin header">>};
|
||||
Origin ->
|
||||
case maps:get(<<"x-relay-signature">>, Headers, undefined) of
|
||||
undefined ->
|
||||
{error, <<"Missing X-Relay-Signature header">>};
|
||||
_Signature ->
|
||||
{ok, Origin}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec proxy_request(binary(), binary(), map(), cowboy_req:req(), term(), binary()) ->
|
||||
{ok, cowboy_req:req(), term()}.
|
||||
proxy_request(Method, Path, Headers, Req, State, OriginInstance) ->
|
||||
Config = fluxer_relay_env:get_map(),
|
||||
UpstreamHost = maps:get(upstream_api_host, Config, "localhost:8080"),
|
||||
UseTls = maps:get(upstream_use_tls, Config, false),
|
||||
Timeout = maps:get(connection_timeout_ms, Config, 30000),
|
||||
{Host, Port} = parse_host_port(UpstreamHost, UseTls),
|
||||
Transport = case UseTls of true -> tls; false -> tcp end,
|
||||
case gun:open(Host, Port, #{transport => Transport, connect_timeout => Timeout}) of
|
||||
{ok, ConnPid} ->
|
||||
case gun:await_up(ConnPid, Timeout) of
|
||||
{ok, _Protocol} ->
|
||||
forward_request(ConnPid, Method, Path, Headers, Req, State, OriginInstance);
|
||||
{error, Reason} ->
|
||||
gun:close(ConnPid),
|
||||
lager:error("Failed to connect to upstream: ~p", [Reason]),
|
||||
error_response(502, <<"Upstream connection failed">>, Req, State)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
lager:error("Failed to open connection to upstream: ~p", [Reason]),
|
||||
error_response(502, <<"Upstream connection failed">>, Req, State)
|
||||
end.
|
||||
|
||||
-spec forward_request(pid(), binary(), binary(), map(), cowboy_req:req(), term(), binary()) ->
|
||||
{ok, cowboy_req:req(), term()}.
|
||||
forward_request(ConnPid, Method, Path, Headers, Req, State, OriginInstance) ->
|
||||
ProxiedHeaders = add_proxy_headers(Headers, OriginInstance),
|
||||
{ok, Body, Req2} = read_body(Req),
|
||||
StreamRef = case Method of
|
||||
<<"GET">> -> gun:get(ConnPid, Path, maps:to_list(ProxiedHeaders));
|
||||
<<"POST">> -> gun:post(ConnPid, Path, maps:to_list(ProxiedHeaders), Body);
|
||||
<<"PUT">> -> gun:put(ConnPid, Path, maps:to_list(ProxiedHeaders), Body);
|
||||
<<"PATCH">> -> gun:patch(ConnPid, Path, maps:to_list(ProxiedHeaders), Body);
|
||||
<<"DELETE">> -> gun:delete(ConnPid, Path, maps:to_list(ProxiedHeaders));
|
||||
<<"OPTIONS">> -> gun:options(ConnPid, Path, maps:to_list(ProxiedHeaders));
|
||||
<<"HEAD">> -> gun:head(ConnPid, Path, maps:to_list(ProxiedHeaders));
|
||||
_ -> gun:request(ConnPid, Method, Path, maps:to_list(ProxiedHeaders), Body)
|
||||
end,
|
||||
Timeout = fluxer_relay_env:get(connection_timeout_ms),
|
||||
case gun:await(ConnPid, StreamRef, Timeout) of
|
||||
{response, fin, Status, RespHeaders} ->
|
||||
gun:close(ConnPid),
|
||||
Req3 = cowboy_req:reply(Status, maps:from_list(RespHeaders), <<>>, Req2),
|
||||
{ok, Req3, State};
|
||||
{response, nofin, Status, RespHeaders} ->
|
||||
case gun:await_body(ConnPid, StreamRef, Timeout) of
|
||||
{ok, RespBody} ->
|
||||
gun:close(ConnPid),
|
||||
Req3 = cowboy_req:reply(Status, maps:from_list(RespHeaders), RespBody, Req2),
|
||||
{ok, Req3, State};
|
||||
{error, Reason} ->
|
||||
gun:close(ConnPid),
|
||||
lager:error("Failed to read upstream response body: ~p", [Reason]),
|
||||
error_response(502, <<"Upstream read failed">>, Req2, State)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
gun:close(ConnPid),
|
||||
lager:error("Upstream request failed: ~p", [Reason]),
|
||||
error_response(502, <<"Upstream request failed">>, Req2, State)
|
||||
end.
|
||||
|
||||
-spec add_proxy_headers(map(), binary()) -> map().
|
||||
add_proxy_headers(Headers, OriginInstance) ->
|
||||
FilteredHeaders = filter_relay_headers(Headers),
|
||||
FilteredHeaders#{
|
||||
<<"x-forwarded-for">> => OriginInstance,
|
||||
<<"x-relay-proxied">> => <<"true">>,
|
||||
<<"x-relay-origin">> => OriginInstance
|
||||
}.
|
||||
|
||||
-spec filter_relay_headers(map()) -> map().
|
||||
filter_relay_headers(Headers) ->
|
||||
RelayHeaders = [
|
||||
<<"x-fluxer-target">>,
|
||||
<<"x-relay-signature">>,
|
||||
<<"x-relay-timestamp">>,
|
||||
<<"x-relay-instance">>,
|
||||
<<"x-relay-request-id">>
|
||||
],
|
||||
lists:foldl(fun(Key, Acc) -> maps:remove(Key, Acc) end, Headers, RelayHeaders).
|
||||
|
||||
-spec read_body(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||
read_body(Req) ->
|
||||
read_body(Req, <<>>).
|
||||
|
||||
-spec read_body(cowboy_req:req(), binary()) -> {ok, binary(), cowboy_req:req()}.
|
||||
read_body(Req, Acc) ->
|
||||
case cowboy_req:read_body(Req) of
|
||||
{ok, Data, Req2} ->
|
||||
{ok, <<Acc/binary, Data/binary>>, Req2};
|
||||
{more, Data, Req2} ->
|
||||
read_body(Req2, <<Acc/binary, Data/binary>>)
|
||||
end.
|
||||
|
||||
-spec parse_host_port(string(), boolean()) -> {string(), inet:port_number()}.
|
||||
parse_host_port(HostPort, UseTls) ->
|
||||
DefaultPort = case UseTls of true -> 443; false -> 80 end,
|
||||
case string:split(HostPort, ":") of
|
||||
[Host, PortStr] ->
|
||||
Port = list_to_integer(PortStr),
|
||||
{Host, Port};
|
||||
[Host] ->
|
||||
{Host, DefaultPort}
|
||||
end.
|
||||
|
||||
-spec error_response(integer(), binary(), cowboy_req:req(), term()) ->
|
||||
{ok, cowboy_req:req(), term()}.
|
||||
error_response(Status, Message, Req, State) ->
|
||||
Response = json:encode(#{
|
||||
<<"error">> => Message
|
||||
}),
|
||||
Req2 = cowboy_req:reply(Status, #{
|
||||
<<"content-type">> => <<"application/json">>
|
||||
}, Response, Req),
|
||||
{ok, Req2, State}.
|
||||
Reference in New Issue
Block a user