Search code examples
erlanggen-tcp

Erlang gen_tcp:recv http_request abs_path


I'm writing code in Erlang that accepts HTTP requests. I have working code which is shown below.

The problem I have is that I'm unsure about the returned result of gen_tcp:recv.

I create a listening socket and accept sockets using

{ok, ListenSock}=gen_tcp:listen(Port, [list,{active, false},{packet,http}])
{ok, Sock}=gen_tcp:accept(ListenSock),

I accept a GET request (or any other) using

{ok, {http_request, Method, Path, Version}} = gen_tcp:recv(Sock, 0),
handle_get(Sock, Path);

Then, to get url parameters (CGI parameters, e.g., ?foo=1&bar=2) I have to match Path with a structure {abs_path, RelativePath}.

handle_get(Sock, ReqPath) ->
    {abs_path, RelPath} = ReqPath,
    Parameters = string:substr(RelPath, string:str(RelPath, "?") + 1),

While I was reading through the docs of Erlang about gen_tcp and more specifically the recv method I found the page describing HttpPacket.

The grammar on the page clearly shows that Path in HttpPacket, and in this case the HttpRequest type, can have multiple types of HttpUri.

HttpRequest = {http_request, HttpMethod, HttpUri, HttpVersion}
HttpUri = '*'
        | {absoluteURI,
           http | https,
           Host :: HttpString,
           Port :: inet:port_number() | undefined,
           Path :: HttpString}
        | {scheme, Scheme :: HttpString, HttpString}
        | {abs_path, HttpString}
        | HttpString

I understand that I have to support each of these possible cases, however I am not sure. I am also wondering how I can test these cases. I have tried using curl and RESTClient in Firefox and both of them make gen_tcp:recv return abs_path.

So to be clear, how is determined whether the request holds {abs_path, HttpString}, {scheme, Scheme :: HttpString, HttpString} or {absoluteURI,...} and do I need to support all of them?

Full listing

start(Port)->
    {ok, ListenSock}=gen_tcp:listen(Port, [list,{active, false},{packet,http}]),
    loop(ListenSock).


loop(ListenSock) -> 
    {ok, Sock}=gen_tcp:accept(ListenSock),
    spawn(?MODULE, handle_request, [Sock]),
    loop(ListenSock).

%% Takes a TCP socket and receives 
%% http://erlang.org/doc/man/erlang.html#decode_packet-3
handle_request(Sock) ->
    {ok, {http_request, Method, Path, _Version}} = gen_tcp:recv(Sock, 0),

    case (Method) of
        'GET' ->
            handle_get(Sock, Path);
        _ -> 
            send_unsupported_error(Sock)
    end.

handle_get(Sock, ReqPath) ->
    {abs_path, RelPath} = ReqPath,
    Parameters = string:substr(RelPath, string:str(RelPath, "?") + 1),
    %% Debugging
    ParsedParms = httpd:parse_query(Parameters),
    io:fwrite("Full Path: ~p~nParameters: ~p~n", [RelPath, ParsedParms]),
    %% End Debugging
    send_accept(Sock).

Solution

  • You can use a simple network-capable client such as netcat (/usr/bin/nc on my system) to send whatever form of request you like. For example, the following connects to a web server listening on localhost port 8000 and sends a GET request where the path is a URL (note that the $ denotes a shell prompt):

    $ nc localhost 8000
    GET http://stackoverflow.com HTTP/1.1
    
    $
    

    The nc program reads from its standard input. Be sure to hit enter twice after the GET line to properly indicate the end of the HTTP headers. This results in the gen_tcp:recv call on the server returning:

    {absoluteURI,http,"stackoverflow.com",undefined,"/"}
    

    Similarly, the following will return a path from gen_tcp:recv that's not an {abs_path, ...} tuple, but rather just "../foo":

    $ nc localhost 8000
    GET ../foo HTTP/1.1
    
    $
    

    You can easily set up test variations like these in text files and feed them into nc using stdin redirection.