Subversion Repositories SE.SVN

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
12 7u83 1
%% @author Bob Ippolito <bob@mochimedia.com>
2
%% @copyright 2007 Mochi Media, Inc.
3
%%
4
%% Permission is hereby granted, free of charge, to any person obtaining a
5
%% copy of this software and associated documentation files (the "Software"),
6
%% to deal in the Software without restriction, including without limitation
7
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
8
%% and/or sell copies of the Software, and to permit persons to whom the
9
%% Software is furnished to do so, subject to the following conditions:
10
%%
11
%% The above copyright notice and this permission notice shall be included in
12
%% all copies or substantial portions of the Software.
13
%%
14
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17
%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20
%% DEALINGS IN THE SOFTWARE.
21
 
22
%% @doc MochiWeb HTTP Request abstraction.
23
 
24
-module(mochiweb_request).
25
 
26
-author('bob@mochimedia.com').
27
 
28
-include_lib("kernel/include/file.hrl").
29
 
30
-include("internal.hrl").
31
 
32
-define(QUIP, "Any of you quaids got a smint?").
33
 
34
-export([new/5, new/6]).
35
 
36
-export([dump/1, get/2, get_combined_header_value/2,
37
	 get_header_value/2, get_primary_header_value/2]).
38
 
39
-export([recv/2, recv/3, recv_body/1, recv_body/2,
40
	 send/2, stream_body/4, stream_body/5]).
41
 
42
-export([start_raw_response/2, start_response/2,
43
	 start_response_length/2]).
44
 
45
-export([ok/2, respond/2]).
46
 
47
-export([not_found/1, not_found/2]).
48
 
49
-export([parse_post/1, parse_qs/1]).
50
 
51
-export([cleanup/1, should_close/1]).
52
 
53
-export([get_cookie_value/2, parse_cookie/1]).
54
 
55
-export([serve_file/3, serve_file/4]).
56
 
57
-export([accepted_encodings/2]).
58
 
59
-export([accepted_content_types/2,
60
	 accepts_content_type/2]).
61
 
62
-define(SAVE_QS, mochiweb_request_qs).
63
 
64
-define(SAVE_PATH, mochiweb_request_path).
65
 
66
-define(SAVE_RECV, mochiweb_request_recv).
67
 
68
-define(SAVE_BODY, mochiweb_request_body).
69
 
70
-define(SAVE_BODY_LENGTH, mochiweb_request_body_length).
71
 
72
-define(SAVE_POST, mochiweb_request_post).
73
 
74
-define(SAVE_COOKIE, mochiweb_request_cookie).
75
 
76
-define(SAVE_FORCE_CLOSE, mochiweb_request_force_close).
77
 
78
%% @type key() = atom() | string() | binary()
79
%% @type value() = atom() | string() | binary() | integer()
80
%% @type headers(). A mochiweb_headers structure.
81
%% @type request(). A mochiweb_request parameterized module instance.
82
%% @type response(). A mochiweb_response parameterized module instance.
83
%% @type ioheaders() = headers() | [{key(), value()}].
84
 
85
% 5 minute default idle timeout
86
-define(IDLE_TIMEOUT, 300000).
87
 
88
% Maximum recv_body() length of 1MB
89
-define(MAX_RECV_BODY, 1024 * 1024).
90
 
91
%% @spec new(Socket, Method, RawPath, Version, headers()) -> request()
92
%% @doc Create a new request instance.
93
new(Socket, Method, RawPath, Version, Headers) ->
94
    new(Socket, [], Method, RawPath, Version, Headers).
95
 
96
%% @spec new(Socket, Opts, Method, RawPath, Version, headers()) -> request()
97
%% @doc Create a new request instance.
98
new(Socket, Opts, Method, RawPath, Version, Headers) ->
99
    {?MODULE,
100
     [Socket, Opts, Method, RawPath, Version, Headers]}.
101
 
102
%% @spec get_header_value(K, request()) -> undefined | Value
103
%% @doc Get the value of a given request header.
104
get_header_value(K,
105
		 {?MODULE,
106
		  [_Socket, _Opts, _Method, _RawPath, _Version,
107
		   Headers]}) ->
108
    mochiweb_headers:get_value(K, Headers).
109
 
110
get_primary_header_value(K,
111
			 {?MODULE,
112
			  [_Socket, _Opts, _Method, _RawPath, _Version,
113
			   Headers]}) ->
114
    mochiweb_headers:get_primary_value(K, Headers).
115
 
116
get_combined_header_value(K,
117
			  {?MODULE,
118
			   [_Socket, _Opts, _Method, _RawPath, _Version,
119
			    Headers]}) ->
120
    mochiweb_headers:get_combined_value(K, Headers).
121
 
122
%% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
123
 
124
%% @spec get(field(), request()) -> term()
125
%% @doc Return the internal representation of the given field. If
126
%%      <code>socket</code> is requested on a HTTPS connection, then
127
%%      an ssl socket will be returned as <code>{ssl, SslSocket}</code>.
128
%%      You can use <code>SslSocket</code> with the <code>ssl</code>
129
%%      application, eg: <code>ssl:peercert(SslSocket)</code>.
130
get(socket,
131
    {?MODULE,
132
     [Socket, _Opts, _Method, _RawPath, _Version,
133
      _Headers]}) ->
134
    Socket;
135
get(scheme,
136
    {?MODULE,
137
     [Socket, _Opts, _Method, _RawPath, _Version,
138
      _Headers]}) ->
139
    case mochiweb_socket:type(Socket) of
140
      plain -> http;
141
      ssl -> https
142
    end;
143
get(method,
144
    {?MODULE,
145
     [_Socket, _Opts, Method, _RawPath, _Version,
146
      _Headers]}) ->
147
    Method;
148
get(raw_path,
149
    {?MODULE,
150
     [_Socket, _Opts, _Method, RawPath, _Version,
151
      _Headers]}) ->
152
    RawPath;
153
get(version,
154
    {?MODULE,
155
     [_Socket, _Opts, _Method, _RawPath, Version,
156
      _Headers]}) ->
157
    Version;
