cowboy_compress_h.erl 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. -export([early_error/5]).
  21. -record(state, {
  22. next :: any(),
  23. threshold :: non_neg_integer() | undefined,
  24. compress = undefined :: undefined | gzip,
  25. deflate = undefined :: undefined | zlib:zstream(),
  26. deflate_flush = sync :: none | sync
  27. }).
  28. -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
  29. -> {cowboy_stream:commands(), #state{}}.
  30. init(StreamID, Req, Opts) ->
  31. State0 = check_req(Req),
  32. CompressThreshold = maps:get(compress_threshold, Opts, 300),
  33. DeflateFlush = buffering_to_zflush(maps:get(compress_buffering, Opts, false)),
  34. {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts),
  35. fold(Commands0, State0#state{next=Next,
  36. threshold=CompressThreshold,
  37. deflate_flush=DeflateFlush}).
  38. -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
  39. -> {cowboy_stream:commands(), State} when State::#state{}.
  40. data(StreamID, IsFin, Data, State0=#state{next=Next0}) ->
  41. {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
  42. fold(Commands0, State0#state{next=Next}).
  43. -spec info(cowboy_stream:streamid(), any(), State)
  44. -> {cowboy_stream:commands(), State} when State::#state{}.
  45. info(StreamID, Info, State0=#state{next=Next0}) ->
  46. {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0),
  47. fold(Commands0, State0#state{next=Next}).
  48. -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
  49. terminate(StreamID, Reason, #state{next=Next, deflate=Z}) ->
  50. %% Clean the zlib:stream() in case something went wrong.
  51. %% In the normal scenario the stream is already closed.
  52. case Z of
  53. undefined -> ok;
  54. _ -> zlib:close(Z)
  55. end,
  56. cowboy_stream:terminate(StreamID, Reason, Next).
  57. -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
  58. cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
  59. when Resp::cowboy_stream:resp_command().
  60. early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
  61. cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
  62. %% Internal.
  63. %% Check if the client supports decoding of gzip responses.
  64. %%
  65. %% A malformed accept-encoding header is ignored (no compression).
  66. check_req(Req) ->
  67. try cowboy_req:parse_header(<<"accept-encoding">>, Req) of
  68. %% Client doesn't support any compression algorithm.
  69. undefined ->
  70. #state{compress=undefined};
  71. Encodings ->
  72. %% We only support gzip so look for it specifically.
  73. %% @todo A recipient SHOULD consider "x-gzip" to be
  74. %% equivalent to "gzip". (RFC7230 4.2.3)
  75. case [E || E={<<"gzip">>, Q} <- Encodings, Q =/= 0] of
  76. [] ->
  77. #state{compress=undefined};
  78. _ ->
  79. #state{compress=gzip}
  80. end
  81. catch
  82. _:_ ->
  83. #state{compress=undefined}
  84. end.
  85. %% Do not compress responses that contain the content-encoding header.
  86. check_resp_headers(#{<<"content-encoding">> := _}, State) ->
  87. State#state{compress=undefined};
  88. check_resp_headers(_, State) ->
  89. State.
  90. fold(Commands, State=#state{compress=undefined}) ->
  91. {Commands, State};
  92. fold(Commands, State) ->
  93. fold(Commands, State, []).
  94. fold([], State, Acc) ->
  95. {lists:reverse(Acc), State};
  96. %% We do not compress full sendfile bodies.
  97. fold([Response={response, _, _, {sendfile, _, _, _}}|Tail], State, Acc) ->
  98. fold(Tail, State, [Response|Acc]);
  99. %% We compress full responses directly, unless they are lower than
  100. %% the configured threshold or we find we are not able to by looking at the headers.
  101. fold([Response0={response, _, Headers, Body}|Tail],
  102. State0=#state{threshold=CompressThreshold}, Acc) ->
  103. case check_resp_headers(Headers, State0) of
  104. State=#state{compress=undefined} ->
  105. fold(Tail, State, [Response0|Acc]);
  106. State1 ->
  107. BodyLength = iolist_size(Body),
  108. if
  109. BodyLength =< CompressThreshold ->
  110. fold(Tail, State1, [Response0|Acc]);
  111. true ->
  112. {Response, State} = gzip_response(Response0, State1),
  113. fold(Tail, State, [Response|Acc])
  114. end
  115. end;
  116. %% Check headers and initiate compression...
  117. fold([Response0={headers, _, Headers}|Tail], State0, Acc) ->
  118. case check_resp_headers(Headers, State0) of
  119. State=#state{compress=undefined} ->
  120. fold(Tail, State, [Response0|Acc]);
  121. State1 ->
  122. {Response, State} = gzip_headers(Response0, State1),
  123. fold(Tail, State, [Response|Acc])
  124. end;
  125. %% then compress each data commands individually.
  126. fold([Data0={data, _, _}|Tail], State0=#state{compress=gzip}, Acc) ->
  127. {Data, State} = gzip_data(Data0, State0),
  128. fold(Tail, State, [Data|Acc]);
  129. %% When trailers are sent we need to end the compression.
  130. %% This results in an extra data command being sent.
  131. fold([Trailers={trailers, _}|Tail], State0=#state{compress=gzip}, Acc) ->
  132. {{data, fin, Data}, State} = gzip_data({data, fin, <<>>}, State0),
  133. fold(Tail, State, [Trailers, {data, nofin, Data}|Acc]);
  134. %% All the options from this handler can be updated for the current stream.
  135. %% The set_options command must be propagated as-is regardless.
  136. fold([SetOptions={set_options, Opts}|Tail], State=#state{
  137. threshold=CompressThreshold0, deflate_flush=DeflateFlush0}, Acc) ->
  138. CompressThreshold = maps:get(compress_threshold, Opts, CompressThreshold0),
  139. DeflateFlush = case Opts of
  140. #{compress_buffering := CompressBuffering} ->
  141. buffering_to_zflush(CompressBuffering);
  142. _ ->
  143. DeflateFlush0
  144. end,
  145. fold(Tail, State#state{threshold=CompressThreshold, deflate_flush=DeflateFlush},
  146. [SetOptions|Acc]);
  147. %% Otherwise, we have an unrelated command or compression is disabled.
  148. fold([Command|Tail], State, Acc) ->
  149. fold(Tail, State, [Command|Acc]).
  150. buffering_to_zflush(true) -> none;
  151. buffering_to_zflush(false) -> sync.
  152. gzip_response({response, Status, Headers, Body}, State) ->
  153. %% We can't call zlib:gzip/1 because it does an
  154. %% iolist_to_binary(GzBody) at the end to return
  155. %% a binary(). Therefore the code here is largely
  156. %% a duplicate of the code of that function.
  157. Z = zlib:open(),
  158. GzBody = try
  159. %% 31 = 16+?MAX_WBITS from zlib.erl
  160. %% @todo It might be good to allow them to be configured?
  161. zlib:deflateInit(Z, default, deflated, 31, 8, default),
  162. Gz = zlib:deflate(Z, Body, finish),
  163. zlib:deflateEnd(Z),
  164. Gz
  165. after
  166. zlib:close(Z)
  167. end,
  168. {{response, Status, vary(Headers#{
  169. <<"content-length">> => integer_to_binary(iolist_size(GzBody)),
  170. <<"content-encoding">> => <<"gzip">>
  171. }), GzBody}, State}.
  172. gzip_headers({headers, Status, Headers0}, State) ->
  173. Z = zlib:open(),
  174. %% We use the same arguments as when compressing the body fully.
  175. %% @todo It might be good to allow them to be configured?
  176. zlib:deflateInit(Z, default, deflated, 31, 8, default),
  177. Headers = maps:remove(<<"content-length">>, Headers0),
  178. {{headers, Status, vary(Headers#{
  179. <<"content-encoding">> => <<"gzip">>
  180. })}, State#state{deflate=Z}}.
  181. %% We must add content-encoding to vary if it's not already there.
  182. vary(Headers=#{<<"vary">> := Vary}) ->
  183. try cow_http_hd:parse_vary(iolist_to_binary(Vary)) of
  184. '*' -> Headers;
  185. List ->
  186. case lists:member(<<"accept-encoding">>, List) of
  187. true -> Headers;
  188. false -> Headers#{<<"vary">> => [Vary, <<", accept-encoding">>]}
  189. end
  190. catch _:_ ->
  191. %% The vary header is invalid. Probably empty. We replace it with ours.
  192. Headers#{<<"vary">> => <<"accept-encoding">>}
  193. end;
  194. vary(Headers) ->
  195. Headers#{<<"vary">> => <<"accept-encoding">>}.
  196. %% It is not possible to combine zlib and the sendfile
  197. %% syscall as far as I can tell, because the zlib format
  198. %% includes a checksum at the end of the stream. We have
  199. %% to read the file in memory, making this not suitable for
  200. %% large files.
  201. gzip_data({data, nofin, Sendfile={sendfile, _, _, _}},
  202. State=#state{deflate=Z, deflate_flush=Flush}) ->
  203. {ok, Data0} = read_file(Sendfile),
  204. Data = zlib:deflate(Z, Data0, Flush),
  205. {{data, nofin, Data}, State};
  206. gzip_data({data, fin, Sendfile={sendfile, _, _, _}}, State=#state{deflate=Z}) ->
  207. {ok, Data0} = read_file(Sendfile),
  208. Data = zlib:deflate(Z, Data0, finish),
  209. zlib:deflateEnd(Z),
  210. zlib:close(Z),
  211. {{data, fin, Data}, State#state{deflate=undefined}};
  212. gzip_data({data, nofin, Data0}, State=#state{deflate=Z, deflate_flush=Flush}) ->
  213. Data = zlib:deflate(Z, Data0, Flush),
  214. {{data, nofin, Data}, State};
  215. gzip_data({data, fin, Data0}, State=#state{deflate=Z}) ->
  216. Data = zlib:deflate(Z, Data0, finish),
  217. zlib:deflateEnd(Z),
  218. zlib:close(Z),
  219. {{data, fin, Data}, State#state{deflate=undefined}}.
  220. read_file({sendfile, Offset, Bytes, Path}) ->
  221. {ok, IoDevice} = file:open(Path, [read, raw, binary]),
  222. try
  223. _ = case Offset of
  224. 0 -> ok;
  225. _ -> file:position(IoDevice, {bof, Offset})
  226. end,
  227. file:read(IoDevice, Bytes)
  228. after
  229. file:close(IoDevice)
  230. end.