cowboy_compress_h.erl 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. %% Copyright (c) 2017, 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. -module(cowboy_compress_h).
  15. -behavior(cowboy_stream).
  16. -export([init/3]).
  17. -export([data/4]).
  18. -export([info/3]).
  19. -export([terminate/3]).
  20. -record(state, {
  21. next :: any(),
  22. compress = undefined :: undefined | gzip,
  23. deflate = undefined :: undefined | zlib:zstream()
  24. }).
  25. -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
  26. -> {cowboy_stream:commands(), #state{}}.
  27. init(StreamID, Req, Opts) ->
  28. State0 = check_req(Req),
  29. {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts),
  30. fold(Commands0, State0#state{next=Next}).
  31. -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
  32. -> {cowboy_stream:commands(), State} when State::#state{}.
  33. data(StreamID, IsFin, Data, State0=#state{next=Next0}) ->
  34. {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
  35. fold(Commands0, State0#state{next=Next}).
  36. -spec info(cowboy_stream:streamid(), any(), State)
  37. -> {cowboy_stream:commands(), State} when State::#state{}.
  38. info(StreamID, Info, State0=#state{next=Next0}) ->
  39. {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0),
  40. fold(Commands0, State0#state{next=Next}).
  41. -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
  42. terminate(StreamID, Reason, #state{next=Next, deflate=Z}) ->
  43. %% Clean the zlib:stream() in case something went wrong.
  44. %% In the normal scenario the stream is already closed.
  45. case Z of
  46. undefined -> ok;
  47. _ -> zlib:close(Z)
  48. end,
  49. cowboy_stream:terminate(StreamID, Reason, Next).
  50. %% Internal.
  51. %% Check if the client supports decoding of gzip responses.
  52. check_req(Req) ->
  53. case cowboy_req:parse_header(<<"accept-encoding">>, Req) of
  54. %% Client doesn't support any compression algorithm.
  55. undefined ->
  56. #state{compress=undefined};
  57. Encodings ->
  58. %% We only support gzip so look for it specifically.
  59. %% @todo A recipient SHOULD consider "x-gzip" to be
  60. %% equivalent to "gzip". (RFC7230 4.2.3)
  61. case [E || E={<<"gzip">>, Q} <- Encodings, Q =/= 0] of
  62. [] ->
  63. #state{compress=undefined};
  64. _ ->
  65. #state{compress=gzip}
  66. end
  67. end.
  68. %% Do not compress responses that contain the content-encoding header.
  69. check_resp_headers(#{<<"content-encoding">> := _}, State) ->
  70. State#state{compress=undefined};
  71. check_resp_headers(_, State) ->
  72. State.
  73. fold(Commands, State=#state{compress=undefined}) ->
  74. {Commands, State};
  75. fold(Commands, State) ->
  76. fold(Commands, State, []).
  77. fold([], State, Acc) ->
  78. {lists:reverse(Acc), State};
  79. %% We do not compress sendfile bodies.
  80. fold([Response={response, _, _, {sendfile, _, _, _}}|Tail], State, Acc) ->
  81. fold(Tail, State, [Response|Acc]);
  82. %% We compress full responses directly, unless they are lower than
  83. %% 300 bytes or we find we are not able to by looking at the headers.
  84. %% @todo It might be good to allow this size to be configured?
  85. fold([Response0={response, _, Headers, Body}|Tail], State0, Acc) ->
  86. case check_resp_headers(Headers, State0) of
  87. State=#state{compress=undefined} ->
  88. fold(Tail, State, [Response0|Acc]);
  89. State1 ->
  90. BodyLength = iolist_size(Body),
  91. if
  92. BodyLength =< 300 ->
  93. fold(Tail, State1, [Response0|Acc]);
  94. true ->
  95. {Response, State} = gzip_response(Response0, State1),
  96. fold(Tail, State, [Response|Acc])
  97. end
  98. end;
  99. %% Check headers and initiate compression...
  100. fold([Response0={headers, _, Headers}|Tail], State0, Acc) ->
  101. case check_resp_headers(Headers, State0) of
  102. State=#state{compress=undefined} ->
  103. fold(Tail, State, [Response0|Acc]);
  104. State1 ->
  105. {Response, State} = gzip_headers(Response0, State1),
  106. fold(Tail, State, [Response|Acc])
  107. end;
  108. %% then compress each data commands individually.
  109. fold([Data0={data, _, _}|Tail], State0=#state{compress=gzip}, Acc) ->
  110. {Data, State} = gzip_data(Data0, State0),
  111. fold(Tail, State, [Data|Acc]);
  112. %% Otherwise, we either have an unrelated command, or a data command
  113. %% with compression disabled.
  114. fold([Command|Tail], State, Acc) ->
  115. fold(Tail, State, [Command|Acc]).
  116. gzip_response({response, Status, Headers, Body}, State) ->
  117. %% We can't call zlib:gzip/1 because it does an
  118. %% iolist_to_binary(GzBody) at the end to return
  119. %% a binary(). Therefore the code here is largely
  120. %% a duplicate of the code of that function.
  121. Z = zlib:open(),
  122. GzBody = try
  123. %% 31 = 16+?MAX_WBITS from zlib.erl
  124. %% @todo It might be good to allow them to be configured?
  125. zlib:deflateInit(Z, default, deflated, 31, 8, default),
  126. Gz = zlib:deflate(Z, Body, finish),
  127. zlib:deflateEnd(Z),
  128. Gz
  129. after
  130. zlib:close(Z)
  131. end,
  132. {{response, Status, Headers#{
  133. <<"content-length">> => integer_to_binary(iolist_size(GzBody)),
  134. <<"content-encoding">> => <<"gzip">>
  135. }, GzBody}, State}.
  136. gzip_headers({headers, Status, Headers0}, State) ->
  137. Z = zlib:open(),
  138. %% We use the same arguments as when compressing the body fully.
  139. %% @todo It might be good to allow them to be configured?
  140. zlib:deflateInit(Z, default, deflated, 31, 8, default),
  141. Headers = maps:remove(<<"content-length">>, Headers0),
  142. {{headers, Status, Headers#{
  143. <<"content-encoding">> => <<"gzip">>
  144. }}, State#state{deflate=Z}}.
  145. gzip_data({data, nofin, Data0}, State=#state{deflate=Z}) ->
  146. Data = zlib:deflate(Z, Data0),
  147. {{data, nofin, Data}, State};
  148. gzip_data({data, fin, Data0}, State=#state{deflate=Z}) ->
  149. Data = zlib:deflate(Z, Data0, finish),
  150. zlib:deflateEnd(Z),
  151. zlib:close(Z),
  152. {{data, fin, Data}, State#state{deflate=undefined}}.