PageRenderTime 49ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/src/mochiweb/mochiweb_request.erl

http://github.com/evanmiller/ChicagoBoss
Erlang | 722 lines | 517 code | 59 blank | 146 comment | 2 complexity | e216f80d7cc8a27bff2a4ac308b2c7a1 MD5 | raw file
  1. %% @author Bob Ippolito <bob@mochimedia.com>
  2. %% @copyright 2007 Mochi Media, Inc.
  3. %% @doc MochiWeb HTTP Request abstraction.
  4. -module(mochiweb_request, [Socket, Method, RawPath, Version, Headers]).
  5. -author('bob@mochimedia.com').
  6. -include_lib("kernel/include/file.hrl").
  7. -include("internal.hrl").
  8. -define(QUIP, "Any of you quaids got a smint?").
  9. -export([get_header_value/1, get_primary_header_value/1, get/1, dump/0]).
  10. -export([send/1, recv/1, recv/2, recv_body/0, recv_body/1, stream_body/3]).
  11. -export([start_response/1, start_response_length/1, start_raw_response/1]).
  12. -export([respond/1, ok/1]).
  13. -export([not_found/0, not_found/1]).
  14. -export([parse_post/0, get_post_value/1]).
  15. -export([parse_qs/0, get_qs_value/1]).
  16. -export([should_close/0, cleanup/0]).
  17. -export([parse_cookie/0, get_cookie_value/1]).
  18. -export([serve_file/2, serve_file/3]).
  19. -export([accepted_encodings/1]).
  20. -define(SAVE_QS, mochiweb_request_qs).
  21. -define(SAVE_PATH, mochiweb_request_path).
  22. -define(SAVE_RECV, mochiweb_request_recv).
  23. -define(SAVE_BODY, mochiweb_request_body).
  24. -define(SAVE_BODY_LENGTH, mochiweb_request_body_length).
  25. -define(SAVE_POST, mochiweb_request_post).
  26. -define(SAVE_COOKIE, mochiweb_request_cookie).
  27. -define(SAVE_FORCE_CLOSE, mochiweb_request_force_close).
  28. %% @type iolist() = [iolist() | binary() | char()].
  29. %% @type iodata() = binary() | iolist().
  30. %% @type key() = atom() | string() | binary()
  31. %% @type value() = atom() | string() | binary() | integer()
  32. %% @type headers(). A mochiweb_headers structure.
  33. %% @type response(). A mochiweb_response parameterized module instance.
  34. %% @type ioheaders() = headers() | [{key(), value()}].
  35. % 10 second default idle timeout
  36. -define(IDLE_TIMEOUT, 10000).
  37. % Maximum recv_body() length of 1MB
  38. -define(MAX_RECV_BODY, (1024*1024)).
  39. %% @spec get_header_value(K) -> undefined | Value
  40. %% @doc Get the value of a given request header.
  41. get_header_value(K) ->
  42. mochiweb_headers:get_value(K, Headers).
  43. get_primary_header_value(K) ->
  44. mochiweb_headers:get_primary_value(K, Headers).
  45. %% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
  46. %% @spec get(field()) -> term()
  47. %% @doc Return the internal representation of the given field. If
  48. %% <code>socket</code> is requested on a HTTPS connection, then
  49. %% an ssl socket will be returned as <code>{ssl, SslSocket}</code>.
  50. %% You can use <code>SslSocket</code> with the <code>ssl</code>
  51. %% application, eg: <code>ssl:peercert(SslSocket)</code>.
  52. get(socket) ->
  53. Socket;
  54. get(scheme) ->
  55. case mochiweb_socket:type(Socket) of
  56. plain ->
  57. http;
  58. ssl ->
  59. https
  60. end;
  61. get(method) ->
  62. Method;
  63. get(raw_path) ->
  64. RawPath;
  65. get(version) ->
  66. Version;
  67. get(headers) ->
  68. Headers;
  69. get(peer) ->
  70. case mochiweb_socket:peername(Socket) of
  71. {ok, {Addr={10, _, _, _}, _Port}} ->
  72. case get_header_value("x-forwarded-for") of
  73. undefined ->
  74. inet_parse:ntoa(Addr);
  75. Hosts ->
  76. string:strip(lists:last(string:tokens(Hosts, ",")))
  77. end;
  78. {ok, {{127, 0, 0, 1}, _Port}} ->
  79. case get_header_value("x-forwarded-for") of
  80. undefined ->
  81. "127.0.0.1";
  82. Hosts ->
  83. string:strip(lists:last(string:tokens(Hosts, ",")))
  84. end;
  85. {ok, {Addr, _Port}} ->
  86. inet_parse:ntoa(Addr)
  87. end;
  88. get(path) ->
  89. case erlang:get(?SAVE_PATH) of
  90. undefined ->
  91. {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
  92. Path = mochiweb_util:unquote(Path0),
  93. put(?SAVE_PATH, Path),
  94. Path;
  95. Cached ->
  96. Cached
  97. end;
  98. get(body_length) ->
  99. case erlang:get(?SAVE_BODY_LENGTH) of
  100. undefined ->
  101. BodyLength = body_length(),
  102. put(?SAVE_BODY_LENGTH, {cached, BodyLength}),
  103. BodyLength;
  104. {cached, Cached} ->
  105. Cached
  106. end;
  107. get(range) ->
  108. case get_header_value(range) of
  109. undefined ->
  110. undefined;
  111. RawRange ->
  112. mochiweb_http:parse_range_request(RawRange)
  113. end.
  114. %% @spec dump() -> {mochiweb_request, [{atom(), term()}]}
  115. %% @doc Dump the internal representation to a "human readable" set of terms
  116. %% for debugging/inspection purposes.
  117. dump() ->
  118. {?MODULE, [{method, Method},
  119. {version, Version},
  120. {raw_path, RawPath},
  121. {headers, mochiweb_headers:to_list(Headers)}]}.
  122. %% @spec send(iodata()) -> ok
  123. %% @doc Send data over the socket.
  124. send(Data) ->
  125. case mochiweb_socket:send(Socket, Data) of
  126. ok ->
  127. ok;
  128. _ ->
  129. exit(normal)
  130. end.
  131. %% @spec recv(integer()) -> binary()
  132. %% @doc Receive Length bytes from the client as a binary, with the default
  133. %% idle timeout.
  134. recv(Length) ->
  135. recv(Length, ?IDLE_TIMEOUT).
  136. %% @spec recv(integer(), integer()) -> binary()
  137. %% @doc Receive Length bytes from the client as a binary, with the given
  138. %% Timeout in msec.
  139. recv(Length, Timeout) ->
  140. case mochiweb_socket:recv(Socket, Length, Timeout) of
  141. {ok, Data} ->
  142. put(?SAVE_RECV, true),
  143. Data;
  144. _ ->
  145. exit(normal)
  146. end.
  147. %% @spec body_length() -> undefined | chunked | unknown_transfer_encoding | integer()
  148. %% @doc Infer body length from transfer-encoding and content-length headers.
  149. body_length() ->
  150. case get_header_value("transfer-encoding") of
  151. undefined ->
  152. case get_header_value("content-length") of
  153. undefined ->
  154. undefined;
  155. Length ->
  156. list_to_integer(Length)
  157. end;
  158. "chunked" ->
  159. chunked;
  160. Unknown ->
  161. {unknown_transfer_encoding, Unknown}
  162. end.
  163. %% @spec recv_body() -> binary()
  164. %% @doc Receive the body of the HTTP request (defined by Content-Length).
  165. %% Will only receive up to the default max-body length of 1MB.
  166. recv_body() ->
  167. recv_body(?MAX_RECV_BODY).
  168. %% @spec recv_body(integer()) -> binary()
  169. %% @doc Receive the body of the HTTP request (defined by Content-Length).
  170. %% Will receive up to MaxBody bytes.
  171. recv_body(MaxBody) ->
  172. % we could use a sane constant for max chunk size
  173. Body = stream_body(?MAX_RECV_BODY, fun
  174. ({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) ->
  175. iolist_to_binary(lists:reverse(BinAcc));
  176. ({Length, Bin}, {LengthAcc, BinAcc}) ->
  177. NewLength = Length + LengthAcc,
  178. if NewLength > MaxBody ->
  179. exit({body_too_large, chunked});
  180. true ->
  181. {NewLength, [Bin | BinAcc]}
  182. end
  183. end, {0, []}, MaxBody),
  184. put(?SAVE_BODY, Body),
  185. Body.
  186. stream_body(MaxChunkSize, ChunkFun, FunState) ->
  187. stream_body(MaxChunkSize, ChunkFun, FunState, undefined).
  188. stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) ->
  189. Expect = case get_header_value("expect") of
  190. undefined ->
  191. undefined;
  192. Value when is_list(Value) ->
  193. string:to_lower(Value)
  194. end,
  195. case Expect of
  196. "100-continue" ->
  197. start_raw_response({100, gb_trees:empty()});
  198. _Else ->
  199. ok
  200. end,
  201. case body_length() of
  202. undefined ->
  203. undefined;
  204. {unknown_transfer_encoding, Unknown} ->
  205. exit({unknown_transfer_encoding, Unknown});
  206. chunked ->
  207. % In this case the MaxBody is actually used to
  208. % determine the maximum allowed size of a single
  209. % chunk.
  210. stream_chunked_body(MaxChunkSize, ChunkFun, FunState);
  211. 0 ->
  212. <<>>;
  213. Length when is_integer(Length) ->
  214. case MaxBodyLength of
  215. MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length ->
  216. exit({body_too_large, content_length});
  217. _ ->
  218. stream_unchunked_body(Length, ChunkFun, FunState)
  219. end;
  220. Length ->
  221. exit({length_not_integer, Length})
  222. end.
  223. %% @spec start_response({integer(), ioheaders()}) -> response()
  224. %% @doc Start the HTTP response by sending the Code HTTP response and
  225. %% ResponseHeaders. The server will set header defaults such as Server
  226. %% and Date if not present in ResponseHeaders.
  227. start_response({Code, ResponseHeaders}) ->
  228. HResponse = mochiweb_headers:make(ResponseHeaders),
  229. HResponse1 = mochiweb_headers:default_from_list(server_headers(),
  230. HResponse),
  231. start_raw_response({Code, HResponse1}).
  232. %% @spec start_raw_response({integer(), headers()}) -> response()
  233. %% @doc Start the HTTP response by sending the Code HTTP response and
  234. %% ResponseHeaders.
  235. start_raw_response({Code, ResponseHeaders}) ->
  236. F = fun ({K, V}, Acc) ->
  237. [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
  238. end,
  239. End = lists:foldl(F, [<<"\r\n">>],
  240. mochiweb_headers:to_list(ResponseHeaders)),
  241. send([make_version(Version), make_code(Code), <<"\r\n">> | End]),
  242. mochiweb:new_response({THIS, Code, ResponseHeaders}).
  243. %% @spec start_response_length({integer(), ioheaders(), integer()}) -> response()
  244. %% @doc Start the HTTP response by sending the Code HTTP response and
  245. %% ResponseHeaders including a Content-Length of Length. The server
  246. %% will set header defaults such as Server
  247. %% and Date if not present in ResponseHeaders.
  248. start_response_length({Code, ResponseHeaders, Length}) ->
  249. HResponse = mochiweb_headers:make(ResponseHeaders),
  250. HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
  251. start_response({Code, HResponse1}).
  252. %% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}) -> response()
  253. %% @doc Start the HTTP response with start_response, and send Body to the
  254. %% client (if the get(method) /= 'HEAD'). The Content-Length header
  255. %% will be set by the Body length, and the server will insert header
  256. %% defaults.
  257. respond({Code, ResponseHeaders, {file, IoDevice}}) ->
  258. Length = mochiweb_io:iodevice_size(IoDevice),
  259. Response = start_response_length({Code, ResponseHeaders, Length}),
  260. case Method of
  261. 'HEAD' ->
  262. ok;
  263. _ ->
  264. mochiweb_io:iodevice_stream(fun send/1, IoDevice)
  265. end,
  266. Response;
  267. respond({Code, ResponseHeaders, chunked}) ->
  268. HResponse = mochiweb_headers:make(ResponseHeaders),
  269. HResponse1 = case Method of
  270. 'HEAD' ->
  271. %% This is what Google does, http://www.google.com/
  272. %% is chunked but HEAD gets Content-Length: 0.
  273. %% The RFC is ambiguous so emulating Google is smart.
  274. mochiweb_headers:enter("Content-Length", "0",
  275. HResponse);
  276. _ when Version >= {1, 1} ->
  277. %% Only use chunked encoding for HTTP/1.1
  278. mochiweb_headers:enter("Transfer-Encoding", "chunked",
  279. HResponse);
  280. _ ->
  281. %% For pre-1.1 clients we send the data as-is
  282. %% without a Content-Length header and without
  283. %% chunk delimiters. Since the end of the document
  284. %% is now ambiguous we must force a close.
  285. put(?SAVE_FORCE_CLOSE, true),
  286. HResponse
  287. end,
  288. start_response({Code, HResponse1});
  289. respond({Code, ResponseHeaders, Body}) ->
  290. Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}),
  291. case Method of
  292. 'HEAD' ->
  293. ok;
  294. _ ->
  295. send(Body)
  296. end,
  297. Response.
  298. %% @spec not_found() -> response()
  299. %% @doc Alias for <code>not_found([])</code>.
  300. not_found() ->
  301. not_found([]).
  302. %% @spec not_found(ExtraHeaders) -> response()
  303. %% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"}
  304. %% | ExtraHeaders], &lt;&lt;"Not found."&gt;&gt;})</code>.
  305. not_found(ExtraHeaders) ->
  306. respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders],
  307. <<"Not found.">>}).
  308. %% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}) ->
  309. %% response()
  310. %% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}).
  311. ok({ContentType, Body}) ->
  312. ok({ContentType, [], Body});
  313. ok({ContentType, ResponseHeaders, Body}) ->
  314. HResponse = mochiweb_headers:make(ResponseHeaders),
  315. case THIS:get(range) of
  316. X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked ->
  317. %% http://code.google.com/p/mochiweb/issues/detail?id=54
  318. %% Range header not supported when chunked, return 200 and provide
  319. %% full response.
  320. HResponse1 = mochiweb_headers:enter("Content-Type", ContentType,
  321. HResponse),
  322. respond({200, HResponse1, Body});
  323. Ranges ->
  324. {PartList, Size} = range_parts(Body, Ranges),
  325. case PartList of
  326. [] -> %% no valid ranges
  327. HResponse1 = mochiweb_headers:enter("Content-Type",
  328. ContentType,
  329. HResponse),
  330. %% could be 416, for now we'll just return 200
  331. respond({200, HResponse1, Body});
  332. PartList ->
  333. {RangeHeaders, RangeBody} =
  334. mochiweb_multipart:parts_to_body(PartList, ContentType, Size),
  335. HResponse1 = mochiweb_headers:enter_from_list(
  336. [{"Accept-Ranges", "bytes"} |
  337. RangeHeaders],
  338. HResponse),
  339. respond({206, HResponse1, RangeBody})
  340. end
  341. end.
  342. %% @spec should_close() -> bool()
  343. %% @doc Return true if the connection must be closed. If false, using
  344. %% Keep-Alive should be safe.
  345. should_close() ->
  346. ForceClose = erlang:get(mochiweb_request_force_close) =/= undefined,
  347. DidNotRecv = erlang:get(mochiweb_request_recv) =:= undefined,
  348. ForceClose orelse Version < {1, 0}
  349. %% Connection: close
  350. orelse get_header_value("connection") =:= "close"
  351. %% HTTP 1.0 requires Connection: Keep-Alive
  352. orelse (Version =:= {1, 0}
  353. andalso get_header_value("connection") =/= "Keep-Alive")
  354. %% unread data left on the socket, can't safely continue
  355. orelse (DidNotRecv
  356. andalso get_header_value("content-length") =/= undefined
  357. andalso list_to_integer(get_header_value("content-length")) > 0)
  358. orelse (DidNotRecv
  359. andalso get_header_value("transfer-encoding") =:= "chunked").
  360. %% @spec cleanup() -> ok
  361. %% @doc Clean up any junk in the process dictionary, required before continuing
  362. %% a Keep-Alive request.
  363. cleanup() ->
  364. [erase(K) || K <- [?SAVE_QS,
  365. ?SAVE_PATH,
  366. ?SAVE_RECV,
  367. ?SAVE_BODY,
  368. ?SAVE_POST,
  369. ?SAVE_COOKIE,
  370. ?SAVE_FORCE_CLOSE]],
  371. ok.
  372. %% @spec get_qs_value(Key::string) -> string() | undefined
  373. %% @doc Get the value of the given query string parameter.
  374. get_qs_value(Key) ->
  375. proplists:get_value(Key, parse_qs()).
  376. %% @spec parse_qs() -> [{Key::string(), Value::string()}]
  377. %% @doc Parse the query string of the URL.
  378. parse_qs() ->
  379. case erlang:get(?SAVE_QS) of
  380. undefined ->
  381. {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
  382. Parsed = mochiweb_util:parse_qs(QueryString),
  383. put(?SAVE_QS, Parsed),
  384. Parsed;
  385. Cached ->
  386. Cached
  387. end.
  388. %% @spec get_cookie_value(Key::string) -> string() | undefined
  389. %% @doc Get the value of the given cookie.
  390. get_cookie_value(Key) ->
  391. proplists:get_value(Key, parse_cookie()).
  392. %% @spec parse_cookie() -> [{Key::string(), Value::string()}]
  393. %% @doc Parse the cookie header.
  394. parse_cookie() ->
  395. case erlang:get(?SAVE_COOKIE) of
  396. undefined ->
  397. Cookies = case get_header_value("cookie") of
  398. undefined ->
  399. [];
  400. Value ->
  401. mochiweb_cookies:parse_cookie(Value)
  402. end,
  403. put(?SAVE_COOKIE, Cookies),
  404. Cookies;
  405. Cached ->
  406. Cached
  407. end.
  408. %% @spec get_post_value(Key::string) -> string() | undefined
  409. %% @doc Get the value of the given POST parameter.
  410. get_post_value(Key) ->
  411. proplists:get_value(Key, parse_post()).
  412. %% @spec parse_post() -> [{Key::string(), Value::string()}]
  413. %% @doc Parse an application/x-www-form-urlencoded form POST. This
  414. %% has the side-effect of calling recv_body().
  415. parse_post() ->
  416. case erlang:get(?SAVE_POST) of
  417. undefined ->
  418. Parsed = case recv_body() of
  419. undefined ->
  420. [];
  421. Binary ->
  422. case get_primary_header_value("content-type") of
  423. "application/x-www-form-urlencoded" ++ _ ->
  424. mochiweb_util:parse_qs(Binary);
  425. _ ->
  426. []
  427. end
  428. end,
  429. put(?SAVE_POST, Parsed),
  430. Parsed;
  431. Cached ->
  432. Cached
  433. end.
  434. %% @spec stream_chunked_body(integer(), fun(), term()) -> term()
  435. %% @doc The function is called for each chunk.
  436. %% Used internally by read_chunked_body.
  437. stream_chunked_body(MaxChunkSize, Fun, FunState) ->
  438. case read_chunk_length() of
  439. 0 ->
  440. Fun({0, read_chunk(0)}, FunState);
  441. Length when Length > MaxChunkSize ->
  442. NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState),
  443. stream_chunked_body(MaxChunkSize, Fun, NewState);
  444. Length ->
  445. NewState = Fun({Length, read_chunk(Length)}, FunState),
  446. stream_chunked_body(MaxChunkSize, Fun, NewState)
  447. end.
  448. stream_unchunked_body(0, Fun, FunState) ->
  449. Fun({0, <<>>}, FunState);
  450. stream_unchunked_body(Length, Fun, FunState) when Length > 0 ->
  451. PktSize = case Length > ?RECBUF_SIZE of
  452. true ->
  453. ?RECBUF_SIZE;
  454. false ->
  455. Length
  456. end,
  457. Bin = recv(PktSize),
  458. NewState = Fun({PktSize, Bin}, FunState),
  459. stream_unchunked_body(Length - PktSize, Fun, NewState).
  460. %% @spec read_chunk_length() -> integer()
  461. %% @doc Read the length of the next HTTP chunk.
  462. read_chunk_length() ->
  463. mochiweb_socket:setopts(Socket, [{packet, line}]),
  464. case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
  465. {ok, Header} ->
  466. mochiweb_socket:setopts(Socket, [{packet, raw}]),
  467. Splitter = fun (C) ->
  468. C =/= $\r andalso C =/= $\n andalso C =/= $
  469. end,
  470. {Hex, _Rest} = lists:splitwith(Splitter, binary_to_list(Header)),
  471. mochihex:to_int(Hex);
  472. _ ->
  473. exit(normal)
  474. end.
  475. %% @spec read_chunk(integer()) -> Chunk::binary() | [Footer::binary()]
  476. %% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the
  477. %% HTTP footers (as a list of binaries, since they're nominal).
  478. read_chunk(0) ->
  479. mochiweb_socket:setopts(Socket, [{packet, line}]),
  480. F = fun (F1, Acc) ->
  481. case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
  482. {ok, <<"\r\n">>} ->
  483. Acc;
  484. {ok, Footer} ->
  485. F1(F1, [Footer | Acc]);
  486. _ ->
  487. exit(normal)
  488. end
  489. end,
  490. Footers = F(F, []),
  491. mochiweb_socket:setopts(Socket, [{packet, raw}]),
  492. put(?SAVE_RECV, true),
  493. Footers;
  494. read_chunk(Length) ->
  495. case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
  496. {ok, <<Chunk:Length/binary, "\r\n">>} ->
  497. Chunk;
  498. _ ->
  499. exit(normal)
  500. end.
  501. read_sub_chunks(Length, MaxChunkSize, Fun, FunState) when Length > MaxChunkSize ->
  502. Bin = recv(MaxChunkSize),
  503. NewState = Fun({size(Bin), Bin}, FunState),
  504. read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState);
  505. read_sub_chunks(Length, _MaxChunkSize, Fun, FunState) ->
  506. Fun({Length, read_chunk(Length)}, FunState).
  507. %% @spec serve_file(Path, DocRoot) -> Response
  508. %% @doc Serve a file relative to DocRoot.
  509. serve_file(Path, DocRoot) ->
  510. serve_file(Path, DocRoot, []).
  511. %% @spec serve_file(Path, DocRoot, ExtraHeaders) -> Response
  512. %% @doc Serve a file relative to DocRoot.
  513. serve_file(Path, DocRoot, ExtraHeaders) ->
  514. case mochiweb_util:safe_relative_path(Path) of
  515. undefined ->
  516. not_found(ExtraHeaders);
  517. RelPath ->
  518. FullPath = filename:join([DocRoot, RelPath]),
  519. case filelib:is_dir(FullPath) of
  520. true ->
  521. maybe_redirect(RelPath, FullPath, ExtraHeaders);
  522. false ->
  523. maybe_serve_file(FullPath, ExtraHeaders)
  524. end
  525. end.
  526. %% Internal API
  527. %% This has the same effect as the DirectoryIndex directive in httpd
  528. directory_index(FullPath) ->
  529. filename:join([FullPath, "index.html"]).
  530. maybe_redirect([], FullPath, ExtraHeaders) ->
  531. maybe_serve_file(directory_index(FullPath), ExtraHeaders);
  532. maybe_redirect(RelPath, FullPath, ExtraHeaders) ->
  533. case string:right(RelPath, 1) of
  534. "/" ->
  535. maybe_serve_file(directory_index(FullPath), ExtraHeaders);
  536. _ ->
  537. Host = mochiweb_headers:get_value("host", Headers),
  538. Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/",
  539. LocationBin = list_to_binary(Location),
  540. MoreHeaders = [{"Location", Location},
  541. {"Content-Type", "text/html"} | ExtraHeaders],
  542. Top = <<"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">"
  543. "<html><head>"
  544. "<title>301 Moved Permanently</title>"
  545. "</head><body>"
  546. "<h1>Moved Permanently</h1>"
  547. "<p>The document has moved <a href=\"">>,
  548. Bottom = <<">here</a>.</p></body></html>\n">>,
  549. Body = <<Top/binary, LocationBin/binary, Bottom/binary>>,
  550. respond({301, MoreHeaders, Body})
  551. end.
  552. maybe_serve_file(File, ExtraHeaders) ->
  553. case file:read_file_info(File) of
  554. {ok, _FileInfo} ->
  555. % HACK EMM
  556. %LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime),
  557. LastModified = httpd_util:rfc1123_date(),
  558. case get_header_value("if-modified-since") of
  559. LastModified ->
  560. respond({304, ExtraHeaders, ""});
  561. _ ->
  562. case file:open(File, [raw, binary]) of
  563. {ok, IoDevice} ->
  564. ContentType = mochiweb_util:guess_mime(File),
  565. Res = ok({ContentType,
  566. [{"last-modified", LastModified}
  567. | ExtraHeaders],
  568. {file, IoDevice}}),
  569. file:close(IoDevice),
  570. Res;
  571. _ ->
  572. not_found(ExtraHeaders)
  573. end
  574. end;
  575. {error, _} ->
  576. not_found(ExtraHeaders)
  577. end.
  578. server_headers() ->
  579. [{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"},
  580. {"Date", httpd_util:rfc1123_date()}].
  581. make_code(X) when is_integer(X) ->
  582. [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]];
  583. make_code(Io) when is_list(Io); is_binary(Io) ->
  584. Io.
  585. make_version({1, 0}) ->
  586. <<"HTTP/1.0 ">>;
  587. make_version(_) ->
  588. <<"HTTP/1.1 ">>.
  589. range_parts({file, IoDevice}, Ranges) ->
  590. Size = mochiweb_io:iodevice_size(IoDevice),
  591. F = fun (Spec, Acc) ->
  592. case mochiweb_http:range_skip_length(Spec, Size) of
  593. invalid_range ->
  594. Acc;
  595. V ->
  596. [V | Acc]
  597. end
  598. end,
  599. LocNums = lists:foldr(F, [], Ranges),
  600. {ok, Data} = file:pread(IoDevice, LocNums),
  601. Bodies = lists:zipwith(fun ({Skip, Length}, PartialBody) ->
  602. {Skip, Skip + Length - 1, PartialBody}
  603. end,
  604. LocNums, Data),
  605. {Bodies, Size};
  606. range_parts(Body0, Ranges) ->
  607. Body = iolist_to_binary(Body0),
  608. Size = size(Body),
  609. F = fun(Spec, Acc) ->
  610. case mochiweb_http:range_skip_length(Spec, Size) of
  611. invalid_range ->
  612. Acc;
  613. {Skip, Length} ->
  614. <<_:Skip/binary, PartialBody:Length/binary, _/binary>> = Body,
  615. [{Skip, Skip + Length - 1, PartialBody} | Acc]
  616. end
  617. end,
  618. {lists:foldr(F, [], Ranges), Size}.
  619. %% @spec accepted_encodings([encoding()]) -> [encoding()] | bad_accept_encoding_value
  620. %% @type encoding() = string().
  621. %%
  622. %% @doc Returns a list of encodings accepted by a request. Encodings that are
  623. %% not supported by the server will not be included in the return list.
  624. %% This list is computed from the "Accept-Encoding" header and
  625. %% its elements are ordered, descendingly, according to their Q values.
  626. %%
  627. %% Section 14.3 of the RFC 2616 (HTTP 1.1) describes the "Accept-Encoding"
  628. %% header and the process of determining which server supported encodings
  629. %% can be used for encoding the body for the request's response.
  630. %%
  631. %% Examples
  632. %%
  633. %% 1) For a missing "Accept-Encoding" header:
  634. %% accepted_encodings(["gzip", "identity"]) -> ["identity"]
  635. %%
  636. %% 2) For an "Accept-Encoding" header with value "gzip, deflate":
  637. %% accepted_encodings(["gzip", "identity"]) -> ["gzip", "identity"]
  638. %%
  639. %% 3) For an "Accept-Encoding" header with value "gzip;q=0.5, deflate":
  640. %% accepted_encodings(["gzip", "deflate", "identity"]) ->
  641. %% ["deflate", "gzip", "identity"]
  642. %%
  643. accepted_encodings(SupportedEncodings) ->
  644. AcceptEncodingHeader = case get_header_value("Accept-Encoding") of
  645. undefined ->
  646. "";
  647. Value ->
  648. Value
  649. end,
  650. case mochiweb_util:parse_qvalues(AcceptEncodingHeader) of
  651. invalid_qvalue_string ->
  652. bad_accept_encoding_value;
  653. QList ->
  654. mochiweb_util:pick_accepted_encodings(
  655. QList, SupportedEncodings, "identity"
  656. )
  657. end.
  658. %%
  659. %% Tests
  660. %%
  661. -include_lib("eunit/include/eunit.hrl").
  662. -ifdef(TEST).
  663. -endif.