158
get(headers,
159
    {?MODULE,
160
     [_Socket, _Opts, _Method, _RawPath, _Version,
161
      Headers]}) ->
162
    Headers;
163
get(peer,
164
    {?MODULE,
165
     [Socket, _Opts, _Method, _RawPath, _Version,
166
      _Headers]} =
167
	THIS) ->
168
    case mochiweb_socket:peername(Socket) of
169
      {ok, {Addr = {10, _, _, _}, _Port}} ->
170
	  case get_header_value("x-forwarded-for", THIS) of
171
	    undefined -> inet_parse:ntoa(Addr);
172
	    Hosts ->
173
		string:strip(lists:last(string:tokens(Hosts, ",")))
174
	  end;
175
      %% Copied this syntax from webmachine contributor Steve Vinoski
176
      {ok, {Addr = {172, Second, _, _}, _Port}}
177
	  when Second > 15 andalso Second < 32 ->
178
	  case get_header_value("x-forwarded-for", THIS) of
179
	    undefined -> inet_parse:ntoa(Addr);
180
	    Hosts ->
181
		string:strip(lists:last(string:tokens(Hosts, ",")))
182
	  end;
183
      %% According to RFC 6598, contributor Gerald Xv
184
      {ok, {Addr = {100, Second, _, _}, _Port}}
185
	  when Second > 63 andalso Second < 128 ->
186
	  case get_header_value("x-forwarded-for", THIS) of
187
	    undefined -> inet_parse:ntoa(Addr);
188
	    Hosts ->
189
		string:strip(lists:last(string:tokens(Hosts, ",")))
190
	  end;
191
      {ok, {Addr = {192, 168, _, _}, _Port}} ->
192
	  case get_header_value("x-forwarded-for", THIS) of
193
	    undefined -> inet_parse:ntoa(Addr);
194
	    Hosts ->
195
		string:strip(lists:last(string:tokens(Hosts, ",")))
196
	  end;
197
      {ok, {{127, 0, 0, 1}, _Port}} ->
198
	  case get_header_value("x-forwarded-for", THIS) of
199
	    undefined -> "127.0.0.1";
200
	    Hosts ->
201
		string:strip(lists:last(string:tokens(Hosts, ",")))
202
	  end;
203
      {ok, {Addr, _Port}} -> inet_parse:ntoa(Addr);
204
      {error, enotconn} -> exit(normal)
205
    end;
206
get(path,
207
    {?MODULE,
208
     [_Socket, _Opts, _Method, RawPath, _Version,
209
      _Headers]}) ->
210
    case erlang:get(?SAVE_PATH) of
211
      undefined ->
212
	  {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
213
	  Path =
214
	      mochiweb_util:normalize_path(mochiweb_util:unquote(Path0)),
215
	  put(?SAVE_PATH, Path),
216
	  Path;
217
      Cached -> Cached
218
    end;
219
get(body_length,
220
    {?MODULE,
221
     [_Socket, _Opts, _Method, _RawPath, _Version,
222
      _Headers]} =
223
	THIS) ->
224
    case erlang:get(?SAVE_BODY_LENGTH) of
225
      undefined ->
226
	  BodyLength = body_length(THIS),
227
	  put(?SAVE_BODY_LENGTH, {cached, BodyLength}),
228
	  BodyLength;
229
      {cached, Cached} -> Cached
230
    end;
231
get(range,
232
    {?MODULE,
233
     [_Socket, _Opts, _Method, _RawPath, _Version,
234
      _Headers]} =
235
	THIS) ->
236
    case get_header_value(range, THIS) of
237
      undefined -> undefined;
238
      RawRange -> mochiweb_http:parse_range_request(RawRange)
239
    end;
240
get(opts,
241
    {?MODULE,
242
     [_Socket, Opts, _Method, _RawPath, _Version,
243
      _Headers]}) ->
244
    Opts.
245
 
246
%% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]}
247
%% @doc Dump the internal representation to a "human readable" set of terms
248
%%      for debugging/inspection purposes.
249
dump({?MODULE,
250
      [_Socket, Opts, Method, RawPath, Version, Headers]}) ->
251
    {?MODULE,
252
     [{method, Method}, {version, Version},
253
      {raw_path, RawPath}, {opts, Opts},
254
      {headers, mochiweb_headers:to_list(Headers)}]}.
255
 
256
%% @spec send(iodata(), request()) -> ok
257
%% @doc Send data over the socket.
258
send(Data,
259
     {?MODULE,
260
      [Socket, _Opts, _Method, _RawPath, _Version,
261
       _Headers]}) ->
262
    case mochiweb_socket:send(Socket, Data) of
263
      ok -> ok;
264
      _ -> exit(normal)
265
    end.
266
 
267
%% @spec recv(integer(), request()) -> binary()
268
%% @doc Receive Length bytes from the client as a binary, with the default
269
%%      idle timeout.
270
recv(Length,
271
     {?MODULE,
272
      [_Socket, _Opts, _Method, _RawPath, _Version,
273
       _Headers]} =
274
	 THIS) ->
275
    recv(Length, ?IDLE_TIMEOUT, THIS).
276
 
277
%% @spec recv(integer(), integer(), request()) -> binary()
278
%% @doc Receive Length bytes from the client as a binary, with the given
279
%%      Timeout in msec.
280
recv(Length, Timeout,
281
     {?MODULE,
282
      [Socket, _Opts, _Method, _RawPath, _Version,
283
       _Headers]}) ->
284
    case mochiweb_socket:recv(Socket, Length, Timeout) of
285
      {ok, Data} -> put(?SAVE_RECV, true), Data;
286
      _ -> exit(normal)
287
    end.
288
 
289
%% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer()
290
%% @doc  Infer body length from transfer-encoding and content-length headers.
291
body_length({?MODULE,
292
	     [_Socket, _Opts, _Method, _RawPath, _Version,
293
	      _Headers]} =
294
		THIS) ->
295
    case get_header_value("transfer-encoding", THIS) of
296
      undefined ->
297
	  case get_combined_header_value("content-length", THIS)
298
	      of
299
	    undefined -> undefined;
