|
| 1 | +% |
| 2 | +% This file is part of AtomVM. |
| 3 | +% |
| 4 | +% Copyright 2024 Davide Bettio <[email protected]> |
| 5 | +% |
| 6 | +% Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +% you may not use this file except in compliance with the License. |
| 8 | +% You may obtain a copy of the License at |
| 9 | +% |
| 10 | +% http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +% |
| 12 | +% Unless required by applicable law or agreed to in writing, software |
| 13 | +% distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +% See the License for the specific language governing permissions and |
| 16 | +% limitations under the License. |
| 17 | +% |
| 18 | +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later |
| 19 | +% |
| 20 | + |
| 21 | +-module(ahttp_client). |
| 22 | +-export([connect/4, request/5, stream/2]). |
| 23 | + |
| 24 | +-define(DEFAULT_WANTED_HEADERS, [<<"Content-Length">>]). |
| 25 | + |
| 26 | +-record(http_client, {host, socket, parser, ref}). |
| 27 | +-record(parser_state, {state, acc, remaining_body_bytes, wanted_headers = ?DEFAULT_WANTED_HEADERS}). |
| 28 | + |
| 29 | +connect(Protocol, Host, Port, Options) when is_binary(Host) -> |
| 30 | + connect(Protocol, binary_to_list(Host), Port, Options); |
| 31 | +connect(http, Host, Port, _Options) -> |
| 32 | + {ok, Sock} = gen_tcp:connect(Host, Port, [binary, {active, true}]), |
| 33 | + {ok, #http_client{host = list_to_binary(Host), socket = {gen_tcp, Sock}}}. |
| 34 | + |
| 35 | +request(#http_client{host = Host, socket = TSocket} = Conn, Method, Path, Headers, Body) -> |
| 36 | + {BodyBin, MaybeBodyHeaders} = prepare_body(Body, Headers), |
| 37 | + HeadersBin = transform_headers(MaybeBodyHeaders), |
| 38 | + Data = |
| 39 | + <<Method/binary, " ", Path/binary, |
| 40 | + " HTTP/1.1\r\n" |
| 41 | + "Host: ", Host/binary, "\r\n", HeadersBin/binary, "\r\n", BodyBin/binary>>, |
| 42 | + {gen_tcp, TCPSocket} = TSocket, |
| 43 | + case gen_tcp:send(TCPSocket, Data) of |
| 44 | + ok -> |
| 45 | + Ref = make_ref(), |
| 46 | + {ok, Conn#http_client{ref = Ref}, Ref}; |
| 47 | + _ -> |
| 48 | + error |
| 49 | + end. |
| 50 | + |
| 51 | +prepare_body(undefined, Headers) -> |
| 52 | + {<<"">>, Headers}; |
| 53 | +prepare_body(nil, Headers) -> |
| 54 | + {<<"">>, Headers}; |
| 55 | +prepare_body(Body, Headers) -> |
| 56 | + BodyLen = integer_to_binary(byte_size(Body)), |
| 57 | + {Body, [{<<"content-length">>, BodyLen} | Headers]}. |
| 58 | + |
| 59 | +transform_headers([]) -> |
| 60 | + <<"">>; |
| 61 | +transform_headers([{Name, Value} | Tail]) -> |
| 62 | + Bin = transform_headers(Tail), |
| 63 | + <<Name/binary, ": ", Value/binary, "\r\n", Bin/binary>>. |
| 64 | + |
| 65 | +stream(#http_client{socket = {gen_tcp, TSocket}, parser = Parser} = Conn, {tcp, TSocket, Chunk}) -> |
| 66 | + {ok, UpdatedParser, Parsed} = feed_parser(Parser, Chunk), |
| 67 | + Responses = make_responses(Parsed, Conn#http_client.ref, []), |
| 68 | + {ok, Conn#http_client{parser = UpdatedParser}, Responses}; |
| 69 | +stream(#http_client{socket = {gen_tcp, TSocket}} = Conn, {tcp_closed, TSocket}) -> |
| 70 | + {ok, Conn, closed}; |
| 71 | +stream(_Conn, _Other) -> |
| 72 | + unknown. |
| 73 | + |
| 74 | +make_responses([], _Ref, Acc) -> |
| 75 | + Acc; |
| 76 | +make_responses([Response | Tail], Ref, Acc) when is_atom(Response) -> |
| 77 | + make_responses(Tail, Ref, [{Response, Ref} | Acc]); |
| 78 | +make_responses([{Tag, Value} | Tail], Ref, Acc) -> |
| 79 | + make_responses(Tail, Ref, [{Tag, Ref, Value} | Acc]). |
| 80 | + |
| 81 | +feed_parser(undefined, Chunk) -> |
| 82 | + feed_parser(#parser_state{}, Chunk); |
| 83 | +feed_parser(#parser_state{state = body} = Parser, Chunk) -> |
| 84 | + consume_bytes(append_chunk(Parser, Chunk), []); |
| 85 | +feed_parser(Parser, Chunk) -> |
| 86 | + consume_lines(append_chunk(Parser, Chunk), []). |
| 87 | + |
| 88 | +consume_bytes(#parser_state{acc = undefined} = Parser, ParsedAcc) -> |
| 89 | + {ok, Parser, ParsedAcc}; |
| 90 | +consume_bytes(#parser_state{acc = Chunk} = Parser, ParsedAcc) when is_binary(Chunk) -> |
| 91 | + ReplacedAccParser = replace_chunk(Parser, undefined), |
| 92 | + NewRemBodyBytes = maybe_decrement( |
| 93 | + ReplacedAccParser#parser_state.remaining_body_bytes, byte_size(Chunk) |
| 94 | + ), |
| 95 | + {UpdatedParser, Parsed} = maybe_handle_end_of_response( |
| 96 | + ReplacedAccParser#parser_state{remaining_body_bytes = NewRemBodyBytes}, [ |
| 97 | + {data, Chunk} | ParsedAcc |
| 98 | + ] |
| 99 | + ), |
| 100 | + {ok, UpdatedParser, Parsed}. |
| 101 | + |
| 102 | +consume_lines(Parser, ParsedAcc) -> |
| 103 | + case binary:split(Parser#parser_state.acc, <<"\r\n">>) of |
| 104 | + [_NotTerminatedLine] -> |
| 105 | + {ok, Parser, ParsedAcc}; |
| 106 | + [Line, Rest] -> |
| 107 | + ReplacedAccParser = replace_chunk(Parser, Rest), |
| 108 | + case parse_line(ReplacedAccParser, Line) of |
| 109 | + {consume_bytes, UpdatedParser} -> |
| 110 | + consume_bytes(UpdatedParser, ParsedAcc); |
| 111 | + {ok, UpdatedParser} -> |
| 112 | + consume_lines(UpdatedParser, ParsedAcc); |
| 113 | + {ok, UpdatedParser, Found} -> |
| 114 | + consume_lines(UpdatedParser, [Found | ParsedAcc]); |
| 115 | + {error, UpdatedParser, NotParsed} -> |
| 116 | + {error, UpdatedParser, ParsedAcc, NotParsed} |
| 117 | + end |
| 118 | + end. |
| 119 | + |
| 120 | +append_chunk(#parser_state{acc = undefined} = Parser, Chunk) -> |
| 121 | + Parser#parser_state{acc = Chunk}; |
| 122 | +append_chunk(#parser_state{acc = Acc} = Parser, Chunk) -> |
| 123 | + Parser#parser_state{acc = <<Acc/binary, Chunk/binary>>}. |
| 124 | + |
| 125 | +replace_chunk(Parser, <<>>) -> |
| 126 | + Parser#parser_state{acc = undefined}; |
| 127 | +replace_chunk(Parser, Chunk) -> |
| 128 | + Parser#parser_state{acc = Chunk}. |
| 129 | + |
| 130 | +parse_line( |
| 131 | + #parser_state{state = undefined} = Parser, <<"HTTP/1.0 ", C:3/binary, " ", _Txt/binary>> |
| 132 | +) -> |
| 133 | + StatusCode = binary_to_integer(C), |
| 134 | + {ok, Parser#parser_state{state = headers}, {status, StatusCode}}; |
| 135 | +parse_line( |
| 136 | + #parser_state{state = undefined} = Parser, <<"HTTP/1.1 ", C:3/binary, " ", _Txt/binary>> |
| 137 | +) -> |
| 138 | + StatusCode = binary_to_integer(C), |
| 139 | + {ok, Parser#parser_state{state = headers}, {status, StatusCode}}; |
| 140 | +parse_line(#parser_state{state = headers} = Parser, <<>>) -> |
| 141 | + {consume_bytes, Parser#parser_state{state = body}}; |
| 142 | +parse_line(#parser_state{state = headers, wanted_headers = WantedHeaders} = Parser, HeaderLine) -> |
| 143 | + case split_header(WantedHeaders, HeaderLine) of |
| 144 | + {ok, Name, Value} -> |
| 145 | + TrimmedValue = trim_left_spaces(Value, 0), |
| 146 | + UpdatedParser = |
| 147 | + case Name of |
| 148 | + <<"Content-Length">> -> |
| 149 | + RemainingLen = binary_to_integer(TrimmedValue), |
| 150 | + Parser#parser_state{remaining_body_bytes = RemainingLen}; |
| 151 | + _ -> |
| 152 | + Parser |
| 153 | + end, |
| 154 | + {ok, UpdatedParser, {header, {Name, TrimmedValue}}}; |
| 155 | + ignore -> |
| 156 | + {ok, Parser}; |
| 157 | + error -> |
| 158 | + {error, Parser, HeaderLine} |
| 159 | + end; |
| 160 | +parse_line(Parser, Any) -> |
| 161 | + {error, Parser, Any}. |
| 162 | + |
| 163 | +trim_left_spaces(Bin, Count) -> |
| 164 | + case Bin of |
| 165 | + <<_Bin:Count/binary, $\s, _Rest/binary>> -> |
| 166 | + trim_left_spaces(Bin, Count + 1); |
| 167 | + <<_Bin:Count/binary, NoLeftSpaces/binary>> -> |
| 168 | + NoLeftSpaces |
| 169 | + end. |
| 170 | + |
| 171 | +maybe_decrement(undefined, _B) -> |
| 172 | + undefined; |
| 173 | +maybe_decrement(A, B) -> |
| 174 | + A - B. |
| 175 | + |
| 176 | +maybe_handle_end_of_response(#parser_state{remaining_body_bytes = 0} = ParserState, AlreadyParsed) -> |
| 177 | + {ParserState#parser_state{state = done}, [done | AlreadyParsed]}; |
| 178 | +maybe_handle_end_of_response(ParserState, AlreadyParsed) -> |
| 179 | + {ParserState, AlreadyParsed}. |
| 180 | + |
| 181 | +split_header(all, HeaderLine) -> |
| 182 | + case binary:split(HeaderLine, <<":">>) of |
| 183 | + [_Invalid, <<>>] -> error; |
| 184 | + [Name, Value] -> {ok, Name, Value}; |
| 185 | + _ -> error |
| 186 | + end; |
| 187 | +split_header([], _HeaderLine) -> |
| 188 | + ignore; |
| 189 | +split_header([Name | Tail], HeaderLine) -> |
| 190 | + NameLen = byte_size(Name), |
| 191 | + case HeaderLine of |
| 192 | + <<Name:NameLen/binary, $:, Value/binary>> -> {ok, Name, Value}; |
| 193 | + _NotMatched -> split_header(Tail, HeaderLine) |
| 194 | + end. |
0 commit comments