aboutsummaryrefslogtreecommitdiffstats
path: root/src/gun_http.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/gun_http.erl')
-rw-r--r--src/gun_http.erl207
1 files changed, 162 insertions, 45 deletions
diff --git a/src/gun_http.erl b/src/gun_http.erl
index 86fc436..745c2a9 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2014, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2014-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -18,16 +18,19 @@
-export([handle/2]).
-export([close/1]).
-export([keepalive/1]).
--export([request/6]).
-export([request/7]).
+-export([request/8]).
-export([data/4]).
-export([cancel/2]).
+-export([ws_upgrade/7]).
-type opts() :: [{version, cow_http:version()}].
-export_type([opts/0]).
-type io() :: head | {body, non_neg_integer()} | body_close | body_chunked.
+-type websocket_info() :: {websocket, reference(), binary(), [], []}. %% key, extensions, protocols
+
-record(http_state, {
owner :: pid(),
socket :: inet:socket() | ssl:sslsocket(),
@@ -35,7 +38,7 @@
version = 'HTTP/1.1' :: cow_http:version(),
connection = keepalive :: keepalive | close,
buffer = <<>> :: binary(),
- streams = [] :: [{reference(), boolean()}], %% ref + whether stream is alive
+ streams = [] :: [{reference() | websocket_info(), boolean()}], %% ref + whether stream is alive
in = head :: io(),
in_state :: {non_neg_integer(), non_neg_integer()},
out = head :: io()
@@ -47,6 +50,9 @@ init(Owner, Socket, Transport, [{version, Version}]) ->
#http_state{owner=Owner, socket=Socket, transport=Transport,
version=Version}.
+%% Stop looping when we got no more data.
+handle(<<>>, State) ->
+ State;
%% Close when server responds and we don't have any open streams.
handle(_, #http_state{streams=[]}) ->
close;
@@ -70,11 +76,11 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState,
{more, Data2, InState2} ->
send_data_if_alive(Data2, State, nofin),
State#http_state{buffer= <<>>, in_state=InState2};
- {more, Data2, _Length, InState2} ->
+ {more, Data2, Length, InState2} when is_integer(Length) ->
%% @todo See if we can recv faster than one message at a time.
send_data_if_alive(Data2, State, nofin),
State#http_state{buffer= <<>>, in_state=InState2};
- {more, Data2, _Length, Rest, InState2} ->
+ {more, Data2, Rest, InState2} ->
%% @todo See if we can recv faster than one message at a time.
send_data_if_alive(Data2, State, nofin),
State#http_state{buffer=Rest, in_state=InState2};
@@ -125,31 +131,40 @@ handle_head(Data, State=#http_state{owner=Owner, version=ClientVersion,
connection=Conn, streams=[{StreamRef, IsAlive}|_]}) ->
{Version, Status, _, Rest} = cow_http:parse_status_line(Data),
{Headers, Rest2} = cow_http:parse_headers(Rest),
- In = io_from_headers(Version, Headers),
- IsFin = case In of head -> fin; _ -> nofin end,
- case IsAlive of
- false ->
- ok;
- true ->
- Owner ! {gun_response, self(), StreamRef,
- IsFin, Status, Headers},
- ok
- end,
- Conn2 = if
- Conn =:= close -> close;
- Version =:= 'HTTP/1.0' -> close;
- ClientVersion =:= 'HTTP/1.0' -> close;
- true -> conn_from_headers(Headers)
- end,
- %% We always reset in_state even if not chunked.
- if
- IsFin =:= fin, Conn2 =:= close ->
- close;
- IsFin =:= fin ->
- handle(Rest2, end_stream(State#http_state{in=In,
- in_state={0, 0}, connection=Conn2}));
- true ->
- handle(Rest2, State#http_state{in=In, in_state={0, 0}, connection=Conn2})
+ case {Status, StreamRef} of
+ {101, {websocket, _, WsKey, WsExtensions, WsProtocols, WsOpts}} ->
+ ws_handshake(Rest2, State, Headers, WsKey, WsExtensions, WsProtocols, WsOpts);
+ _ ->
+ In = response_io_from_headers(Version, Headers),
+ IsFin = case In of head -> fin; _ -> nofin end,
+ case IsAlive of
+ false ->
+ ok;
+ true ->
+ StreamRef2 = case StreamRef of
+ {websocket, SR, _, _, _} -> SR;
+ _ -> StreamRef
+ end,
+ Owner ! {gun_response, self(), StreamRef2,
+ IsFin, Status, Headers},
+ ok
+ end,
+ Conn2 = if
+ Conn =:= close -> close;
+ Version =:= 'HTTP/1.0' -> close;
+ ClientVersion =:= 'HTTP/1.0' -> close;
+ true -> conn_from_headers(Version, Headers)
+ end,
+ %% We always reset in_state even if not chunked.
+ if
+ IsFin =:= fin, Conn2 =:= close ->
+ close;
+ IsFin =:= fin ->
+ handle(Rest2, end_stream(State#http_state{in=In,
+ in_state={0, 0}, connection=Conn2}));
+ true ->
+ handle(Rest2, State#http_state{in=In, in_state={0, 0}, connection=Conn2})
+ end
end.
send_data_if_alive(<<>>, _, nofin) ->
@@ -177,39 +192,38 @@ close_streams(Owner, [{StreamRef, _}|Tail]) ->
close_streams(Owner, Tail).
%% We can only keep-alive by sending an empty line in-between streams.
-keepalive(State=#http_state{socket=Socket, transport=Transport}) ->
+keepalive(State=#http_state{socket=Socket, transport=Transport, out=head}) ->
Transport:send(Socket, <<"\r\n">>),
+ State;
+keepalive(State) ->
State.
request(State=#http_state{socket=Socket, transport=Transport, version=Version,
- out=head}, StreamRef, Method, Host, Path, Headers) ->
- Headers2 = case Version of
- 'HTTP/1.0' -> lists:keydelete(<<"transfer-encoding">>, 1, Headers);
- 'HTTP/1.1' -> Headers
- end,
+ out=head}, StreamRef, Method, Host, Port, Path, Headers) ->
+ Headers2 = lists:keydelete(<<"transfer-encoding">>, 1, Headers),
Headers3 = case lists:keymember(<<"host">>, 1, Headers) of
- false -> [{<<"host">>, Host}|Headers2];
+ false -> [{<<"host">>, [Host, $:, integer_to_binary(Port)]}|Headers2];
true -> Headers2
end,
%% We use Headers2 because this is the smallest list.
- Conn = conn_from_headers(Headers2),
- Out = io_from_headers(Version, Headers2),
+ Conn = conn_from_headers(Version, Headers2),
+ Out = request_io_from_headers(Headers2),
Transport:send(Socket, cow_http:request(Method, Path, Version, Headers3)),
new_stream(State#http_state{connection=Conn, out=Out}, StreamRef).
request(State=#http_state{socket=Socket, transport=Transport, version=Version,
- out=head}, StreamRef, Method, Host, Path, Headers, Body) ->
+ out=head}, StreamRef, Method, Host, Port, Path, Headers, Body) ->
Headers2 = lists:keydelete(<<"content-length">>, 1,
lists:keydelete(<<"transfer-encoding">>, 1, Headers)),
Headers3 = case lists:keymember(<<"host">>, 1, Headers) of
- false -> [{<<"host">>, Host}|Headers2];
+ false -> [{<<"host">>, [Host, $:, integer_to_binary(Port)]}|Headers2];
true -> Headers2
end,
%% We use Headers2 because this is the smallest list.
- Conn = conn_from_headers(Headers2),
+ Conn = conn_from_headers(Version, Headers2),
Transport:send(Socket, [
cow_http:request(Method, Path, Version, [
- {<<"content-length">>, integer_to_list(iolist_size(Body))}
+ {<<"content-length">>, integer_to_binary(iolist_size(Body))}
|Headers3]),
Body]),
new_stream(State#http_state{connection=Conn}, StreamRef).
@@ -278,8 +292,10 @@ error_stream_not_found(State=#http_state{owner=Owner}) ->
%% Headers information retrieval.
-conn_from_headers(Headers) ->
+conn_from_headers(Version, Headers) ->
case lists:keyfind(<<"connection">>, 1, Headers) of
+ false when Version =:= 'HTTP/1.0' ->
+ close;
false ->
keepalive;
{_, ConnHd} ->
@@ -290,7 +306,20 @@ conn_from_headers(Headers) ->
end
end.
-io_from_headers(Version, Headers) ->
+request_io_from_headers(Headers) ->
+ case lists:keyfind(<<"content-length">>, 1, Headers) of
+ {_, <<"0">>} ->
+ head;
+ {_, Length} ->
+ {body, cow_http_hd:parse_content_length(Length)};
+ _ ->
+ case lists:keymember(<<"content-type">>, 1, Headers) of
+ true -> body_chunked;
+ false -> head
+ end
+ end.
+
+response_io_from_headers(Version, Headers) ->
case lists:keyfind(<<"content-length">>, 1, Headers) of
{_, <<"0">>} ->
head;
@@ -329,3 +358,91 @@ cancel_stream(State=#http_state{streams=Streams}, StreamRef) ->
end_stream(State=#http_state{streams=[_|Tail]}) ->
State#http_state{in=head, streams=Tail}.
+
+%% Websocket upgrade.
+
+%% Ensure version is 1.1.
+ws_upgrade(#http_state{version='HTTP/1.0'}, _, _, _, _, _, _) ->
+ error; %% @todo
+ws_upgrade(State=#http_state{socket=Socket, transport=Transport, out=head},
+ StreamRef, Host, Port, Path, Headers, WsOpts) ->
+ %% @todo Add option for setting protocol.
+ {ExtHeaders, GunExtensions} = case maps:get(compress, WsOpts, false) of
+ true -> {[{<<"sec-websocket-extensions">>, <<"permessage-deflate; client_max_window_bits; server_max_window_bits=15">>}],
+ [<<"permessage-deflate">>]};
+ false -> {[], []}
+ end,
+ Key = cow_ws:key(),
+ Headers2 = [
+ {<<"connection">>, <<"upgrade">>},
+ {<<"upgrade">>, <<"websocket">>},
+ {<<"sec-websocket-version">>, <<"13">>},
+ {<<"sec-websocket-key">>, Key}
+ |ExtHeaders
+ ],
+ IsSecure = Transport:secure(),
+ Headers3 = case lists:keymember(<<"host">>, 1, Headers) of
+ true -> Headers2;
+ false when Port =:= 80, not IsSecure -> [{<<"host">>, Host}|Headers2];
+ false when Port =:= 443, IsSecure -> [{<<"host">>, Host}|Headers2];
+ false -> [{<<"host">>, [Host, $:, integer_to_binary(Port)]}|Headers2]
+ end,
+ Transport:send(Socket, cow_http:request(<<"GET">>, Path, 'HTTP/1.1', Headers3)),
+ new_stream(State#http_state{connection=keepalive, out=head},
+ {websocket, StreamRef, Key, GunExtensions, [], WsOpts}).
+
+ws_handshake(Buffer, State, Headers, Key, GunExtensions, GunProtocols, Opts) ->
+ %% @todo check upgrade, connection
+ case lists:keyfind(<<"sec-websocket-accept">>, 1, Headers) of
+ false ->
+ close;
+ {_, Accept} ->
+ case cow_ws:encode_key(Key) of
+ Accept -> ws_handshake_extensions(Buffer, State, Headers, GunExtensions, GunProtocols, Opts);
+ _ -> close
+ end
+ end.
+
+ws_handshake_extensions(Buffer, State, Headers, GunExtensions, GunProtocols, Opts) ->
+ case lists:keyfind(<<"sec-websocket-extensions">>, 1, Headers) of
+ false ->
+ ws_handshake_protocols(Buffer, State, Headers, #{}, GunProtocols);
+ {_, ExtHd} ->
+ case ws_validate_extensions(cow_http_hd:parse_sec_websocket_extensions(ExtHd), GunExtensions, #{}, Opts) of
+ close -> close;
+ Extensions -> ws_handshake_protocols(Buffer, State, Headers, Extensions, GunProtocols)
+ end
+ end.
+
+ws_validate_extensions([], _, Acc, _) ->
+ Acc;
+ws_validate_extensions([{Name = <<"permessage-deflate">>, Params}|Tail], GunExts, Acc, Opts) ->
+ case lists:member(Name, GunExts) of
+ true ->
+ case cow_ws:validate_permessage_deflate(Params, Acc, Opts) of
+ {ok, Acc2} -> ws_validate_extensions(Tail, GunExts, Acc2, Opts);
+ error -> close
+ end;
+ %% Fail the connection if extension was not requested.
+ false ->
+ close
+ end;
+%% Fail the connection on unknown extension.
+ws_validate_extensions(_, _, _, _) ->
+ close.
+
+%% @todo Validate protocols.
+ws_handshake_protocols(Buffer, State, _Headers, Extensions, _GunProtocols = []) ->
+ Protocols = [],
+ ws_handshake_end(Buffer, State, Extensions, Protocols).
+
+ws_handshake_end(Buffer, #http_state{owner=Owner, socket=Socket, transport=Transport}, Extensions, Protocols) ->
+ %% Send ourselves the remaining buffer, if any.
+ _ = case Buffer of
+ <<>> ->
+ ok;
+ _ ->
+ {OK, _, _} = Transport:messages(),
+ self() ! {OK, Socket, Buffer}
+ end,
+ gun_ws:init(Owner, Socket, Transport, Extensions, Protocols).