300
	    Length -> list_to_integer(Length)
301
	  end;
302
      "chunked" -> chunked;
303
      Unknown -> {unknown_transfer_encoding, Unknown}
304
    end.
305
 
306
%% @spec recv_body(request()) -> binary()
307
%% @doc Receive the body of the HTTP request (defined by Content-Length).
308
%%      Will only receive up to the default max-body length of 1MB.
309
recv_body({?MODULE,
310
	   [_Socket, _Opts, _Method, _RawPath, _Version,
311
	    _Headers]} =
312
	      THIS) ->
313
    recv_body(?MAX_RECV_BODY, THIS).
314
 
315
%% @spec recv_body(integer(), request()) -> binary()
316
%% @doc Receive the body of the HTTP request (defined by Content-Length).
317
%%      Will receive up to MaxBody bytes.
318
recv_body(MaxBody,
319
	  {?MODULE,
320
	   [_Socket, _Opts, _Method, _RawPath, _Version,
321
	    _Headers]} =
322
	      THIS) ->
323
    case erlang:get(?SAVE_BODY) of
324
      undefined ->
325
	  % we could use a sane constant for max chunk size
326
	  Body = stream_body(?MAX_RECV_BODY,
327
			     fun ({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) ->
328
				     iolist_to_binary(lists:reverse(BinAcc));
329
				 ({Length, Bin}, {LengthAcc, BinAcc}) ->
330
				     NewLength = Length + LengthAcc,
331
				     if NewLength > MaxBody ->
332
					    exit({body_too_large, chunked});
333
					true -> {NewLength, [Bin | BinAcc]}
334
				     end
335
			     end,
336
			     {0, []}, MaxBody, THIS),
337
	  put(?SAVE_BODY, Body),
338
	  Body;
339
      Cached -> Cached
340
    end.
341
 
342
stream_body(MaxChunkSize, ChunkFun, FunState,
343
	    {?MODULE,
344
	     [_Socket, _Opts, _Method, _RawPath, _Version,
345
	      _Headers]} =
346
		THIS) ->
347
    stream_body(MaxChunkSize, ChunkFun, FunState, undefined,
348
		THIS).
349
 
350
stream_body(MaxChunkSize, ChunkFun, FunState,
351
	    MaxBodyLength,
352
	    {?MODULE,
353
	     [_Socket, _Opts, _Method, _RawPath, _Version,
354
	      _Headers]} =
355
		THIS) ->
356
    Expect = case get_header_value("expect", THIS) of
357
	       undefined -> undefined;
358
	       Value when is_list(Value) -> string:to_lower(Value)
359
	     end,
360
    case Expect of
361
      "100-continue" ->
362
	  _ = start_raw_response({100, gb_trees:empty()}, THIS),
363
	  ok;
364
      _Else -> ok
365
    end,
366
    case body_length(THIS) of
367
      undefined -> undefined;
368
      {unknown_transfer_encoding, Unknown} ->
369
	  exit({unknown_transfer_encoding, Unknown});
370
      chunked ->
371
	  % In this case the MaxBody is actually used to
372
	  % determine the maximum allowed size of a single
373
	  % chunk.
374
	  stream_chunked_body(MaxChunkSize, ChunkFun, FunState,
375
			      THIS);
376
 
377
      Length when is_integer(Length) ->
378
	  case MaxBodyLength of
379
	    MaxBodyLength
380
		when is_integer(MaxBodyLength),
381
		     MaxBodyLength < Length ->
382
		exit({body_too_large, content_length});
383
	    _ ->
384
		stream_unchunked_body(MaxChunkSize, Length, ChunkFun,
385
				      FunState, THIS)
386
	  end
387
    end.
388
 
389
%% @spec start_response({integer(), ioheaders()}, request()) -> response()
390
%% @doc Start the HTTP response by sending the Code HTTP response and
391
%%      ResponseHeaders. The server will set header defaults such as Server
392
%%      and Date if not present in ResponseHeaders.
393
start_response({Code, ResponseHeaders},
394
	       {?MODULE,
395
		[_Socket, _Opts, _Method, _RawPath, _Version,
396
		 _Headers]} =
397
		   THIS) ->
398
    start_raw_response({Code, ResponseHeaders}, THIS).
399
 
400
%% @spec start_raw_response({integer(), headers()}, request()) -> response()
401
%% @doc Start the HTTP response by sending the Code HTTP response and
402
%%      ResponseHeaders.
403
start_raw_response({Code, ResponseHeaders},
404
		   {?MODULE,
405
		    [_Socket, _Opts, _Method, _RawPath, _Version,
406
		     _Headers]} =
407
		       THIS) ->
408
    {Header, Response} = format_response_header({Code,
409
						 ResponseHeaders},
410
						THIS),
411
    send(Header, THIS),
412
    Response.
413
 
414
%% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response()
415
%% @doc Start the HTTP response by sending the Code HTTP response and
416
%%      ResponseHeaders including a Content-Length of Length. The server
417
%%      will set header defaults such as Server
418
%%      and Date if not present in ResponseHeaders.
419
start_response_length({Code, ResponseHeaders, Length},
420
		      {?MODULE,
421
		       [_Socket, _Opts, _Method, _RawPath, _Version,
422
			_Headers]} =
423
			  THIS) ->
424
    HResponse = mochiweb_headers:make(ResponseHeaders),
425
    HResponse1 = mochiweb_headers:enter("Content-Length",
426
					Length, HResponse),
427
    start_response({Code, HResponse1}, THIS).
428
 
429
%% @spec format_response_header({integer(), ioheaders()} | {integer(), ioheaders(), integer()}, request()) -> iolist()
430
%% @doc Format the HTTP response header, including the Code HTTP response and
431
%%      ResponseHeaders including an optional Content-Length of Length. The server
432
%%      will set header defaults such as Server
433
%%      and Date if not present in ResponseHeaders.
434
format_response_header({Code, ResponseHeaders},
435
		       {?MODULE,
436
			[_Socket, _Opts, _Method, _RawPath, Version,
437
			 _Headers]} =
438
			   THIS) ->
439
    HResponse = mochiweb_headers:make(ResponseHeaders),
440
    HResponse1 =
441
	mochiweb_headers:default_from_list(server_headers(),
442
					   HResponse),
443
    HResponse2 = case should_close(THIS) of
444
		   true ->
445
		       mochiweb_headers:enter("Connection", "close",
446
					      HResponse1);
447
		   false -> HResponse1
448
		 end,
449
    End = [[mochiweb_util:make_io(K), <<": ">>, V,
450
	    <<"\r\n">>]
451
	   || {K, V} <- mochiweb_headers:to_list(HResponse2)],
452
    Response = mochiweb:new_response({THIS, Code,
453
				      HResponse2}),
454
    {[make_version(Version), make_code(Code), <<"\r\n">>,
455
      End, <<"\r\n">>],
456
     Response};
457
format_response_header({Code, ResponseHeaders, Length},
458
		       {?MODULE,
459
			[_Socket, _Opts, _Method, _RawPath, _Version,
460
			 _Headers]} =
461
			   THIS) ->
462
    HResponse = mochiweb_headers:make(ResponseHeaders),
463
    HResponse1 = mochiweb_headers:enter("Content-Length",
464
					Length, HResponse),
465
    format_response_header({Code, HResponse1}, THIS).
466
 
467
%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response()
468
%% @doc Start the HTTP response with start_response, and send Body to the
469
%%      client (if the get(method) /= 'HEAD'). The Content-Length header
470
%%      will be set by the Body length, and the server will insert header
471
%%      defaults.
472
respond({Code, ResponseHeaders, {file, IoDevice}},
473
	{?MODULE,
474
	 [_Socket, _Opts, Method, _RawPath, _Version,
475
	  _Headers]} =
476
	    THIS) ->
477
    Length = mochiweb_io:iodevice_size(IoDevice),
478
    Response = start_response_length({Code, ResponseHeaders,
479
				      Length},
480
				     THIS),
481
    case Method of
482
      'HEAD' -> ok;
483
      _ ->
484
	  mochiweb_io:iodevice_stream(fun (Body) ->
485
					      send(Body, THIS)
486
				      end,
487
				      IoDevice)
488
    end,
489
    Response;
490
respond({Code, ResponseHeaders, chunked},
491
	{?MODULE,
492
	 [_Socket, _Opts, Method, _RawPath, Version, _Headers]} =
493
	    THIS) ->
494
    HResponse = mochiweb_headers:make(ResponseHeaders),
495
    HResponse1 = case Method of
496
		   'HEAD' ->
497
		       %% This is what Google does, http://www.google.com/
498
		       %% is chunked but HEAD gets Content-Length: 0.
499
		       %% The RFC is ambiguous so emulating Google is smart.
500
		       mochiweb_headers:enter("Content-Length", "0",
501
					      HResponse);
502
		   _ when Version >= {1, 1} ->
503
		       %% Only use chunked encoding for HTTP/1.1
504
		       mochiweb_headers:enter("Transfer-Encoding", "chunked",
505
					      HResponse);
506
		   _ ->
507
		       %% For pre-1.1 clients we send the data as-is
508
		       %% without a Content-Length header and without
509
		       %% chunk delimiters. Since the end of the document
510
		       %% is now ambiguous we must force a close.
511
		       put(?SAVE_FORCE_CLOSE, true),
512
		       HResponse
513
		 end,
514
    start_response({Code, HResponse1}, THIS);
515
respond({Code, ResponseHeaders, Body},
516
	{?MODULE,
517
	 [_Socket, _Opts, Method, _RawPath, _Version,
518
	  _Headers]} =
519
	    THIS) ->
520
    {Header, Response} = format_response_header({Code,
521
						 ResponseHeaders,
522
						 iolist_size(Body)},
523
						THIS),
524
    case Method of
525
      'HEAD' -> send(Header, THIS);
526
      _ -> send([Header, Body], THIS)
527
    end,
528
    Response.
529
 
530
%% @spec not_found(request()) -> response()
531
%% @doc Alias for <code>not_found([])</code>.
532
not_found({?MODULE,
533
	   [_Socket, _Opts, _Method, _RawPath, _Version,
534
	    _Headers]} =
535
	      THIS) ->
536
    not_found([], THIS).
537
 
538
%% @spec not_found(ExtraHeaders, request()) -> response()
539
%% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"}
540
%% | ExtraHeaders], &lt;&lt;"Not found."&gt;&gt;})</code>.
541
not_found(ExtraHeaders,
542
	  {?MODULE,
543
	   [_Socket, _Opts, _Method, _RawPath, _Version,
544
	    _Headers]} =
545
	      THIS) ->
