PageRenderTime 30ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/apps/couch/src/couch_httpd.erl

http://github.com/cloudant/bigcouch
Erlang | 1014 lines | 834 code | 127 blank | 53 comment | 12 complexity | 1fb4312f323455c5403d4d3933fad250 MD5 | raw file
Possible License(s): Apache-2.0
  1. % Licensed under the Apache License, Version 2.0 (the "License"); you may not
  2. % use this file except in compliance with the License. You may obtain a copy of
  3. % the License at
  4. %
  5. % http://www.apache.org/licenses/LICENSE-2.0
  6. %
  7. % Unless required by applicable law or agreed to in writing, software
  8. % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. % License for the specific language governing permissions and limitations under
  11. % the License.
  12. -module(couch_httpd).
  13. -include("couch_db.hrl").
  14. -export([start_link/0, start_link/1, stop/0, handle_request/5]).
  15. -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,qs_json_value/3]).
  16. -export([path/1,absolute_uri/2,body_length/1]).
  17. -export([verify_is_server_admin/1,unquote/1,quote/1,recv/2,recv_chunked/4,error_info/1]).
  18. -export([make_fun_spec_strs/1]).
  19. -export([make_arity_1_fun/1, make_arity_2_fun/1, make_arity_3_fun/1]).
  20. -export([parse_form/1,json_body/1,json_body_obj/1,body/1,doc_etag/1, make_etag/1, etag_respond/3]).
  21. -export([primary_header_value/2,partition/1,serve_file/3,serve_file/4, server_header/0]).
  22. -export([start_chunked_response/3,send_chunk/2,log_request/2]).
  23. -export([start_response_length/4, start_response/3, send/2]).
  24. -export([start_json_response/2, start_json_response/3, end_json_response/1]).
  25. -export([send_response/4,send_method_not_allowed/2,send_error/4, send_redirect/2,send_chunked_error/2]).
  26. -export([send_json/2,send_json/3,send_json/4,last_chunk/1,parse_multipart_request/3]).
  27. -export([accepted_encodings/1,handle_request_int/5,validate_referer/1,validate_ctype/2]).
  28. start_link() ->
  29. start_link(http).
  30. start_link(http) ->
  31. Port = couch_config:get("httpd", "port", "5984"),
  32. start_link(?MODULE, [{port, Port}]);
  33. start_link(https) ->
  34. Port = couch_config:get("ssl", "port", "6984"),
  35. CertFile = couch_config:get("ssl", "cert_file", nil),
  36. KeyFile = couch_config:get("ssl", "key_file", nil),
  37. Options = case CertFile /= nil andalso KeyFile /= nil of
  38. true ->
  39. [{port, Port},
  40. {ssl, true},
  41. {ssl_opts, [
  42. {certfile, CertFile},
  43. {keyfile, KeyFile}]}];
  44. false ->
  45. io:format("SSL enabled but PEM certificates are missing.", []),
  46. throw({error, missing_certs})
  47. end,
  48. start_link(https, Options).
  49. start_link(Name, Options) ->
  50. % read config and register for configuration changes
  51. % just stop if one of the config settings change. couch_server_sup
  52. % will restart us and then we will pick up the new settings.
  53. BindAddress = couch_config:get("httpd", "bind_address", any),
  54. DefaultSpec = "{couch_httpd_db, handle_request}",
  55. DefaultFun = make_arity_1_fun(
  56. couch_config:get("httpd", "default_handler", DefaultSpec)
  57. ),
  58. UrlHandlersList = lists:map(
  59. fun({UrlKey, SpecStr}) ->
  60. {?l2b(UrlKey), make_arity_1_fun(SpecStr)}
  61. end, couch_config:get("httpd_global_handlers")),
  62. DbUrlHandlersList = lists:map(
  63. fun({UrlKey, SpecStr}) ->
  64. {?l2b(UrlKey), make_arity_2_fun(SpecStr)}
  65. end, couch_config:get("httpd_db_handlers")),
  66. DesignUrlHandlersList = lists:map(
  67. fun({UrlKey, SpecStr}) ->
  68. {?l2b(UrlKey), make_arity_3_fun(SpecStr)}
  69. end, couch_config:get("httpd_design_handlers")),
  70. UrlHandlers = dict:from_list(UrlHandlersList),
  71. DbUrlHandlers = dict:from_list(DbUrlHandlersList),
  72. DesignUrlHandlers = dict:from_list(DesignUrlHandlersList),
  73. {ok, ServerOptions} = couch_util:parse_term(
  74. couch_config:get("httpd", "server_options", "[]")),
  75. {ok, SocketOptions} = couch_util:parse_term(
  76. couch_config:get("httpd", "socket_options", "[]")),
  77. Loop = fun(Req)->
  78. case SocketOptions of
  79. [] ->
  80. ok;
  81. _ ->
  82. ok = mochiweb_socket:setopts(Req:get(socket), SocketOptions)
  83. end,
  84. apply(?MODULE, handle_request, [
  85. Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers
  86. ])
  87. end,
  88. % and off we go
  89. {ok, Pid} = case mochiweb_http:start(Options ++ ServerOptions ++ [
  90. {loop, Loop},
  91. {name, Name},
  92. {ip, BindAddress}
  93. ]) of
  94. {ok, MochiPid} -> {ok, MochiPid};
  95. {error, Reason} ->
  96. io:format("Failure to start Mochiweb: ~s~n",[Reason]),
  97. throw({error, Reason})
  98. end,
  99. ok = couch_config:register(
  100. fun("httpd", "bind_address") ->
  101. ?MODULE:stop();
  102. ("httpd", "port") ->
  103. ?MODULE:stop();
  104. ("httpd", "default_handler") ->
  105. ?MODULE:stop();
  106. ("httpd", "server_options") ->
  107. ?MODULE:stop();
  108. ("httpd", "socket_options") ->
  109. ?MODULE:stop();
  110. ("httpd_global_handlers", _) ->
  111. ?MODULE:stop();
  112. ("httpd_db_handlers", _) ->
  113. ?MODULE:stop();
  114. ("vhosts", _) ->
  115. ?MODULE:stop();
  116. ("ssl", _) ->
  117. ?MODULE:stop()
  118. end, Pid),
  119. {ok, Pid}.
  120. % SpecStr is a string like "{my_module, my_fun}"
  121. % or "{my_module, my_fun, <<"my_arg">>}"
  122. make_arity_1_fun(SpecStr) ->
  123. case couch_util:parse_term(SpecStr) of
  124. {ok, {Mod, Fun, SpecArg}} ->
  125. fun(Arg) -> Mod:Fun(Arg, SpecArg) end;
  126. {ok, {Mod, Fun}} ->
  127. fun(Arg) -> Mod:Fun(Arg) end
  128. end.
  129. make_arity_2_fun(SpecStr) ->
  130. case couch_util:parse_term(SpecStr) of
  131. {ok, {Mod, Fun, SpecArg}} ->
  132. fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2, SpecArg) end;
  133. {ok, {Mod, Fun}} ->
  134. fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2) end
  135. end.
  136. make_arity_3_fun(SpecStr) ->
  137. case couch_util:parse_term(SpecStr) of
  138. {ok, {Mod, Fun, SpecArg}} ->
  139. fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3, SpecArg) end;
  140. {ok, {Mod, Fun}} ->
  141. fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3) end
  142. end.
  143. % SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}"
  144. make_fun_spec_strs(SpecStr) ->
  145. re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}]).
  146. stop() ->
  147. mochiweb_http:stop(?MODULE).
  148. handle_request(MochiReq, DefaultFun, UrlHandlers, DbUrlHandlers,
  149. DesignUrlHandlers) ->
  150. MochiReq1 = couch_httpd_vhost:match_vhost(MochiReq),
  151. handle_request_int(MochiReq1, DefaultFun,
  152. UrlHandlers, DbUrlHandlers, DesignUrlHandlers).
  153. handle_request_int(MochiReq, DefaultFun,
  154. UrlHandlers, DbUrlHandlers, DesignUrlHandlers) ->
  155. Begin = now(),
  156. AuthenticationSrcs = make_fun_spec_strs(
  157. couch_config:get("httpd", "authentication_handlers")),
  158. % for the path, use the raw path with the query string and fragment
  159. % removed, but URL quoting left intact
  160. RawUri = MochiReq:get(raw_path),
  161. {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri),
  162. Headers = MochiReq:get(headers),
  163. % get requested path
  164. RequestedPath = case MochiReq:get_header_value("x-couchdb-vhost-path") of
  165. undefined -> RawUri;
  166. P -> P
  167. end,
  168. HandlerKey =
  169. case mochiweb_util:partition(Path, "/") of
  170. {"", "", ""} ->
  171. <<"/">>; % Special case the root url handler
  172. {FirstPart, _, _} ->
  173. list_to_binary(FirstPart)
  174. end,
  175. ?LOG_DEBUG("~p ~s ~p from ~p~nHeaders: ~p", [
  176. MochiReq:get(method),
  177. RawUri,
  178. MochiReq:get(version),
  179. MochiReq:get(peer),
  180. mochiweb_headers:to_list(MochiReq:get(headers))
  181. ]),
  182. Method1 =
  183. case MochiReq:get(method) of
  184. % already an atom
  185. Meth when is_atom(Meth) -> Meth;
  186. % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when
  187. % possible (if any module references the atom, then it's existing).
  188. Meth -> couch_util:to_existing_atom(Meth)
  189. end,
  190. increment_method_stats(Method1),
  191. % allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header
  192. MethodOverride = MochiReq:get_primary_header_value("X-HTTP-Method-Override"),
  193. Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "COPY"]) of
  194. true ->
  195. ?LOG_INFO("MethodOverride: ~s (real method was ~s)", [MethodOverride, Method1]),
  196. case Method1 of
  197. 'POST' -> couch_util:to_existing_atom(MethodOverride);
  198. _ ->
  199. % Ignore X-HTTP-Method-Override when the original verb isn't POST.
  200. % I'd like to send a 406 error to the client, but that'd require a nasty refactor.
  201. % throw({not_acceptable, <<"X-HTTP-Method-Override may only be used with POST requests.">>})
  202. Method1
  203. end;
  204. _ -> Method1
  205. end,
  206. % alias HEAD to GET as mochiweb takes care of stripping the body
  207. Method = case Method2 of
  208. 'HEAD' -> 'GET';
  209. Other -> Other
  210. end,
  211. HttpReq = #httpd{
  212. mochi_req = MochiReq,
  213. peer = MochiReq:get(peer),
  214. method = Method,
  215. requested_path_parts = [list_to_binary(couch_httpd:unquote(Part))
  216. || Part <- string:tokens(RequestedPath, "/")],
  217. path_parts = [list_to_binary(couch_httpd:unquote(Part))
  218. || Part <- string:tokens(Path, "/")],
  219. db_url_handlers = DbUrlHandlers,
  220. design_url_handlers = DesignUrlHandlers,
  221. default_fun = DefaultFun,
  222. url_handlers = UrlHandlers
  223. },
  224. HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun),
  225. {ok, Resp} =
  226. try
  227. case authenticate_request(HttpReq, AuthenticationSrcs) of
  228. #httpd{} = Req ->
  229. HandlerFun(Req);
  230. Response ->
  231. Response
  232. end
  233. catch
  234. throw:{http_head_abort, Resp0} ->
  235. {ok, Resp0};
  236. throw:{invalid_json, S} ->
  237. ?LOG_ERROR("attempted upload of invalid JSON (set log_level to debug to log it)", []),
  238. ?LOG_DEBUG("Invalid JSON: ~p",[S]),
  239. send_error(HttpReq, {bad_request, io_lib:format("invalid UTF-8 JSON: ~p",[S])});
  240. throw:unacceptable_encoding ->
  241. ?LOG_ERROR("unsupported encoding method for the response", []),
  242. send_error(HttpReq, {not_acceptable, "unsupported encoding"});
  243. throw:bad_accept_encoding_value ->
  244. ?LOG_ERROR("received invalid Accept-Encoding header", []),
  245. send_error(HttpReq, bad_request);
  246. exit:normal ->
  247. exit(normal);
  248. throw:Error ->
  249. ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]),
  250. ?LOG_DEBUG("Stacktrace: ~p",[erlang:get_stacktrace()]),
  251. send_error(HttpReq, Error);
  252. error:badarg ->
  253. ?LOG_ERROR("Badarg error in HTTP request",[]),
  254. ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]),
  255. send_error(HttpReq, badarg);
  256. error:function_clause ->
  257. ?LOG_ERROR("function_clause error in HTTP request",[]),
  258. ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]),
  259. send_error(HttpReq, function_clause);
  260. Tag:Error ->
  261. ?LOG_ERROR("Uncaught error in HTTP request: ~p",[{Tag, Error}]),
  262. ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]),
  263. send_error(HttpReq, Error)
  264. end,
  265. RequestTime = round(timer:now_diff(now(), Begin)/1000),
  266. couch_stats_collector:record({couchdb, request_time}, RequestTime),
  267. couch_stats_collector:increment({httpd, requests}),
  268. {ok, Resp}.
  269. % Try authentication handlers in order until one sets a user_ctx
  270. % the auth funs also have the option of returning a response
  271. % move this to couch_httpd_auth?
  272. authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthSrcs) ->
  273. Req;
  274. authenticate_request(#httpd{} = Req, []) ->
  275. case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
  276. "true" ->
  277. throw({unauthorized, <<"Authentication required.">>});
  278. "false" ->
  279. Req#httpd{user_ctx=#user_ctx{}}
  280. end;
  281. authenticate_request(#httpd{} = Req, [AuthSrc|Rest]) ->
  282. AuthFun = make_arity_1_fun(AuthSrc),
  283. R = case AuthFun(Req) of
  284. #httpd{user_ctx=#user_ctx{}=UserCtx}=Req2 ->
  285. Req2#httpd{user_ctx=UserCtx#user_ctx{handler=?l2b(AuthSrc)}};
  286. Else -> Else
  287. end,
  288. authenticate_request(R, Rest);
  289. authenticate_request(Response, _AuthSrcs) ->
  290. Response.
  291. increment_method_stats(Method) ->
  292. couch_stats_collector:increment({httpd_request_methods, Method}).
  293. validate_referer(Req) ->
  294. Host = host_for_request(Req),
  295. Referer = header_value(Req, "Referer", fail),
  296. case Referer of
  297. fail ->
  298. throw({bad_request, <<"Referer header required.">>});
  299. Referer ->
  300. {_,RefererHost,_,_,_} = mochiweb_util:urlsplit(Referer),
  301. if
  302. RefererHost =:= Host -> ok;
  303. true -> throw({bad_request, <<"Referer header must match host.">>})
  304. end
  305. end.
  306. validate_ctype(Req, Ctype) ->
  307. case couch_httpd:header_value(Req, "Content-Type") of
  308. undefined ->
  309. throw({bad_ctype, "Content-Type must be "++Ctype});
  310. ReqCtype ->
  311. % ?LOG_ERROR("Ctype ~p ReqCtype ~p",[Ctype,ReqCtype]),
  312. case re:split(ReqCtype, ";", [{return, list}]) of
  313. [Ctype] -> ok;
  314. [Ctype, _Rest] -> ok;
  315. _Else ->
  316. throw({bad_ctype, "Content-Type must be "++Ctype})
  317. end
  318. end.
  319. % Utilities
  320. partition(Path) ->
  321. mochiweb_util:partition(Path, "/").
  322. header_value(#httpd{mochi_req=MochiReq}, Key) ->
  323. MochiReq:get_header_value(Key).
  324. header_value(#httpd{mochi_req=MochiReq}, Key, Default) ->
  325. case MochiReq:get_header_value(Key) of
  326. undefined -> Default;
  327. Value -> Value
  328. end.
  329. primary_header_value(#httpd{mochi_req=MochiReq}, Key) ->
  330. MochiReq:get_primary_header_value(Key).
  331. accepted_encodings(#httpd{mochi_req=MochiReq}) ->
  332. case MochiReq:accepted_encodings(["gzip", "identity"]) of
  333. bad_accept_encoding_value ->
  334. throw(bad_accept_encoding_value);
  335. [] ->
  336. throw(unacceptable_encoding);
  337. EncList ->
  338. EncList
  339. end.
  340. serve_file(Req, RelativePath, DocumentRoot) ->
  341. serve_file(Req, RelativePath, DocumentRoot, []).
  342. serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot, ExtraHeaders) ->
  343. {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
  344. server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ ExtraHeaders)}.
  345. qs_value(Req, Key) ->
  346. qs_value(Req, Key, undefined).
  347. qs_value(Req, Key, Default) ->
  348. couch_util:get_value(Key, qs(Req), Default).
  349. qs_json_value(Req, Key, Default) ->
  350. case qs_value(Req, Key, Default) of
  351. Default ->
  352. Default;
  353. Result ->
  354. ?JSON_DECODE(Result)
  355. end.
  356. qs(#httpd{mochi_req=MochiReq}) ->
  357. MochiReq:parse_qs().
  358. path(#httpd{mochi_req=MochiReq}) ->
  359. MochiReq:get(path).
  360. host_for_request(#httpd{mochi_req=MochiReq}) ->
  361. XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"),
  362. case MochiReq:get_header_value(XHost) of
  363. undefined ->
  364. case MochiReq:get_header_value("Host") of
  365. undefined ->
  366. {ok, {Address, Port}} = inet:sockname(MochiReq:get(socket)),
  367. inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port);
  368. Value1 ->
  369. Value1
  370. end;
  371. Value -> Value
  372. end.
  373. absolute_uri(#httpd{mochi_req=MochiReq}=Req, Path) ->
  374. Host = host_for_request(Req),
  375. XSsl = couch_config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"),
  376. Scheme = case MochiReq:get_header_value(XSsl) of
  377. "on" -> "https";
  378. _ ->
  379. XProto = couch_config:get("httpd", "x_forwarded_proto", "X-Forwarded-Proto"),
  380. case MochiReq:get_header_value(XProto) of
  381. %% Restrict to "https" and "http" schemes only
  382. "https" -> "https";
  383. _ -> case MochiReq:get(scheme) of
  384. https -> "https";
  385. http -> "http"
  386. end
  387. end
  388. end,
  389. Scheme ++ "://" ++ Host ++ Path.
  390. unquote(UrlEncodedString) ->
  391. mochiweb_util:unquote(UrlEncodedString).
  392. quote(UrlDecodedString) ->
  393. mochiweb_util:quote_plus(UrlDecodedString).
  394. parse_form(#httpd{mochi_req=MochiReq}) ->
  395. mochiweb_multipart:parse_form(MochiReq).
  396. recv(#httpd{mochi_req=MochiReq}, Len) ->
  397. MochiReq:recv(Len).
  398. recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) ->
  399. % Fun is called once with each chunk
  400. % Fun({Length, Binary}, State)
  401. % called with Length == 0 on the last time.
  402. MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState).
  403. body_length(Req) ->
  404. case header_value(Req, "Transfer-Encoding") of
  405. undefined ->
  406. case header_value(Req, "Content-Length") of
  407. undefined -> undefined;
  408. Length -> list_to_integer(Length)
  409. end;
  410. "chunked" -> chunked;
  411. Unknown -> {unknown_transfer_encoding, Unknown}
  412. end.
  413. body(#httpd{mochi_req=MochiReq, req_body=undefined} = Req) ->
  414. case body_length(Req) of
  415. undefined ->
  416. MaxSize = list_to_integer(
  417. couch_config:get("couchdb", "max_document_size", "4294967296")),
  418. MochiReq:recv_body(MaxSize);
  419. chunked ->
  420. ChunkFun = fun({0, _Footers}, Acc) ->
  421. lists:reverse(Acc);
  422. ({_Len, Chunk}, Acc) ->
  423. [Chunk | Acc]
  424. end,
  425. recv_chunked(Req, 8192, ChunkFun, []);
  426. Len ->
  427. MochiReq:recv_body(Len)
  428. end;
  429. body(#httpd{req_body=ReqBody}) ->
  430. ReqBody.
  431. json_body(Httpd) ->
  432. ?JSON_DECODE(body(Httpd)).
  433. json_body_obj(Httpd) ->
  434. case json_body(Httpd) of
  435. {Props} -> {Props};
  436. _Else ->
  437. throw({bad_request, "Request body must be a JSON object"})
  438. end.
  439. doc_etag(#doc{revs={Start, [DiskRev|_]}}) ->
  440. "\"" ++ ?b2l(couch_doc:rev_to_str({Start, DiskRev})) ++ "\"".
  441. make_etag(Term) ->
  442. <<SigInt:128/integer>> = couch_util:md5(term_to_binary(Term)),
  443. list_to_binary("\"" ++ lists:flatten(io_lib:format("~.36B",[SigInt])) ++ "\"").
  444. etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) ->
  445. etag_match(Req, binary_to_list(CurrentEtag));
  446. etag_match(Req, CurrentEtag) ->
  447. EtagsToMatch = string:tokens(
  448. couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
  449. lists:member(CurrentEtag, EtagsToMatch).
  450. etag_respond(Req, CurrentEtag, RespFun) ->
  451. case etag_match(Req, CurrentEtag) of
  452. true ->
  453. % the client has this in their cache.
  454. couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
  455. false ->
  456. % Run the function.
  457. RespFun()
  458. end.
  459. verify_is_server_admin(#httpd{user_ctx=UserCtx}) ->
  460. verify_is_server_admin(UserCtx);
  461. verify_is_server_admin(#user_ctx{roles=Roles}) ->
  462. case lists:member(<<"_admin">>, Roles) of
  463. true -> ok;
  464. false -> throw({unauthorized, <<"You are not a server admin.">>})
  465. end.
  466. log_request(_, _) ->
  467. noop. % we have twig
  468. start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) ->
  469. log_request(Req, Code),
  470. couch_stats_collector:increment({httpd_status_codes, Code}),
  471. Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), Length}),
  472. case MochiReq:get(method) of
  473. 'HEAD' -> throw({http_head_abort, Resp});
  474. _ -> ok
  475. end,
  476. {ok, Resp}.
  477. start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
  478. log_request(Req, Code),
  479. couch_stats_collector:increment({httpd_status_cdes, Code}),
  480. CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers),
  481. Headers2 = Headers ++ server_header() ++ CookieHeader,
  482. Resp = MochiReq:start_response({Code, Headers2}),
  483. case MochiReq:get(method) of
  484. 'HEAD' -> throw({http_head_abort, Resp});
  485. _ -> ok
  486. end,
  487. {ok, Resp}.
  488. send(Resp, Data) ->
  489. Resp:send(Data),
  490. {ok, Resp}.
  491. no_resp_conn_header([]) ->
  492. true;
  493. no_resp_conn_header([{Hdr, _}|Rest]) ->
  494. case string:to_lower(Hdr) of
  495. "connection" -> false;
  496. _ -> no_resp_conn_header(Rest)
  497. end.
  498. http_1_0_keep_alive(Req, Headers) ->
  499. KeepOpen = Req:should_close() == false,
  500. IsHttp10 = Req:get(version) == {1, 0},
  501. NoRespHeader = no_resp_conn_header(Headers),
  502. case KeepOpen andalso IsHttp10 andalso NoRespHeader of
  503. true -> [{"Connection", "Keep-Alive"} | Headers];
  504. false -> Headers
  505. end.
  506. start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
  507. log_request(Req, Code),
  508. couch_stats_collector:increment({httpd_status_codes, Code}),
  509. Headers2 = http_1_0_keep_alive(MochiReq, Headers),
  510. Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers2), chunked}),
  511. case MochiReq:get(method) of
  512. 'HEAD' -> throw({http_head_abort, Resp});
  513. _ -> ok
  514. end,
  515. {ok, Resp}.
  516. send_chunk(Resp, Data) ->
  517. case iolist_size(Data) of
  518. 0 -> ok; % do nothing
  519. _ -> Resp:write_chunk(Data)
  520. end,
  521. {ok, Resp}.
  522. last_chunk(Resp) ->
  523. Resp:write_chunk([]),
  524. {ok, Resp}.
  525. send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
  526. log_request(Req, Code),
  527. couch_stats_collector:increment({httpd_status_codes, Code}),
  528. Headers2 = http_1_0_keep_alive(MochiReq, Headers),
  529. if Code >= 400 ->
  530. ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]);
  531. true -> ok
  532. end,
  533. {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers2), Body})}.
  534. send_method_not_allowed(Req, Methods) ->
  535. send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")).
  536. send_json(Req, Value) ->
  537. send_json(Req, 200, Value).
  538. send_json(Req, Code, Value) ->
  539. send_json(Req, Code, [], Value).
  540. send_json(Req, Code, Headers, Value) ->
  541. initialize_jsonp(Req),
  542. DefaultHeaders = [
  543. {"Content-Type", negotiate_content_type(Req)},
  544. {"Cache-Control", "must-revalidate"}
  545. ],
  546. Body = [start_jsonp(), ?JSON_ENCODE(Value), end_jsonp(), $\n],
  547. send_response(Req, Code, DefaultHeaders ++ Headers, Body).
  548. start_json_response(Req, Code) ->
  549. start_json_response(Req, Code, []).
  550. start_json_response(Req, Code, Headers) ->
  551. initialize_jsonp(Req),
  552. DefaultHeaders = [
  553. {"Content-Type", negotiate_content_type(Req)},
  554. {"Cache-Control", "must-revalidate"}
  555. ],
  556. {ok, Resp} = start_chunked_response(Req, Code, DefaultHeaders ++ Headers),
  557. case start_jsonp() of
  558. [] -> ok;
  559. Start -> send_chunk(Resp, Start)
  560. end,
  561. {ok, Resp}.
  562. end_json_response(Resp) ->
  563. send_chunk(Resp, end_jsonp() ++ [$\n]),
  564. last_chunk(Resp).
  565. initialize_jsonp(Req) ->
  566. case get(jsonp) of
  567. undefined -> put(jsonp, qs_value(Req, "callback", no_jsonp));
  568. _ -> ok
  569. end,
  570. case get(jsonp) of
  571. no_jsonp -> [];
  572. [] -> [];
  573. CallBack ->
  574. try
  575. % make sure jsonp is configured on (default off)
  576. case couch_config:get("httpd", "allow_jsonp", "false") of
  577. "true" ->
  578. validate_callback(CallBack);
  579. _Else ->
  580. put(jsonp, no_jsonp)
  581. end
  582. catch
  583. Error ->
  584. put(jsonp, no_jsonp),
  585. throw(Error)
  586. end
  587. end.
  588. start_jsonp() ->
  589. case get(jsonp) of
  590. no_jsonp -> [];
  591. [] -> [];
  592. CallBack -> CallBack ++ "("
  593. end.
  594. end_jsonp() ->
  595. Resp = case get(jsonp) of
  596. no_jsonp -> [];
  597. [] -> [];
  598. _ -> ");"
  599. end,
  600. put(jsonp, undefined),
  601. Resp.
  602. validate_callback(CallBack) when is_binary(CallBack) ->
  603. validate_callback(binary_to_list(CallBack));
  604. validate_callback([]) ->
  605. ok;
  606. validate_callback([Char | Rest]) ->
  607. case Char of
  608. _ when Char >= $a andalso Char =< $z -> ok;
  609. _ when Char >= $A andalso Char =< $Z -> ok;
  610. _ when Char >= $0 andalso Char =< $9 -> ok;
  611. _ when Char == $. -> ok;
  612. _ when Char == $_ -> ok;
  613. _ when Char == $[ -> ok;
  614. _ when Char == $] -> ok;
  615. _ ->
  616. throw({bad_request, invalid_callback})
  617. end,
  618. validate_callback(Rest).
  619. error_info({Error, Reason}) when is_list(Reason) ->
  620. error_info({Error, ?l2b(Reason)});
  621. error_info(bad_request) ->
  622. {400, <<"bad_request">>, <<>>};
  623. error_info({bad_request, Reason}) ->
  624. {400, <<"bad_request">>, Reason};
  625. error_info({query_parse_error, Reason}) ->
  626. {400, <<"query_parse_error">>, Reason};
  627. % Prior art for md5 mismatch resulting in a 400 is from AWS S3
  628. error_info(md5_mismatch) ->
  629. {400, <<"content_md5_mismatch">>, <<"Possible message corruption.">>};
  630. error_info(not_found) ->
  631. {404, <<"not_found">>, <<"missing">>};
  632. error_info({not_found, Reason}) ->
  633. {404, <<"not_found">>, Reason};
  634. error_info({not_acceptable, Reason}) ->
  635. {406, <<"not_acceptable">>, Reason};
  636. error_info(conflict) ->
  637. {409, <<"conflict">>, <<"Document update conflict.">>};
  638. error_info({forbidden, Msg}) ->
  639. {403, <<"forbidden">>, Msg};
  640. error_info({unauthorized, Msg}) ->
  641. {401, <<"unauthorized">>, Msg};
  642. error_info(file_exists) ->
  643. {412, <<"file_exists">>, <<"The database could not be "
  644. "created, the file already exists.">>};
  645. error_info({bad_ctype, Reason}) ->
  646. {415, <<"bad_content_type">>, Reason};
  647. error_info(requested_range_not_satisfiable) ->
  648. {416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>};
  649. error_info({error, illegal_database_name}) ->
  650. {400, <<"illegal_database_name">>, <<"Only lowercase characters (a-z), "
  651. "digits (0-9), and any of the characters _, $, (, ), +, -, and / "
  652. "are allowed. Must begin with a letter.">>};
  653. error_info({missing_stub, Reason}) ->
  654. {412, <<"missing_stub">>, Reason};
  655. error_info({Error, Reason}) ->
  656. {500, couch_util:to_binary(Error), couch_util:to_binary(Reason)};
  657. error_info(Error) ->
  658. {500, <<"unknown_error">>, couch_util:to_binary(Error)}.
  659. error_headers(#httpd{mochi_req=MochiReq}=Req, Code, ErrorStr, ReasonStr) ->
  660. if Code == 401 ->
  661. % this is where the basic auth popup is triggered
  662. case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of
  663. undefined ->
  664. case couch_config:get("httpd", "WWW-Authenticate", nil) of
  665. nil ->
  666. % If the client is a browser and the basic auth popup isn't turned on
  667. % redirect to the session page.
  668. case ErrorStr of
  669. <<"unauthorized">> ->
  670. case couch_config:get("couch_httpd_auth", "authentication_redirect", nil) of
  671. nil -> {Code, []};
  672. AuthRedirect ->
  673. case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
  674. "true" ->
  675. % send the browser popup header no matter what if we are require_valid_user
  676. {Code, [{"WWW-Authenticate", "Basic realm=\"server\""}]};
  677. _False ->
  678. case MochiReq:accepts_content_type("application/json") of
  679. true ->
  680. {Code, []};
  681. false ->
  682. case MochiReq:accepts_content_type("text/html") of
  683. true ->
  684. % Redirect to the path the user requested, not
  685. % the one that is used internally.
  686. UrlReturnRaw = case MochiReq:get_header_value("x-couchdb-vhost-path") of
  687. undefined ->
  688. MochiReq:get(path);
  689. VHostPath ->
  690. VHostPath
  691. end,
  692. RedirectLocation = lists:flatten([
  693. AuthRedirect,
  694. "?return=", couch_util:url_encode(UrlReturnRaw),
  695. "&reason=", couch_util:url_encode(ReasonStr)
  696. ]),
  697. {302, [{"Location", absolute_uri(Req, RedirectLocation)}]};
  698. false ->
  699. {Code, []}
  700. end
  701. end
  702. end
  703. end;
  704. _Else ->
  705. {Code, []}
  706. end;
  707. Type ->
  708. {Code, [{"WWW-Authenticate", Type}]}
  709. end;
  710. Type ->
  711. {Code, [{"WWW-Authenticate", Type}]}
  712. end;
  713. true ->
  714. {Code, []}
  715. end.
  716. send_error(_Req, {already_sent, Resp, _Error}) ->
  717. {ok, Resp};
  718. send_error(Req, Error) ->
  719. {Code, ErrorStr, ReasonStr} = error_info(Error),
  720. {Code1, Headers} = error_headers(Req, Code, ErrorStr, ReasonStr),
  721. send_error(Req, Code1, Headers, ErrorStr, ReasonStr).
  722. send_error(Req, Code, ErrorStr, ReasonStr) ->
  723. send_error(Req, Code, [], ErrorStr, ReasonStr).
  724. send_error(Req, Code, Headers, ErrorStr, ReasonStr) ->
  725. send_json(Req, Code, Headers,
  726. {[{<<"error">>, ErrorStr},
  727. {<<"reason">>, ReasonStr}]}).
  728. % give the option for list functions to output html or other raw errors
  729. send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) ->
  730. send_chunk(Resp, Reason),
  731. last_chunk(Resp);
  732. send_chunked_error(Resp, Error) ->
  733. {Code, ErrorStr, ReasonStr} = error_info(Error),
  734. JsonError = {[{<<"code">>, Code},
  735. {<<"error">>, ErrorStr},
  736. {<<"reason">>, ReasonStr}]},
  737. send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])),
  738. last_chunk(Resp).
  739. send_redirect(Req, Path) ->
  740. Headers = [{"Location", couch_httpd:absolute_uri(Req, Path)}],
  741. send_response(Req, 301, Headers, <<>>).
  742. negotiate_content_type(Req) ->
  743. case get(jsonp) of
  744. no_jsonp -> negotiate_content_type1(Req);
  745. [] -> negotiate_content_type1(Req);
  746. _Callback -> "text/javascript"
  747. end.
  748. negotiate_content_type1(#httpd{mochi_req=MochiReq}) ->
  749. %% Determine the appropriate Content-Type header for a JSON response
  750. %% depending on the Accept header in the request. A request that explicitly
  751. %% lists the correct JSON MIME type will get that type, otherwise the
  752. %% response will have the generic MIME type "text/plain"
  753. AcceptedTypes = case MochiReq:get_header_value("Accept") of
  754. undefined -> [];
  755. AcceptHeader -> string:tokens(AcceptHeader, ", ")
  756. end,
  757. case lists:member("application/json", AcceptedTypes) of
  758. true -> "application/json";
  759. false -> "text/plain;charset=utf-8"
  760. end.
  761. server_header() ->
  762. [{"Server", "CouchDB/" ++ couch:version() ++
  763. " (Erlang OTP/" ++ erlang:system_info(otp_release) ++ ")"}].
  764. -record(mp, {boundary, buffer, data_fun, callback}).
  765. parse_multipart_request(ContentType, DataFun, Callback) ->
  766. Boundary0 = iolist_to_binary(get_boundary(ContentType)),
  767. Boundary = <<"\r\n--", Boundary0/binary>>,
  768. Mp = #mp{boundary= Boundary,
  769. buffer= <<>>,
  770. data_fun=DataFun,
  771. callback=Callback},
  772. {Mp2, _NilCallback} = read_until(Mp, <<"--", Boundary0/binary>>,
  773. fun(Next)-> nil_callback(Next) end),
  774. #mp{buffer=Buffer, data_fun=DataFun2, callback=Callback2} =
  775. parse_part_header(Mp2),
  776. {Buffer, DataFun2, Callback2}.
  777. nil_callback(_Data)->
  778. fun(Next) -> nil_callback(Next) end.
  779. get_boundary({"multipart/" ++ _, Opts}) ->
  780. case couch_util:get_value("boundary", Opts) of
  781. S when is_list(S) ->
  782. S
  783. end;
  784. get_boundary(ContentType) ->
  785. {"multipart/" ++ _ , Opts} = mochiweb_util:parse_header(ContentType),
  786. get_boundary({"multipart/", Opts}).
  787. split_header(<<>>) ->
  788. [];
  789. split_header(Line) ->
  790. {Name, [$: | Value]} = lists:splitwith(fun (C) -> C =/= $: end,
  791. binary_to_list(Line)),
  792. [{string:to_lower(string:strip(Name)),
  793. mochiweb_util:parse_header(Value)}].
  794. read_until(#mp{data_fun=DataFun, buffer=Buffer}=Mp, Pattern, Callback) ->
  795. case find_in_binary(Pattern, Buffer) of
  796. not_found ->
  797. Callback2 = Callback(Buffer),
  798. {Buffer2, DataFun2} = DataFun(),
  799. Buffer3 = iolist_to_binary(Buffer2),
  800. read_until(Mp#mp{data_fun=DataFun2,buffer=Buffer3}, Pattern, Callback2);
  801. {partial, 0} ->
  802. {NewData, DataFun2} = DataFun(),
  803. read_until(Mp#mp{data_fun=DataFun2,
  804. buffer= iolist_to_binary([Buffer,NewData])},
  805. Pattern, Callback);
  806. {partial, Skip} ->
  807. <<DataChunk:Skip/binary, Rest/binary>> = Buffer,
  808. Callback2 = Callback(DataChunk),
  809. {NewData, DataFun2} = DataFun(),
  810. read_until(Mp#mp{data_fun=DataFun2,
  811. buffer= iolist_to_binary([Rest | NewData])},
  812. Pattern, Callback2);
  813. {exact, 0} ->
  814. PatternLen = size(Pattern),
  815. <<_:PatternLen/binary, Rest/binary>> = Buffer,
  816. {Mp#mp{buffer= Rest}, Callback};
  817. {exact, Skip} ->
  818. PatternLen = size(Pattern),
  819. <<DataChunk:Skip/binary, _:PatternLen/binary, Rest/binary>> = Buffer,
  820. Callback2 = Callback(DataChunk),
  821. {Mp#mp{buffer= Rest}, Callback2}
  822. end.
  823. parse_part_header(#mp{callback=UserCallBack}=Mp) ->
  824. {Mp2, AccCallback} = read_until(Mp, <<"\r\n\r\n">>,
  825. fun(Next) -> acc_callback(Next, []) end),
  826. HeaderData = AccCallback(get_data),
  827. Headers =
  828. lists:foldl(fun(Line, Acc) ->
  829. split_header(Line) ++ Acc
  830. end, [], re:split(HeaderData,<<"\r\n">>, [])),
  831. NextCallback = UserCallBack({headers, Headers}),
  832. parse_part_body(Mp2#mp{callback=NextCallback}).
  833. parse_part_body(#mp{boundary=Prefix, callback=Callback}=Mp) ->
  834. {Mp2, WrappedCallback} = read_until(Mp, Prefix,
  835. fun(Data) -> body_callback_wrapper(Data, Callback) end),
  836. Callback2 = WrappedCallback(get_callback),
  837. Callback3 = Callback2(body_end),
  838. case check_for_last(Mp2#mp{callback=Callback3}) of
  839. {last, #mp{callback=Callback3}=Mp3} ->
  840. Mp3#mp{callback=Callback3(eof)};
  841. {more, Mp3} ->
  842. parse_part_header(Mp3)
  843. end.
  844. acc_callback(get_data, Acc)->
  845. iolist_to_binary(lists:reverse(Acc));
  846. acc_callback(Data, Acc)->
  847. fun(Next) -> acc_callback(Next, [Data | Acc]) end.
  848. body_callback_wrapper(get_callback, Callback) ->
  849. Callback;
  850. body_callback_wrapper(Data, Callback) ->
  851. Callback2 = Callback({body, Data}),
  852. fun(Next) -> body_callback_wrapper(Next, Callback2) end.
  853. check_for_last(#mp{buffer=Buffer, data_fun=DataFun}=Mp) ->
  854. case Buffer of
  855. <<"--",_/binary>> -> {last, Mp};
  856. <<_, _, _/binary>> -> {more, Mp};
  857. _ -> % not long enough
  858. {Data, DataFun2} = DataFun(),
  859. check_for_last(Mp#mp{buffer= <<Buffer/binary, Data/binary>>,
  860. data_fun = DataFun2})
  861. end.
  862. find_in_binary(B, Data) when size(B) > 0 ->
  863. case size(Data) - size(B) of
  864. Last when Last < 0 ->
  865. partial_find(B, Data, 0, size(Data));
  866. Last ->
  867. find_in_binary(B, size(B), Data, 0, Last)
  868. end.
  869. find_in_binary(B, BS, D, N, Last) when N =< Last->
  870. case D of
  871. <<_:N/binary, B:BS/binary, _/binary>> ->
  872. {exact, N};
  873. _ ->
  874. find_in_binary(B, BS, D, 1 + N, Last)
  875. end;
  876. find_in_binary(B, BS, D, N, Last) when N =:= 1 + Last ->
  877. partial_find(B, D, N, BS - 1).
  878. partial_find(_B, _D, _N, 0) ->
  879. not_found;
  880. partial_find(B, D, N, K) ->
  881. <<B1:K/binary, _/binary>> = B,
  882. case D of
  883. <<_Skip:N/binary, B1/binary>> ->
  884. {partial, N};
  885. _ ->
  886. partial_find(B, D, 1 + N, K - 1)
  887. end.