1%% Copyright (c) 2014-2018, Loïc Hoguin <essen@ninenines.eu> 2%% 3%% Permission to use, copy, modify, and/or distribute this software for any 4%% purpose with or without fee is hereby granted, provided that the above 5%% copyright notice and this permission notice appear in all copies. 6%% 7%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 15-module(gun_http). 16 17-export([check_options/1]). 18-export([name/0]). 19-export([init/4]). 20-export([handle/2]). 21-export([close/1]). 22-export([keepalive/1]). 23-export([request/8]). 24-export([request/9]). 25-export([data/5]). 26-export([connect/5]). 27-export([cancel/3]). 28-export([down/1]). 29-export([ws_upgrade/7]). 30 31-type io() :: head | {body, non_neg_integer()} | body_close | body_chunked | body_trailer. 32 33%% @todo Make that a record. 34-type connect_info() :: {connect, reference(), gun:connect_destination()}. 35 36%% @todo Make that a record. 37-type websocket_info() :: {websocket, reference(), binary(), [binary()], gun:ws_opts()}. %% key, extensions, options 38 39-record(stream, { 40 ref :: reference() | connect_info() | websocket_info(), 41 reply_to :: pid(), 42 method :: binary(), 43 is_alive :: boolean(), 44 handler_state :: undefined | gun_content_handler:state() 45}). 46 47-record(http_state, { 48 owner :: pid(), 49 socket :: inet:socket() | ssl:sslsocket(), 50 transport :: module(), 51 version = 'HTTP/1.1' :: cow_http:version(), 52 content_handlers :: gun_content_handler:opt(), 53 connection = keepalive :: keepalive | close, 54 buffer = <<>> :: binary(), 55 streams = [] :: [#stream{}], 56 in = head :: io(), 57 in_state = {0, 0} :: {non_neg_integer(), non_neg_integer()}, 58 out = head :: io(), 59 transform_header_name :: fun((binary()) -> binary()) 60}). 61 62check_options(Opts) -> 63 do_check_options(maps:to_list(Opts)). 64 65do_check_options([]) -> 66 ok; 67do_check_options([Opt={content_handlers, Handlers}|Opts]) -> 68 case gun_content_handler:check_option(Handlers) of 69 ok -> do_check_options(Opts); 70 error -> {error, {options, {http, Opt}}} 71 end; 72do_check_options([{keepalive, infinity}|Opts]) -> 73 do_check_options(Opts); 74do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> 75 do_check_options(Opts); 76do_check_options([{transform_header_name, F}|Opts]) when is_function(F) -> 77 do_check_options(Opts); 78do_check_options([{version, V}|Opts]) when V =:= 'HTTP/1.1'; V =:= 'HTTP/1.0' -> 79 do_check_options(Opts); 80do_check_options([Opt|_]) -> 81 {error, {options, {http, Opt}}}. 82 83name() -> http. 84 85init(Owner, Socket, Transport, Opts) -> 86 Version = maps:get(version, Opts, 'HTTP/1.1'), 87 Handlers = maps:get(content_handlers, Opts, [gun_data_h]), 88 TransformHeaderName = maps:get(transform_header_name, Opts, fun (N) -> N end), 89 #http_state{owner=Owner, socket=Socket, transport=Transport, version=Version, 90 content_handlers=Handlers, transform_header_name=TransformHeaderName}. 91 92%% Stop looping when we got no more data. 93handle(<<>>, State) -> 94 {state, State}; 95%% Close when server responds and we don't have any open streams. 96handle(_, #http_state{streams=[]}) -> 97 close; 98%% Wait for the full response headers before trying to parse them. 99handle(Data, State=#http_state{in=head, buffer=Buffer}) -> 100 Data2 = << Buffer/binary, Data/binary >>, 101 case binary:match(Data2, <<"\r\n\r\n">>) of 102 nomatch -> {state, State#http_state{buffer=Data2}}; 103 {_, _} -> handle_head(Data2, State#http_state{buffer= <<>>}) 104 end; 105%% Everything sent to the socket until it closes is part of the response body. 106handle(Data, State=#http_state{in=body_close}) -> 107 {state, send_data_if_alive(Data, State, nofin)}; 108%% Chunked transfer-encoding may contain both data and trailers. 109handle(Data, State=#http_state{in=body_chunked, in_state=InState, 110 buffer=Buffer, connection=Conn}) -> 111 Buffer2 = << Buffer/binary, Data/binary >>, 112 case cow_http_te:stream_chunked(Buffer2, InState) of 113 more -> 114 {state, State#http_state{buffer=Buffer2}}; 115 {more, Data2, InState2} -> 116 {state, send_data_if_alive(Data2, 117 State#http_state{buffer= <<>>, in_state=InState2}, 118 nofin)}; 119 {more, Data2, Length, InState2} when is_integer(Length) -> 120 %% @todo See if we can recv faster than one message at a time. 121 {state, send_data_if_alive(Data2, 122 State#http_state{buffer= <<>>, in_state=InState2}, 123 nofin)}; 124 {more, Data2, Rest, InState2} -> 125 %% @todo See if we can recv faster than one message at a time. 126 {state, send_data_if_alive(Data2, 127 State#http_state{buffer=Rest, in_state=InState2}, 128 nofin)}; 129 {done, HasTrailers, Rest} -> 130 IsFin = case HasTrailers of 131 trailers -> nofin; 132 no_trailers -> fin 133 end, 134 %% I suppose it doesn't hurt to append an empty binary. 135 State1 = send_data_if_alive(<<>>, State, IsFin), 136 case {HasTrailers, Conn} of 137 {trailers, _} -> 138 handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}); 139 {no_trailers, keepalive} -> 140 handle(Rest, end_stream(State1#http_state{buffer= <<>>})); 141 {no_trailers, close} -> 142 close 143 end; 144 {done, Data2, HasTrailers, Rest} -> 145 IsFin = case HasTrailers of 146 trailers -> nofin; 147 no_trailers -> fin 148 end, 149 State1 = send_data_if_alive(Data2, State, IsFin), 150 case {HasTrailers, Conn} of 151 {trailers, _} -> 152 handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}); 153 {no_trailers, keepalive} -> 154 handle(Rest, end_stream(State1#http_state{buffer= <<>>})); 155 {no_trailers, close} -> 156 close 157 end 158 end; 159handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn, 160 streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}) -> 161 Data2 = << Buffer/binary, Data/binary >>, 162 case binary:match(Data2, <<"\r\n\r\n">>) of 163 nomatch -> {state, State#http_state{buffer=Data2}}; 164 {_, _} -> 165 {Trailers, Rest} = cow_http:parse_headers(Data2), 166 %% @todo We probably want to pass this to gun_content_handler? 167 ReplyTo ! {gun_trailers, self(), stream_ref(StreamRef), Trailers}, 168 case Conn of 169 keepalive -> 170 handle(Rest, end_stream(State#http_state{buffer= <<>>})); 171 close -> 172 close 173 end 174 end; 175%% We know the length of the rest of the body. 176handle(Data, State=#http_state{in={body, Length}, connection=Conn}) -> 177 DataSize = byte_size(Data), 178 if 179 %% More data coming. 180 DataSize < Length -> 181 {state, send_data_if_alive(Data, 182 State#http_state{in={body, Length - DataSize}}, 183 nofin)}; 184 %% Stream finished, no rest. 185 DataSize =:= Length -> 186 State1 = send_data_if_alive(Data, State, fin), 187 case Conn of 188 keepalive -> {state, end_stream(State1)}; 189 close -> close 190 end; 191 %% Stream finished, rest. 192 true -> 193 << Body:Length/binary, Rest/bits >> = Data, 194 State1 = send_data_if_alive(Body, State, fin), 195 case Conn of 196 keepalive -> handle(Rest, end_stream(State1)); 197 close -> close 198 end 199 end. 200 201handle_head(Data, State=#http_state{socket=Socket, version=ClientVersion, 202 content_handlers=Handlers0, connection=Conn, 203 streams=[Stream=#stream{ref=StreamRef, reply_to=ReplyTo, 204 method=Method, is_alive=IsAlive}|Tail]}) -> 205 {Version, Status, _, Rest} = cow_http:parse_status_line(Data), 206 {Headers, Rest2} = cow_http:parse_headers(Rest), 207 case {Status, StreamRef} of 208 {101, {websocket, RealStreamRef, WsKey, WsExtensions, WsOpts}} -> 209 ws_handshake(Rest2, State, RealStreamRef, Headers, WsKey, WsExtensions, WsOpts); 210 {_, {connect, RealStreamRef, Destination}} when Status >= 200, Status < 300 -> 211 case IsAlive of 212 false -> 213 ok; 214 true -> 215 ReplyTo ! {gun_response, self(), RealStreamRef, 216 fin, Status, Headers}, 217 ok 218 end, 219 %% We expect there to be no additional data after the CONNECT response. 220 <<>> = Rest2, 221 State2 = end_stream(State#http_state{streams=[Stream|Tail]}), 222 NewHost = maps:get(host, Destination), 223 NewPort = maps:get(port, Destination), 224 case Destination of 225 #{transport := tls} -> 226 TLSOpts = maps:get(tls_opts, Destination, []), 227 TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity), 228 case gun_tls:connect(Socket, TLSOpts, TLSTimeout) of 229 {ok, TLSSocket} -> 230 case ssl:negotiated_protocol(TLSSocket) of 231 {ok, <<"h2">>} -> 232 [{origin, <<"https">>, NewHost, NewPort, connect}, 233 {switch_transport, gun_tls, TLSSocket}, 234 {switch_protocol, gun_http2, State2}]; 235 _ -> 236 [{state, State2#http_state{socket=TLSSocket, transport=gun_tls}}, 237 {origin, <<"https">>, NewHost, NewPort, connect}, 238 {switch_transport, gun_tls, TLSSocket}] 239 end; 240 Error -> 241 Error 242 end; 243 _ -> 244 case maps:get(protocols, Destination, [http]) of 245 [http] -> 246 [{state, State2}, 247 {origin, <<"http">>, NewHost, NewPort, connect}]; 248 [http2] -> 249 [{origin, <<"http">>, NewHost, NewPort, connect}, 250 {switch_protocol, gun_http2, State2}] 251 end 252 end; 253 {_, _} when Status >= 100, Status =< 199 -> 254 ReplyTo ! {gun_inform, self(), stream_ref(StreamRef), Status, Headers}, 255 handle(Rest2, State); 256 _ -> 257 In = response_io_from_headers(Method, Version, Status, Headers), 258 IsFin = case In of head -> fin; _ -> nofin end, 259 Handlers = case IsAlive of 260 false -> 261 undefined; 262 true -> 263 ReplyTo ! {gun_response, self(), stream_ref(StreamRef), 264 IsFin, Status, Headers}, 265 case IsFin of 266 fin -> undefined; 267 nofin -> 268 gun_content_handler:init(ReplyTo, StreamRef, 269 Status, Headers, Handlers0) 270 end 271 end, 272 Conn2 = if 273 Conn =:= close -> close; 274 Version =:= 'HTTP/1.0' -> close; 275 ClientVersion =:= 'HTTP/1.0' -> close; 276 true -> conn_from_headers(Version, Headers) 277 end, 278 %% We always reset in_state even if not chunked. 279 if 280 IsFin =:= fin, Conn2 =:= close -> 281 close; 282 IsFin =:= fin -> 283 handle(Rest2, end_stream(State#http_state{in=In, 284 in_state={0, 0}, connection=Conn2, 285 streams=[Stream#stream{handler_state=Handlers}|Tail]})); 286 true -> 287 handle(Rest2, State#http_state{in=In, 288 in_state={0, 0}, connection=Conn2, 289 streams=[Stream#stream{handler_state=Handlers}|Tail]}) 290 end 291 end. 292 293stream_ref({connect, StreamRef, _}) -> StreamRef; 294stream_ref({websocket, StreamRef, _, _, _}) -> StreamRef; 295stream_ref(StreamRef) -> StreamRef. 296 297send_data_if_alive(<<>>, State, nofin) -> 298 State; 299%% @todo What if we receive data when the HEAD method was used? 300send_data_if_alive(Data, State=#http_state{streams=[Stream=#stream{ 301 is_alive=true, handler_state=Handlers0}|Tail]}, IsFin) -> 302 Handlers = gun_content_handler:handle(IsFin, Data, Handlers0), 303 State#http_state{streams=[Stream#stream{handler_state=Handlers}|Tail]}; 304send_data_if_alive(_, State, _) -> 305 State. 306 307close(State=#http_state{in=body_close, streams=[_|Tail]}) -> 308 _ = send_data_if_alive(<<>>, State, fin), 309 close_streams(Tail); 310close(#http_state{streams=Streams}) -> 311 close_streams(Streams). 312 313close_streams([]) -> 314 ok; 315close_streams([#stream{is_alive=false}|Tail]) -> 316 close_streams(Tail); 317close_streams([#stream{ref=StreamRef, reply_to=ReplyTo}|Tail]) -> 318 ReplyTo ! {gun_error, self(), StreamRef, {closed, 319 "The connection was lost."}}, 320 close_streams(Tail). 321 322%% We don't send a keep-alive when a CONNECT request was initiated. 323keepalive(State=#http_state{streams=[#stream{ref={connect, _, _}}]}) -> 324 State; 325%% We can only keep-alive by sending an empty line in-between streams. 326keepalive(State=#http_state{socket=Socket, transport=Transport, out=head}) -> 327 Transport:send(Socket, <<"\r\n">>), 328 State; 329keepalive(State) -> 330 State. 331 332request(State=#http_state{socket=Socket, transport=Transport, version=Version, 333 out=head}, StreamRef, ReplyTo, Method, Host, Port, Path, Headers) -> 334 Host2 = case Host of 335 {local, _SocketPath} -> <<>>; 336 Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); 337 _ -> Host 338 end, 339 Headers2 = lists:keydelete(<<"transfer-encoding">>, 1, Headers), 340 Headers3 = case lists:keymember(<<"host">>, 1, Headers) of 341 false -> [{<<"host">>, [Host2, $:, integer_to_binary(Port)]}|Headers2]; 342 true -> Headers2 343 end, 344 %% We use Headers2 because this is the smallest list. 345 Conn = conn_from_headers(Version, Headers2), 346 Out = request_io_from_headers(Headers2), 347 Headers4 = case Out of 348 body_chunked when Version =:= 'HTTP/1.0' -> Headers3; 349 body_chunked -> [{<<"transfer-encoding">>, <<"chunked">>}|Headers3]; 350 _ -> Headers3 351 end, 352 Headers5 = transform_header_names(State, Headers4), 353 Transport:send(Socket, cow_http:request(Method, Path, Version, Headers5)), 354 new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo, Method). 355 356request(State=#http_state{socket=Socket, transport=Transport, version=Version, 357 out=head}, StreamRef, ReplyTo, Method, Host, Port, Path, Headers, Body) -> 358 Host2 = case Host of 359 {local, _SocketPath} -> <<>>; 360 Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); 361 _ -> Host 362 end, 363 Headers2 = lists:keydelete(<<"content-length">>, 1, 364 lists:keydelete(<<"transfer-encoding">>, 1, Headers)), 365 Headers3 = case lists:keymember(<<"host">>, 1, Headers) of 366 false -> [{<<"host">>, [Host2, $:, integer_to_binary(Port)]}|Headers2]; 367 true -> Headers2 368 end, 369 Headers4 = transform_header_names(State, Headers3), 370 %% We use Headers2 because this is the smallest list. 371 Conn = conn_from_headers(Version, Headers2), 372 Transport:send(Socket, [ 373 cow_http:request(Method, Path, Version, [ 374 {<<"content-length">>, integer_to_binary(iolist_size(Body))} 375 |Headers4]), 376 Body]), 377 new_stream(State#http_state{connection=Conn}, StreamRef, ReplyTo, Method). 378 379transform_header_names(#http_state{transform_header_name = Fun}, Headers) -> 380 lists:keymap(Fun, 1, Headers). 381 382%% We are expecting a new stream. 383data(State=#http_state{out=head}, StreamRef, ReplyTo, _, _) -> 384 error_stream_closed(State, StreamRef, ReplyTo); 385%% There are no active streams. 386data(State=#http_state{streams=[]}, StreamRef, ReplyTo, _, _) -> 387 error_stream_not_found(State, StreamRef, ReplyTo); 388%% We can only send data on the last created stream. 389data(State=#http_state{socket=Socket, transport=Transport, version=Version, 390 out=Out, streams=Streams}, StreamRef, ReplyTo, IsFin, Data) -> 391 case lists:last(Streams) of 392 #stream{ref=StreamRef, is_alive=true} -> 393 DataLength = iolist_size(Data), 394 case Out of 395 body_chunked when Version =:= 'HTTP/1.1', IsFin =:= fin -> 396 case Data of 397 <<>> -> 398 Transport:send(Socket, cow_http_te:last_chunk()); 399 _ -> 400 Transport:send(Socket, [ 401 cow_http_te:chunk(Data), 402 cow_http_te:last_chunk() 403 ]) 404 end, 405 State#http_state{out=head}; 406 body_chunked when Version =:= 'HTTP/1.1' -> 407 Transport:send(Socket, cow_http_te:chunk(Data)), 408 State; 409 {body, Length} when DataLength =< Length -> 410 Transport:send(Socket, Data), 411 Length2 = Length - DataLength, 412 if 413 Length2 =:= 0, IsFin =:= fin -> 414 State#http_state{out=head}; 415 Length2 > 0, IsFin =:= nofin -> 416 State#http_state{out={body, Length2}} 417 end; 418 body_chunked -> %% HTTP/1.0 419 Transport:send(Socket, Data), 420 State 421 end; 422 _ -> 423 error_stream_not_found(State, StreamRef, ReplyTo) 424 end. 425 426connect(State=#http_state{streams=Streams}, StreamRef, ReplyTo, _, _) when Streams =/= [] -> 427 ReplyTo ! {gun_error, self(), StreamRef, {badstate, 428 "CONNECT can only be used with HTTP/1.1 when no other streams are active."}}, 429 State; 430connect(State=#http_state{socket=Socket, transport=Transport, version=Version}, 431 StreamRef, ReplyTo, Destination=#{host := Host0}, Headers0) -> 432 Host = case Host0 of 433 Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); 434 _ -> Host0 435 end, 436 Port = maps:get(port, Destination, 1080), 437 Authority = [Host, $:, integer_to_binary(Port)], 438 Headers1 = lists:keydelete(<<"content-length">>, 1, 439 lists:keydelete(<<"transfer-encoding">>, 1, Headers0)), 440 Headers2 = case lists:keymember(<<"host">>, 1, Headers1) of 441 false -> [{<<"host">>, Authority}|Headers1]; 442 true -> Headers1 443 end, 444 HasProxyAuthorization = lists:keymember(<<"proxy-authorization">>, 1, Headers2), 445 Headers3 = case {HasProxyAuthorization, Destination} of 446 {false, #{username := UserID, password := Password}} -> 447 [{<<"proxy-authorization">>, [ 448 <<"Basic ">>, 449 base64:encode(iolist_to_binary([UserID, $:, Password]))]} 450 |Headers2]; 451 _ -> 452 Headers2 453 end, 454 Headers = transform_header_names(State, Headers3), 455 Transport:send(Socket, [ 456 cow_http:request(<<"CONNECT">>, Authority, Version, Headers) 457 ]), 458 new_stream(State, {connect, StreamRef, Destination}, ReplyTo, <<"CONNECT">>). 459 460%% We can't cancel anything, we can just stop forwarding messages to the owner. 461cancel(State, StreamRef, ReplyTo) -> 462 case is_stream(State, StreamRef) of 463 true -> 464 cancel_stream(State, StreamRef); 465 false -> 466 error_stream_not_found(State, StreamRef, ReplyTo) 467 end. 468 469%% HTTP does not provide any way to figure out what streams are unprocessed. 470down(#http_state{streams=Streams}) -> 471 KilledStreams = [case Ref of 472 {connect, Ref2, _} -> Ref2; 473 {websocket, Ref2, _, _, _} -> Ref2; 474 _ -> Ref 475 end || #stream{ref=Ref} <- Streams], 476 {KilledStreams, []}. 477 478error_stream_closed(State, StreamRef, ReplyTo) -> 479 ReplyTo ! {gun_error, self(), StreamRef, {badstate, 480 "The stream has already been closed."}}, 481 State. 482 483error_stream_not_found(State, StreamRef, ReplyTo) -> 484 ReplyTo ! {gun_error, self(), StreamRef, {badstate, 485 "The stream cannot be found."}}, 486 State. 487 488%% Headers information retrieval. 489 490conn_from_headers(Version, Headers) -> 491 case lists:keyfind(<<"connection">>, 1, Headers) of 492 false when Version =:= 'HTTP/1.0' -> 493 close; 494 false -> 495 keepalive; 496 {_, ConnHd} -> 497 ConnList = cow_http_hd:parse_connection(ConnHd), 498 case lists:member(<<"keep-alive">>, ConnList) of 499 true -> keepalive; 500 false -> close 501 end 502 end. 503 504request_io_from_headers(Headers) -> 505 case lists:keyfind(<<"content-length">>, 1, Headers) of 506 {_, <<"0">>} -> 507 head; 508 {_, Length} -> 509 {body, cow_http_hd:parse_content_length(Length)}; 510 _ -> 511 case lists:keymember(<<"content-type">>, 1, Headers) of 512 true -> body_chunked; 513 false -> head 514 end 515 end. 516 517response_io_from_headers(<<"HEAD">>, _, _, _) -> 518 head; 519response_io_from_headers(_, _, Status, _) when (Status =:= 204) or (Status =:= 304) -> 520 head; 521response_io_from_headers(_, Version, _Status, Headers) -> 522 case lists:keyfind(<<"transfer-encoding">>, 1, Headers) of 523 {_, TE} when Version =:= 'HTTP/1.1' -> 524 case cow_http_hd:parse_transfer_encoding(TE) of 525 [<<"chunked">>] -> body_chunked; 526 [<<"identity">>] -> body_close 527 end; 528 _ -> 529 case lists:keyfind(<<"content-length">>, 1, Headers) of 530 {_, <<"0">>} -> 531 head; 532 {_, Length} -> 533 {body, cow_http_hd:parse_content_length(Length)}; 534 _ -> 535 body_close 536 end 537 end. 538 539%% Streams. 540 541new_stream(State=#http_state{streams=Streams}, StreamRef, ReplyTo, Method) -> 542 State#http_state{streams=Streams 543 ++ [#stream{ref=StreamRef, reply_to=ReplyTo, 544 method=iolist_to_binary(Method), is_alive=true}]}. 545 546is_stream(#http_state{streams=Streams}, StreamRef) -> 547 lists:keymember(StreamRef, #stream.ref, Streams). 548 549cancel_stream(State=#http_state{streams=Streams}, StreamRef) -> 550 Streams2 = [case Ref of 551 StreamRef -> 552 Tuple#stream{is_alive=false}; 553 _ -> 554 Tuple 555 end || Tuple = #stream{ref=Ref} <- Streams], 556 State#http_state{streams=Streams2}. 557 558end_stream(State=#http_state{streams=[_|Tail]}) -> 559 State#http_state{in=head, streams=Tail}. 560 561%% Websocket upgrade. 562 563%% Ensure version is 1.1. 564ws_upgrade(#http_state{version='HTTP/1.0'}, _, _, _, _, _, _) -> 565 error; %% @todo 566ws_upgrade(State=#http_state{socket=Socket, transport=Transport, owner=Owner, out=head}, 567 StreamRef, Host, Port, Path, Headers0, WsOpts) -> 568 {Headers1, GunExtensions} = case maps:get(compress, WsOpts, false) of 569 true -> {[{<<"sec-websocket-extensions">>, 570 <<"permessage-deflate; client_max_window_bits; server_max_window_bits=15">>} 571 |Headers0], 572 [<<"permessage-deflate">>]}; 573 false -> {Headers0, []} 574 end, 575 Headers2 = case maps:get(protocols, WsOpts, []) of 576 [] -> Headers1; 577 ProtoOpt -> 578 << _, _, Proto/bits >> = iolist_to_binary([[<<", ">>, P] || {P, _} <- ProtoOpt]), 579 [{<<"sec-websocket-protocol">>, Proto}|Headers1] 580 end, 581 Key = cow_ws:key(), 582 Headers3 = [ 583 {<<"connection">>, <<"upgrade">>}, 584 {<<"upgrade">>, <<"websocket">>}, 585 {<<"sec-websocket-version">>, <<"13">>}, 586 {<<"sec-websocket-key">>, Key} 587 |Headers2 588 ], 589 IsSecure = Transport =:= gun_tls, 590 Headers = case lists:keymember(<<"host">>, 1, Headers0) of 591 true -> Headers3; 592 false when Port =:= 80, not IsSecure -> [{<<"host">>, Host}|Headers3]; 593 false when Port =:= 443, IsSecure -> [{<<"host">>, Host}|Headers3]; 594 false -> [{<<"host">>, [Host, $:, integer_to_binary(Port)]}|Headers3] 595 end, 596 Transport:send(Socket, cow_http:request(<<"GET">>, Path, 'HTTP/1.1', Headers)), 597 new_stream(State#http_state{connection=keepalive, out=head}, 598 {websocket, StreamRef, Key, GunExtensions, WsOpts}, Owner, <<"GET">>). 599 600ws_handshake(Buffer, State, StreamRef, Headers, Key, GunExtensions, Opts) -> 601 %% @todo check upgrade, connection 602 case lists:keyfind(<<"sec-websocket-accept">>, 1, Headers) of 603 false -> 604 close; 605 {_, Accept} -> 606 case cow_ws:encode_key(Key) of 607 Accept -> 608 ws_handshake_extensions(Buffer, State, StreamRef, 609 Headers, GunExtensions, Opts); 610 _ -> 611 close 612 end 613 end. 614 615ws_handshake_extensions(Buffer, State, StreamRef, Headers, GunExtensions, Opts) -> 616 case lists:keyfind(<<"sec-websocket-extensions">>, 1, Headers) of 617 false -> 618 ws_handshake_protocols(Buffer, State, StreamRef, Headers, #{}, Opts); 619 {_, ExtHd} -> 620 case ws_validate_extensions(cow_http_hd:parse_sec_websocket_extensions(ExtHd), GunExtensions, #{}, Opts) of 621 close -> 622 close; 623 Extensions -> 624 ws_handshake_protocols(Buffer, State, StreamRef, Headers, Extensions, Opts) 625 end 626 end. 627 628ws_validate_extensions([], _, Acc, _) -> 629 Acc; 630ws_validate_extensions([{Name = <<"permessage-deflate">>, Params}|Tail], GunExts, Acc, Opts) -> 631 case lists:member(Name, GunExts) of 632 true -> 633 case cow_ws:validate_permessage_deflate(Params, Acc, Opts) of 634 {ok, Acc2} -> ws_validate_extensions(Tail, GunExts, Acc2, Opts); 635 error -> close 636 end; 637 %% Fail the connection if extension was not requested. 638 false -> 639 close 640 end; 641%% Fail the connection on unknown extension. 642ws_validate_extensions(_, _, _, _) -> 643 close. 644 645%% @todo Validate protocols. 646ws_handshake_protocols(Buffer, State, StreamRef, Headers, Extensions, Opts) -> 647 case lists:keyfind(<<"sec-websocket-protocol">>, 1, Headers) of 648 false -> 649 ws_handshake_end(Buffer, State, StreamRef, Headers, Extensions, 650 maps:get(default_protocol, Opts, gun_ws_h), Opts); 651 {_, Proto} -> 652 ProtoOpt = maps:get(protocols, Opts, []), 653 case lists:keyfind(Proto, 1, ProtoOpt) of 654 {_, Handler} -> 655 ws_handshake_end(Buffer, State, StreamRef, 656 Headers, Extensions, Handler, Opts); 657 false -> 658 close 659 end 660 end. 661 662ws_handshake_end(Buffer, #http_state{owner=Owner, socket=Socket, transport=Transport}, 663 StreamRef, Headers, Extensions, Handler, Opts) -> 664 %% Send ourselves the remaining buffer, if any. 665 _ = case Buffer of 666 <<>> -> 667 ok; 668 _ -> 669 {OK, _, _} = Transport:messages(), 670 self() ! {OK, Socket, Buffer} 671 end, 672 gun_ws:init(Owner, Socket, Transport, StreamRef, Headers, Extensions, Handler, Opts). 673