546
    respond({404,
547
	     [{"Content-Type", "text/plain"} | ExtraHeaders],
548
	     <<"Not found.">>},
549
	    THIS).
550
 
551
%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) ->
552
%%           response()
553
%% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}).
554
ok({ContentType, Body},
555
   {?MODULE,
556
    [_Socket, _Opts, _Method, _RawPath, _Version,
557
     _Headers]} =
558
       THIS) ->
559
    ok({ContentType, [], Body}, THIS);
560
ok({ContentType, ResponseHeaders, Body},
561
   {?MODULE,
562
    [_Socket, _Opts, _Method, _RawPath, _Version,
563
     _Headers]} =
564
       THIS) ->
565
    HResponse = mochiweb_headers:make(ResponseHeaders),
566
    case get(range, THIS) of
567
      X
568
	  when (X =:= undefined orelse X =:= fail) orelse
569
		 Body =:= chunked ->
570
	  %% http://code.google.com/p/mochiweb/issues/detail?id=54
571
	  %% Range header not supported when chunked, return 200 and provide
572
	  %% full response.
573
	  HResponse1 = mochiweb_headers:enter("Content-Type",
574
					      ContentType, HResponse),
575
	  respond({200, HResponse1, Body}, THIS);
576
      Ranges ->
577
	  {PartList, Size} = range_parts(Body, Ranges),
578
	  case PartList of
579
	    [] -> %% no valid ranges
580
		HResponse1 = mochiweb_headers:enter("Content-Type",
581
						    ContentType, HResponse),
582
		%% could be 416, for now we'll just return 200
583
		respond({200, HResponse1, Body}, THIS);
584
	    PartList ->
585
		{RangeHeaders, RangeBody} =
586
		    mochiweb_multipart:parts_to_body(PartList, ContentType,
587
						     Size),
588
		HResponse1 =
589
		    mochiweb_headers:enter_from_list([{"Accept-Ranges",
590
						       "bytes"}
591
						      | RangeHeaders],
592
						     HResponse),
593
		respond({206, HResponse1, RangeBody}, THIS)
594
	  end
595
    end.
596
 
597
%% @spec should_close(request()) -> bool()
598
%% @doc Return true if the connection must be closed. If false, using
599
%%      Keep-Alive should be safe.
600
should_close({?MODULE,
601
	      [_Socket, _Opts, _Method, _RawPath, Version,
602
	       _Headers]} =
603
		 THIS) ->
604
    ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/=
605
		   undefined,
606
    DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined,
607
    ForceClose orelse
608
      Version < {1, 0}
609
	%% Connection: close
610
	orelse
611
	is_close(get_header_value("connection", THIS))
612
	  %% HTTP 1.0 requires Connection: Keep-Alive
613
	  orelse
614
	  Version =:= {1, 0} andalso
615
	    get_header_value("connection", THIS) =/= "Keep-Alive"
616
	    %% unread data left on the socket, can't safely continue
617
	    orelse
618
	    DidNotRecv andalso
619
	      get_combined_header_value("content-length", THIS) =/=
620
		undefined
621
		andalso
622
		list_to_integer(get_combined_header_value("content-length",
623
							  THIS))
624
		  > 0
625
	      orelse
626
	      DidNotRecv andalso
627
		get_header_value("transfer-encoding", THIS) =:=
628
		  "chunked".
629
 
630
is_close("close") -> true;
631
is_close(S = [_C, _L, _O, _S, _E]) ->
632
    string:to_lower(S) =:= "close";
633
is_close(_) -> false.
634
 
635
%% @spec cleanup(request()) -> ok
636
%% @doc Clean up any junk in the process dictionary, required before continuing
637
%%      a Keep-Alive request.
638
cleanup({?MODULE,
639
	 [_Socket, _Opts, _Method, _RawPath, _Version,
640
	  _Headers]}) ->
641
    L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY,
642
	 ?SAVE_BODY_LENGTH, ?SAVE_POST, ?SAVE_COOKIE,
643
	 ?SAVE_FORCE_CLOSE],
644
    lists:foreach(fun (K) -> erase(K) end, L),
645
    ok.
646
 
647
%% @spec parse_qs(request()) -> [{Key::string(), Value::string()}]
648
%% @doc Parse the query string of the URL.
649
parse_qs({?MODULE,
650
	  [_Socket, _Opts, _Method, RawPath, _Version,
651
	   _Headers]}) ->
652
    case erlang:get(?SAVE_QS) of
653
      undefined ->
654
	  {_, QueryString, _} =
655
	      mochiweb_util:urlsplit_path(RawPath),
656
	  Parsed = mochiweb_util:parse_qs(QueryString),
657
	  put(?SAVE_QS, Parsed),
658
	  Parsed;
659
      Cached -> Cached
660
    end.
661
 
662
%% @spec get_cookie_value(Key::string, request()) -> string() | undefined
663
%% @doc Get the value of the given cookie.
664
get_cookie_value(Key,
665
		 {?MODULE,
666
		  [_Socket, _Opts, _Method, _RawPath, _Version,
667
		   _Headers]} =
668
		     THIS) ->
669
    proplists:get_value(Key, parse_cookie(THIS)).
670
 
671
%% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}]
672
%% @doc Parse the cookie header.
673
parse_cookie({?MODULE,
674
	      [_Socket, _Opts, _Method, _RawPath, _Version,
675
	       _Headers]} =
676
		 THIS) ->
677
    case erlang:get(?SAVE_COOKIE) of
678
      undefined ->
679
	  Cookies = case get_header_value("cookie", THIS) of
680
		      undefined -> [];
681
		      Value -> mochiweb_cookies:parse_cookie(Value)
682
		    end,
683
	  put(?SAVE_COOKIE, Cookies),
684
	  Cookies;
685
      Cached -> Cached
686
    end.
687
 
688
%% @spec parse_post(request()) -> [{Key::string(), Value::string()}]
689
%% @doc Parse an application/x-www-form-urlencoded form POST. This
690
%%      has the side-effect of calling recv_body().
691
parse_post({?MODULE,
692
	    [_Socket, _Opts, _Method, _RawPath, _Version,
693
	     _Headers]} =
694
	       THIS) ->
695
    case erlang:get(?SAVE_POST) of
696
      undefined ->
697
	  Parsed = case recv_body(THIS) of
698
		     undefined -> [];
699
		     Binary ->
700
			 case get_primary_header_value("content-type", THIS) of
701
			   "application/x-www-form-urlencoded" ++ _ ->
702
			       mochiweb_util:parse_qs(Binary);
703
			   _ -> []
704
			 end
705
		   end,
706
	  put(?SAVE_POST, Parsed),
707
	  Parsed;
708
      Cached -> Cached
709
    end.
710
 
711
%% @spec stream_chunked_body(integer(), fun(), term(), request()) -> term()
712
%% @doc The function is called for each chunk.
713
%%      Used internally by read_chunked_body.
714
stream_chunked_body(MaxChunkSize, Fun, FunState,
715
		    {?MODULE,
716
		     [_Socket, _Opts, _Method, _RawPath, _Version,
717
		      _Headers]} =
718
			THIS) ->
719
    case read_chunk_length(THIS) of
720
 
721
      Length when Length > MaxChunkSize ->
722
	  NewState = read_sub_chunks(Length, MaxChunkSize, Fun,
723
				     FunState, THIS),
724
	  stream_chunked_body(MaxChunkSize, Fun, NewState, THIS);
725
      Length ->
726
	  NewState = Fun({Length, read_chunk(Length, THIS)},
727
			 FunState),
728
	  stream_chunked_body(MaxChunkSize, Fun, NewState, THIS)
729
    end.
730
 
731
stream_unchunked_body(_MaxChunkSize, 0, Fun, FunState,
732
		      {?MODULE,
733
		       [_Socket, _Opts, _Method, _RawPath, _Version,
734
			_Headers]}) ->
735
    Fun({0, <<>>}, FunState);
736
stream_unchunked_body(MaxChunkSize, Length, Fun,
737
		      FunState,
738
		      {?MODULE,
739
		       [_Socket, Opts, _Method, _RawPath, _Version,
740
			_Headers]} =
741
			  THIS)
742
    when Length > 0 ->
743
    RecBuf = case mochilists:get_value(recbuf, Opts,
744
				       ?RECBUF_SIZE)
745
		 of
746
	       undefined -> %os controlled buffer size
747
		   MaxChunkSize;
748
	       Val -> Val
749
	     end,
750
    PktSize = min(Length, RecBuf),
751
    Bin = recv(PktSize, THIS),
752
    NewState = Fun({PktSize, Bin}, FunState),
753
    stream_unchunked_body(MaxChunkSize, Length - PktSize,
754
			  Fun, NewState, THIS).
755
 
756
%% @spec read_chunk_length(request()) -> integer()
757
%% @doc Read the length of the next HTTP chunk.
758
read_chunk_length({?MODULE,
759
		   [Socket, _Opts, _Method, _RawPath, _Version,
760
		    _Headers]}) ->
761
    ok =
762
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
763
							       [{packet,
764
								 line}])),
765
    case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
766
      {ok, Header} ->
767
	  ok =
768
	      mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
769
								     [{packet,
770
								       raw}])),
771
	  Splitter = fun (C) ->
772
			     C =/= $\r andalso C =/= $\n andalso C =/= $\n
773
		     end,
774
	  {Hex, _Rest} = lists:splitwith(Splitter,
775
					 binary_to_list(Header)),
776
	  mochihex:to_int(Hex);
777
      _ -> exit(normal)
778
    end.
779
 
780
%% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()]
781
%% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the
782
%%      HTTP footers (as a list of binaries, since they're nominal).
783
read_chunk(0,
784
	   {?MODULE,
785
	    [Socket, _Opts, _Method, _RawPath, _Version,
786
	     _Headers]}) ->
787
    ok =
788
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
789
							       [{packet,
790
								 line}])),
791
    F = fun (F1, Acc) ->
792
		case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
793
		  {ok, <<"\r\n">>} -> Acc;
794
		  {ok, Footer} -> F1(F1, [Footer | Acc]);
795
		  _ -> exit(normal)
796
		end
797
	end,
798
    Footers = F(F, []),
799
    ok =
800
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
801
							       [{packet,
802
								 raw}])),
803
    put(?SAVE_RECV, true),
804
    Footers;
805
read_chunk(Length,
806
	   {?MODULE,
807
	    [Socket, _Opts, _Method, _RawPath, _Version,
808
	     _Headers]}) ->
809
    case mochiweb_socket:recv(Socket, 2 + Length,
810
			      ?IDLE_TIMEOUT)
811
	of
812
      {ok, <<Chunk:Length/binary, "\r\n">>} -> Chunk;
813
      _ -> exit(normal)
814
    end.
815
 
816
read_sub_chunks(Length, MaxChunkSize, Fun, FunState,
817
		{?MODULE,
818
		 [_Socket, _Opts, _Method, _RawPath, _Version,
819
		  _Headers]} =
820
		    THIS)
821
    when Length > MaxChunkSize ->
822
    Bin = recv(MaxChunkSize, THIS),
823
    NewState = Fun({size(Bin), Bin}, FunState),
824
    read_sub_chunks(Length - MaxChunkSize, MaxChunkSize,
825
		    Fun, NewState, THIS);
826
read_sub_chunks(Length, _MaxChunkSize, Fun, FunState,
827
		{?MODULE,
828
		 [_Socket, _Opts, _Method, _RawPath, _Version,
829
		  _Headers]} =
830
		    THIS) ->
831
    Fun({Length, read_chunk(Length, THIS)}, FunState).
832
 
833
%% @spec serve_file(Path, DocRoot, request()) -> Response
834
%% @doc Serve a file relative to DocRoot.
835
serve_file(Path, DocRoot,
836
	   {?MODULE,
837
	    [_Socket, _Opts, _Method, _RawPath, _Version,
838
	     _Headers]} =
839
	       THIS) ->
840
    serve_file(Path, DocRoot, [], THIS).
841
 
842
%% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response
843
%% @doc Serve a file relative to DocRoot.
844
serve_file(Path, DocRoot, ExtraHeaders,
845
	   {?MODULE,
846
	    [_Socket, _Opts, _Method, _RawPath, _Version,
847
	     _Headers]} =
848
	       THIS) ->
849
    case mochiweb_util:safe_relative_path(Path) of
850
      undefined -> not_found(ExtraHeaders, THIS);
851
      RelPath ->
852
	  FullPath = filename:join([DocRoot, RelPath]),
853
	  case filelib:is_dir(FullPath) of
854
	    true ->
855
		maybe_redirect(RelPath, FullPath, ExtraHeaders, THIS);
856
	    false -> maybe_serve_file(FullPath, ExtraHeaders, THIS)
857
	  end
858
    end.
859
 
860
%% Internal API
861
 
862
%% This has the same effect as the DirectoryIndex directive in httpd
863
directory_index(FullPath) ->
864
    filename:join([FullPath, "index.html"]).
865
 
866
maybe_redirect([], FullPath, ExtraHeaders,
867
	       {?MODULE,
868
		[_Socket, _Opts, _Method, _RawPath, _Version,
869
		 _Headers]} =
870
		   THIS) ->
871
    maybe_serve_file(directory_index(FullPath),
872
		     ExtraHeaders, THIS);
873
maybe_redirect(RelPath, FullPath, ExtraHeaders,
874
	       {?MODULE,
875
		[_Socket, _Opts, _Method, _RawPath, _Version,
876
		 Headers]} =
877
		   THIS) ->
878
    case string:right(RelPath, 1) of
879
      "/" ->
880
	  maybe_serve_file(directory_index(FullPath),
881
			   ExtraHeaders, THIS);
882
      _ ->
883
	  Host = mochiweb_headers:get_value("host", Headers),
884
	  Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/",
885
	  LocationBin = list_to_binary(Location),
886
	  MoreHeaders = [{"Location", Location},
887
			 {"Content-Type", "text/html"}
888
			 | ExtraHeaders],
889
	  Top = <<"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD "
890
		  "HTML 2.0//EN\"><html><head><title>301 "
891
		  "Moved Permanently</title></head><body><h1>Mov"
892
		  "ed Permanently</h1><p>The document has "
893
		  "moved <a href=\"">>,
894
	  Bottom = <<">here</a>.</p></body></html>\n">>,
895
	  Body = <<Top/binary, LocationBin/binary,
896
		   Bottom/binary>>,
897
	  respond({301, MoreHeaders, Body}, THIS)
898
    end.
899
 
900
maybe_serve_file(File, ExtraHeaders,
901
		 {?MODULE,
902
		  [_Socket, _Opts, _Method, _RawPath, _Version,
903
		   _Headers]} =
904
		     THIS) ->
905
    case file:read_file_info(File) of
906
      {ok, FileInfo} ->
907
	  LastModified =
908
	      httpd_util:rfc1123_date(FileInfo#file_info.mtime),
909
	  case get_header_value("if-modified-since", THIS) of
910
	    LastModified -> respond({304, ExtraHeaders, ""}, THIS);
911
	    _ ->
912
		case file:open(File, [raw, binary]) of
913
		  {ok, IoDevice} ->
914
		      ContentType = mochiweb_util:guess_mime(File),
915
		      Res = ok({ContentType,
916
				[{"last-modified", LastModified}
917
				 | ExtraHeaders],
918
				{file, IoDevice}},
919
			       THIS),
920
		      ok = file:close(IoDevice),
921
		      Res;
922
		  _ -> not_found(ExtraHeaders, THIS)
923
		end
924
	  end;
925
      {error, _} -> not_found(ExtraHeaders, THIS)
926
    end.
927
 
928
server_headers() ->
929
    [{"Server", "MochiWeb/1.0 (" ++ (?QUIP) ++ ")"},
930
     {"Date", mochiweb_clock:rfc1123()}].
931
 
932
make_code(X) when is_integer(X) ->
933
    [integer_to_list(X),
934
     [" " | httpd_util:reason_phrase(X)]];
935
make_code(Io) when is_list(Io); is_binary(Io) -> Io.
936
 
937
make_version({1, 0}) -> <<"HTTP/1.0 ">>;
938
make_version(_) -> <<"HTTP/1.1 ">>.
939
 
940
range_parts({file, IoDevice}, Ranges) ->
941
    Size = mochiweb_io:iodevice_size(IoDevice),
942
    F = fun (Spec, Acc) ->
943
		case mochiweb_http:range_skip_length(Spec, Size) of
944
		  invalid_range -> Acc;
945
		  V -> [V | Acc]
946
		end
947
	end,
948
    LocNums = lists:foldr(F, [], Ranges),
949
    {ok, Data} = file:pread(IoDevice, LocNums),
950
    Bodies = lists:zipwith(fun ({Skip, Length},
951
				PartialBody) ->
952
				   case Length of
953
 
954
				     _ -> {Skip, Skip + Length - 1, PartialBody}
955
				   end
956
			   end,
957
			   LocNums, Data),
958
    {Bodies, Size};
959
range_parts(Body0, Ranges) ->
960
    Body = iolist_to_binary(Body0),
961
    Size = size(Body),
962
    F = fun (Spec, Acc) ->
963
		case mochiweb_http:range_skip_length(Spec, Size) of
964
		  invalid_range -> Acc;
965
		  {Skip, Length} ->
966
		      <<_:Skip/binary, PartialBody:Length/binary, _/binary>> =
967
			  Body,
968
		      [{Skip, Skip + Length - 1, PartialBody} | Acc]
969
		end
970
	end,
971
    {lists:foldr(F, [], Ranges), Size}.
972
 
973
%% @spec accepted_encodings([encoding()], request()) -> [encoding()] | bad_accept_encoding_value
974
%% @type encoding() = string().
975
%%
976
%% @doc Returns a list of encodings accepted by a request. Encodings that are
977
%%      not supported by the server will not be included in the return list.
978
%%      This list is computed from the "Accept-Encoding" header and
979
%%      its elements are ordered, descendingly, according to their Q values.
980
%%
981
%%      Section 14.3 of the RFC 2616 (HTTP 1.1) describes the "Accept-Encoding"
982
%%      header and the process of determining which server supported encodings
983
%%      can be used for encoding the body for the request's response.
984
%%
985
%%      Examples
986
%%
987
%%      1) For a missing "Accept-Encoding" header:
988
%%         accepted_encodings(["gzip", "identity"]) -> ["identity"]
989
%%
990
%%      2) For an "Accept-Encoding" header with value "gzip, deflate":
991
%%         accepted_encodings(["gzip", "identity"]) -> ["gzip", "identity"]
992
%%
993
%%      3) For an "Accept-Encoding" header with value "gzip;q=0.5, deflate":
994
%%         accepted_encodings(["gzip", "deflate", "identity"]) ->
995
%%            ["deflate", "gzip", "identity"]
996
%%
997
accepted_encodings(SupportedEncodings,
998
		   {?MODULE,
999
		    [_Socket, _Opts, _Method, _RawPath, _Version,
1000
		     _Headers]} =
1001
		       THIS) ->
1002
    AcceptEncodingHeader = case
1003
			     get_header_value("Accept-Encoding", THIS)
1004
			       of
1005
			     undefined -> "";
1006
			     Value -> Value
1007
			   end,
1008
    case mochiweb_util:parse_qvalues(AcceptEncodingHeader)
1009
	of
1010
      invalid_qvalue_string -> bad_accept_encoding_value;
1011
      QList ->
1012
	  mochiweb_util:pick_accepted_encodings(QList,
1013
						SupportedEncodings, "identity")
1014
    end.
1015
 
1016
%% @spec accepts_content_type(string() | binary(), request()) -> boolean() | bad_accept_header
1017
%%
1018
%% @doc Determines whether a request accepts a given media type by analyzing its
1019
%%      "Accept" header.
1020
%%
1021
%%      Examples
1022
%%
1023
%%      1) For a missing "Accept" header:
1024
%%         accepts_content_type("application/json") -> true
1025
%%
1026
%%      2) For an "Accept" header with value "text/plain, application/*":
1027
%%         accepts_content_type("application/json") -> true
1028
%%
1029
%%      3) For an "Accept" header with value "text/plain, */*; q=0.0":
1030
%%         accepts_content_type("application/json") -> false
1031
%%
1032
%%      4) For an "Accept" header with value "text/plain; q=0.5, */*; q=0.1":
1033
%%         accepts_content_type("application/json") -> true
1034
%%
1035
%%      5) For an "Accept" header with value "text/*; q=0.0, */*":
1036
%%         accepts_content_type("text/plain") -> false
1037
%%
1038
accepts_content_type(ContentType1,
1039
		     {?MODULE,
1040
		      [_Socket, _Opts, _Method, _RawPath, _Version,
1041
		       _Headers]} =
1042
			 THIS) ->
1043
    ContentType = re:replace(ContentType1, "\\s", "",
1044
			     [global, {return, list}]),
1045
    AcceptHeader = accept_header(THIS),
1046
    case mochiweb_util:parse_qvalues(AcceptHeader) of
1047
      invalid_qvalue_string -> bad_accept_header;
1048
      QList ->
1049
	  [MainType, _SubType] = string:tokens(ContentType, "/"),
1050
	  SuperType = MainType ++ "/*",
1051
	  lists:any(fun ({"*/*", Q}) when Q > 0.0 -> true;
1052
			({Type, Q}) when Q > 0.0 ->
1053
			    Type =:= ContentType orelse Type =:= SuperType;
1054
			(_) -> false
1055
		    end,
1056
		    QList)
1057
	    andalso
1058
	    not lists:member({ContentType, 0.0}, QList) andalso
1059
	      not lists:member({SuperType, 0.0}, QList)
1060
    end.
1061
 
1062
%% @spec accepted_content_types([string() | binary()], request()) -> [string()] | bad_accept_header
1063
%%
1064
%% @doc Filters which of the given media types this request accepts. This filtering
1065
%%      is performed by analyzing the "Accept" header. The returned list is sorted
1066
%%      according to the preferences specified in the "Accept" header (higher Q values
1067
%%      first). If two or more types have the same preference (Q value), they're order
1068
%%      in the returned list is the same as they're order in the input list.
1069
%%
1070
%%      Examples
1071
%%
1072
%%      1) For a missing "Accept" header:
1073
%%         accepted_content_types(["text/html", "application/json"]) ->
1074
%%             ["text/html", "application/json"]
1075
%%
1076
%%      2) For an "Accept" header with value "text/html, application/*":
1077
%%         accepted_content_types(["application/json", "text/html"]) ->
1078
%%             ["application/json", "text/html"]
1079
%%
1080
%%      3) For an "Accept" header with value "text/html, */*; q=0.0":
1081
%%         accepted_content_types(["text/html", "application/json"]) ->
1082
%%             ["text/html"]
1083
%%
1084
%%      4) For an "Accept" header with value "text/html; q=0.5, */*; q=0.1":
1085
%%         accepts_content_types(["application/json", "text/html"]) ->
1086
%%             ["text/html", "application/json"]
1087
%%
1088
accepted_content_types(Types1,
1089
		       {?MODULE,
1090
			[_Socket, _Opts, _Method, _RawPath, _Version,
1091
			 _Headers]} =
1092
			   THIS) ->
1093
    Types = [accepted_content_types_1(V1) || V1 <- Types1],
1094
    AcceptHeader = accept_header(THIS),
1095
    case mochiweb_util:parse_qvalues(AcceptHeader) of
1096
      invalid_qvalue_string -> bad_accept_header;
1097
      QList ->
1098
	  TypesQ = lists:foldr(fun (T, Acc) ->
1099
				       case proplists:get_value(T, QList) of
1100
					 undefined ->
1101
					     [MainType, _SubType] =
1102
						 string:tokens(T, "/"),
1103
					     case proplists:get_value(MainType
1104
									++ "/*",
1105
								      QList)
1106
						 of
1107
					       undefined ->
1108
						   case
1109
						     proplists:get_value("*/*",
1110
									 QList)
1111
						       of
1112
						     Q
1113
							 when is_float(Q),
1114
							      Q > 0.0 ->
1115
							 [{Q, T} | Acc];
1116
						     _ -> Acc
1117
						   end;
1118
					       Q when Q > 0.0 -> [{Q, T} | Acc];
1119
					       _ -> Acc
1120
					     end;
1121
					 Q when Q > 0.0 -> [{Q, T} | Acc];
1122
					 _ -> Acc
1123
				       end
1124
			       end,
1125
			       [], Types),
1126
	  % Note: Stable sort. If 2 types have the same Q value we leave them in the
1127
	  % same order as in the input list.
1128
	  SortFun = fun ({Q1, _}, {Q2, _}) -> Q1 >= Q2 end,
1129
	  [Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)]
1130
    end.
1131
 
1132
accepted_content_types_1(T) ->
1133
    re:replace(T, "\\s", "", [global, {return, list}]).
1134
 
1135
accept_header({?MODULE,
1136
	       [_Socket, _Opts, _Method, _RawPath, _Version,
1137
		_Headers]} =
1138
		  THIS) ->
1139
    case get_header_value("Accept", THIS) of
1140
      undefined -> "*/*";
1141
      Value -> Value
1142
    end.
1143
 
1144
%%
1145
%% Tests
1146
%%
1147
-ifdef(TEST).
1148
 
1149
-include_lib("eunit/include/eunit.hrl").
1150
 
1151
-endif.