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 13-module(chttpd). 14 15-compile(tuple_calls). 16 17-include_lib("couch/include/couch_db.hrl"). 18-include_lib("chttpd/include/chttpd.hrl"). 19 20-export([start_link/0, start_link/1, start_link/2, 21 stop/0, handle_request/1, handle_request_int/1, 22 primary_header_value/2, header_value/2, header_value/3, qs_value/2, 23 qs_value/3, qs/1, qs_json_value/3, path/1, absolute_uri/2, body_length/1, 24 verify_is_server_admin/1, unquote/1, quote/1, recv/2, recv_chunked/4, 25 error_info/1, parse_form/1, json_body/1, json_body_obj/1, body/1, 26 doc_etag/1, make_etag/1, etag_respond/3, etag_match/2, 27 partition/1, serve_file/3, serve_file/4, 28 server_header/0, start_chunked_response/3,send_chunk/2, 29 start_response_length/4, send/2, start_json_response/2, 30 start_json_response/3, end_json_response/1, send_response/4, 31 send_response_no_cors/4, 32 send_method_not_allowed/2, send_error/2, send_error/4, send_redirect/2, 33 send_chunked_error/2, send_json/2,send_json/3,send_json/4, 34 validate_ctype/2]). 35 36-export([authenticate_request/3]). 37 38-export([start_delayed_json_response/2, start_delayed_json_response/3, 39 start_delayed_json_response/4, 40 start_delayed_chunked_response/3, start_delayed_chunked_response/4, 41 send_delayed_chunk/2, send_delayed_last_chunk/1, 42 send_delayed_error/2, end_delayed_json_response/1, 43 get_delayed_req/1]). 44 45-export([ 46 chunked_response_buffer_size/0, 47 close_delayed_json_object/4 48]). 49 50-record(delayed_resp, { 51 start_fun, 52 req, 53 code, 54 headers, 55 chunks, 56 resp=nil, 57 buffer_response=false 58}). 59 60-define(DEFAULT_SERVER_OPTIONS, "[{recbuf, undefined}]"). 61-define(DEFAULT_SOCKET_OPTIONS, "[{sndbuf, 262144}, {nodelay, true}]"). 62 63start_link() -> 64 start_link(http). 65start_link(http) -> 66 Port = config:get("chttpd", "port", "5984"), 67 start_link(?MODULE, [{port, Port}]); 68 69start_link(https) -> 70 Port = config:get("ssl", "port", "6984"), 71 {ok, Ciphers} = couch_util:parse_term(config:get("ssl", "ciphers", "undefined")), 72 {ok, Versions} = couch_util:parse_term(config:get("ssl", "tls_versions", "undefined")), 73 {ok, SecureRenegotiate} = couch_util:parse_term(config:get("ssl", "secure_renegotiate", "undefined")), 74 ServerOpts0 = 75 [{cacertfile, config:get("ssl", "cacert_file", undefined)}, 76 {keyfile, config:get("ssl", "key_file", undefined)}, 77 {certfile, config:get("ssl", "cert_file", undefined)}, 78 {password, config:get("ssl", "password", undefined)}, 79 {secure_renegotiate, SecureRenegotiate}, 80 {versions, Versions}, 81 {ciphers, Ciphers}], 82 83 case (couch_util:get_value(keyfile, ServerOpts0) == undefined orelse 84 couch_util:get_value(certfile, ServerOpts0) == undefined) of 85 true -> 86 io:format("SSL enabled but PEM certificates are missing.", []), 87 throw({error, missing_certs}); 88 false -> 89 ok 90 end, 91 92 ServerOpts = [Opt || {_, V}=Opt <- ServerOpts0, V /= undefined], 93 94 ClientOpts = case config:get("ssl", "verify_ssl_certificates", "false") of 95 "false" -> 96 []; 97 "true" -> 98 FailIfNoPeerCert = case config:get("ssl", "fail_if_no_peer_cert", "false") of 99 "false" -> false; 100 "true" -> true 101 end, 102 [{depth, list_to_integer(config:get("ssl", 103 "ssl_certificate_max_depth", "1"))}, 104 {fail_if_no_peer_cert, FailIfNoPeerCert}, 105 {verify, verify_peer}] ++ 106 case config:get("ssl", "verify_fun", undefined) of 107 undefined -> []; 108 SpecStr -> 109 [{verify_fun, couch_httpd:make_arity_3_fun(SpecStr)}] 110 end 111 end, 112 SslOpts = ServerOpts ++ ClientOpts, 113 114 Options0 = 115 [{port, Port}, 116 {ssl, true}, 117 {ssl_opts, SslOpts}], 118 CustomServerOpts = get_server_options("httpsd"), 119 Options = merge_server_options(Options0, CustomServerOpts), 120 start_link(https, Options). 121 122start_link(Name, Options) -> 123 IP = case config:get("chttpd", "bind_address", "any") of 124 "any" -> any; 125 Else -> Else 126 end, 127 ok = couch_httpd:validate_bind_address(IP), 128 129 set_auth_handlers(), 130 131 Options1 = Options ++ [ 132 {loop, fun ?MODULE:handle_request/1}, 133 {name, Name}, 134 {ip, IP} 135 ], 136 ServerOpts = get_server_options("chttpd"), 137 Options2 = merge_server_options(Options1, ServerOpts), 138 case mochiweb_http:start(Options2) of 139 {ok, Pid} -> 140 {ok, Pid}; 141 {error, Reason} -> 142 io:format("Failure to start Mochiweb: ~s~n", [Reason]), 143 {error, Reason} 144 end. 145 146get_server_options(Module) -> 147 ServerOptsCfg = 148 case Module of 149 "chttpd" -> config:get(Module, 150 "server_options", ?DEFAULT_SERVER_OPTIONS); 151 _ -> config:get(Module, "server_options", "[]") 152 end, 153 {ok, ServerOpts} = couch_util:parse_term(ServerOptsCfg), 154 ServerOpts. 155 156merge_server_options(A, B) -> 157 lists:keymerge(1, lists:sort(A), lists:sort(B)). 158 159stop() -> 160 catch mochiweb_http:stop(https), 161 mochiweb_http:stop(?MODULE). 162 163handle_request(MochiReq0) -> 164 erlang:put(?REWRITE_COUNT, 0), 165 MochiReq = couch_httpd_vhost:dispatch_host(MochiReq0), 166 handle_request_int(MochiReq). 167 168handle_request_int(MochiReq) -> 169 Begin = os:timestamp(), 170 SocketOptsCfg = config:get( 171 "chttpd", "socket_options", ?DEFAULT_SOCKET_OPTIONS), 172 {ok, SocketOpts} = couch_util:parse_term(SocketOptsCfg), 173 ok = mochiweb_socket:setopts(MochiReq:get(socket), SocketOpts), 174 175 % for the path, use the raw path with the query string and fragment 176 % removed, but URL quoting left intact 177 RawUri = MochiReq:get(raw_path), 178 {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri), 179 180 % get requested path 181 RequestedPath = case MochiReq:get_header_value("x-couchdb-vhost-path") of 182 undefined -> 183 case MochiReq:get_header_value("x-couchdb-requested-path") of 184 undefined -> RawUri; 185 R -> R 186 end; 187 P -> P 188 end, 189 190 Peer = MochiReq:get(peer), 191 192 Method1 = 193 case MochiReq:get(method) of 194 % already an atom 195 Meth when is_atom(Meth) -> Meth; 196 197 % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when 198 % possible (if any module references the atom, then it's existing). 199 Meth -> couch_util:to_existing_atom(Meth) 200 end, 201 increment_method_stats(Method1), 202 203 % allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header 204 MethodOverride = MochiReq:get_primary_header_value("X-HTTP-Method-Override"), 205 Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "COPY"]) of 206 true -> 207 couch_log:notice("MethodOverride: ~s (real method was ~s)", [MethodOverride, Method1]), 208 case Method1 of 209 'POST' -> couch_util:to_existing_atom(MethodOverride); 210 _ -> 211 % Ignore X-HTTP-Method-Override when the original verb isn't POST. 212 % I'd like to send a 406 error to the client, but that'd require a nasty refactor. 213 % throw({not_acceptable, <<"X-HTTP-Method-Override may only be used with POST requests.">>}) 214 Method1 215 end; 216 _ -> Method1 217 end, 218 219 % alias HEAD to GET as mochiweb takes care of stripping the body 220 Method = case Method2 of 221 'HEAD' -> 'GET'; 222 Other -> Other 223 end, 224 225 Nonce = couch_util:to_hex(crypto:strong_rand_bytes(5)), 226 227 HttpReq0 = #httpd{ 228 mochi_req = MochiReq, 229 begin_ts = Begin, 230 peer = Peer, 231 original_method = Method1, 232 nonce = Nonce, 233 method = Method, 234 path_parts = [list_to_binary(unquote(Part)) 235 || Part <- string:tokens(Path, "/")], 236 requested_path_parts = [?l2b(unquote(Part)) 237 || Part <- string:tokens(RequestedPath, "/")] 238 }, 239 240 % put small token on heap to keep requests synced to backend calls 241 erlang:put(nonce, Nonce), 242 243 % suppress duplicate log 244 erlang:put(dont_log_request, true), 245 erlang:put(dont_log_response, true), 246 247 {HttpReq2, Response} = case before_request(HttpReq0) of 248 {ok, HttpReq1} -> 249 process_request(HttpReq1); 250 {error, Response0} -> 251 {HttpReq0, Response0} 252 end, 253 254 {Status, Code, Reason, Resp} = split_response(Response), 255 256 HttpResp = #httpd_resp{ 257 code = Code, 258 status = Status, 259 response = Resp, 260 nonce = HttpReq2#httpd.nonce, 261 reason = Reason 262 }, 263 264 case after_request(HttpReq2, HttpResp) of 265 #httpd_resp{status = ok, response = Resp} -> 266 {ok, Resp}; 267 #httpd_resp{status = aborted, reason = Reason} -> 268 couch_log:error("Response abnormally terminated: ~p", [Reason]), 269 exit(normal) 270 end. 271 272before_request(HttpReq) -> 273 try 274 chttpd_stats:init(), 275 chttpd_plugin:before_request(HttpReq) 276 catch ?STACKTRACE(ErrorType, Error, Stack) 277 {error, catch_error(HttpReq, ErrorType, Error, Stack)} 278 end. 279 280after_request(HttpReq, HttpResp0) -> 281 {ok, HttpResp1} = 282 try 283 chttpd_plugin:after_request(HttpReq, HttpResp0) 284 catch ?STACKTRACE(_ErrorType, Error, Stack) 285 send_error(HttpReq, {Error, nil, Stack}), 286 {ok, HttpResp0#httpd_resp{status = aborted}} 287 end, 288 HttpResp2 = update_stats(HttpReq, HttpResp1), 289 chttpd_stats:report(HttpReq, HttpResp2), 290 maybe_log(HttpReq, HttpResp2), 291 HttpResp2. 292 293process_request(#httpd{mochi_req = MochiReq} = HttpReq) -> 294 HandlerKey = 295 case HttpReq#httpd.path_parts of 296 [] -> <<>>; 297 [Key|_] -> ?l2b(quote(Key)) 298 end, 299 300 RawUri = MochiReq:get(raw_path), 301 302 try 303 couch_httpd:validate_host(HttpReq), 304 check_request_uri_length(RawUri), 305 check_url_encoding(RawUri), 306 case chttpd_cors:maybe_handle_preflight_request(HttpReq) of 307 not_preflight -> 308 case chttpd_auth:authenticate(HttpReq, fun authenticate_request/1) of 309 #httpd{} = Req -> 310 handle_req_after_auth(HandlerKey, Req); 311 Response -> 312 {HttpReq, Response} 313 end; 314 Response -> 315 {HttpReq, Response} 316 end 317 catch ?STACKTRACE(ErrorType, Error, Stack) 318 {HttpReq, catch_error(HttpReq, ErrorType, Error, Stack)} 319 end. 320 321handle_req_after_auth(HandlerKey, HttpReq) -> 322 try 323 HandlerFun = chttpd_handlers:url_handler(HandlerKey, 324 fun chttpd_db:handle_request/1), 325 AuthorizedReq = chttpd_auth:authorize(possibly_hack(HttpReq), 326 fun chttpd_auth_request:authorize_request/1), 327 {AuthorizedReq, HandlerFun(AuthorizedReq)} 328 catch ?STACKTRACE(ErrorType, Error, Stack) 329 {HttpReq, catch_error(HttpReq, ErrorType, Error, Stack)} 330 end. 331 332catch_error(_HttpReq, throw, {http_head_abort, Resp}, _Stack) -> 333 {ok, Resp}; 334catch_error(_HttpReq, throw, {http_abort, Resp, Reason}, _Stack) -> 335 {aborted, Resp, Reason}; 336catch_error(HttpReq, throw, {invalid_json, _}, _Stack) -> 337 send_error(HttpReq, {bad_request, "invalid UTF-8 JSON"}); 338catch_error(HttpReq, exit, {mochiweb_recv_error, E}, _Stack) -> 339 #httpd{ 340 mochi_req = MochiReq, 341 peer = Peer, 342 original_method = Method 343 } = HttpReq, 344 couch_log:notice("mochiweb_recv_error for ~s - ~p ~s - ~p", [ 345 Peer, 346 Method, 347 MochiReq:get(raw_path), 348 E]), 349 exit(normal); 350catch_error(HttpReq, exit, {uri_too_long, _}, _Stack) -> 351 send_error(HttpReq, request_uri_too_long); 352catch_error(HttpReq, exit, {body_too_large, _}, _Stack) -> 353 send_error(HttpReq, request_entity_too_large); 354catch_error(HttpReq, throw, Error, _Stack) -> 355 send_error(HttpReq, Error); 356catch_error(HttpReq, error, database_does_not_exist, _Stack) -> 357 send_error(HttpReq, database_does_not_exist); 358catch_error(HttpReq, Tag, Error, Stack) -> 359 % TODO improve logging and metrics collection for client disconnects 360 case {Tag, Error, Stack} of 361 {exit, normal, [{mochiweb_request, send, _, _} | _]} -> 362 exit(normal); % Client disconnect (R15+) 363 _Else -> 364 send_error(HttpReq, {Error, nil, Stack}) 365 end. 366 367split_response({ok, #delayed_resp{resp=Resp}}) -> 368 {ok, Resp:get(code), undefined, Resp}; 369split_response({ok, Resp}) -> 370 {ok, Resp:get(code), undefined, Resp}; 371split_response({aborted, Resp, AbortReason}) -> 372 {aborted, Resp:get(code), AbortReason, Resp}. 373 374update_stats(HttpReq, #httpd_resp{end_ts = undefined} = Res) -> 375 update_stats(HttpReq, Res#httpd_resp{end_ts = os:timestamp()}); 376update_stats(#httpd{begin_ts = BeginTime}, #httpd_resp{} = Res) -> 377 #httpd_resp{status = Status, end_ts = EndTime} = Res, 378 RequestTime = timer:now_diff(EndTime, BeginTime) / 1000, 379 couch_stats:update_histogram([couchdb, request_time], RequestTime), 380 case Status of 381 ok -> 382 couch_stats:increment_counter([couchdb, httpd, requests]); 383 aborted -> 384 couch_stats:increment_counter([couchdb, httpd, aborted_requests]) 385 end, 386 Res. 387 388maybe_log(#httpd{} = HttpReq, #httpd_resp{should_log = true} = HttpResp) -> 389 #httpd{ 390 mochi_req = MochiReq, 391 begin_ts = BeginTime, 392 original_method = Method, 393 peer = Peer 394 } = HttpReq, 395 #httpd_resp{ 396 end_ts = EndTime, 397 code = Code, 398 status = Status 399 } = HttpResp, 400 User = get_user(HttpReq), 401 Host = MochiReq:get_header_value("Host"), 402 RawUri = MochiReq:get(raw_path), 403 RequestTime = timer:now_diff(EndTime, BeginTime) / 1000, 404 couch_log:notice("~s ~s ~s ~s ~s ~B ~p ~B", [Host, Peer, User, 405 Method, RawUri, Code, Status, round(RequestTime)]); 406maybe_log(_HttpReq, #httpd_resp{should_log = false}) -> 407 ok. 408 409 410%% HACK: replication currently handles two forms of input, #db{} style 411%% and #http_db style. We need a third that makes use of fabric. #db{} 412%% works fine for replicating the dbs and nodes database because they 413%% aren't sharded. So for now when a local db is specified as the source or 414%% the target, it's hacked to make it a full url and treated as a remote. 415possibly_hack(#httpd{path_parts=[<<"_replicate">>]}=Req) -> 416 {Props0} = chttpd:json_body_obj(Req), 417 Props1 = fix_uri(Req, Props0, <<"source">>), 418 Props2 = fix_uri(Req, Props1, <<"target">>), 419 Req#httpd{req_body={Props2}}; 420possibly_hack(Req) -> 421 Req. 422 423check_request_uri_length(Uri) -> 424 check_request_uri_length(Uri, 425 chttpd_util:get_chttpd_config("max_uri_length")). 426 427check_request_uri_length(_Uri, undefined) -> 428 ok; 429check_request_uri_length(Uri, MaxUriLen) when is_list(MaxUriLen) -> 430 case length(Uri) > list_to_integer(MaxUriLen) of 431 true -> 432 throw(request_uri_too_long); 433 false -> 434 ok 435 end. 436 437check_url_encoding([]) -> 438 ok; 439check_url_encoding([$%, A, B | Rest]) when ?is_hex(A), ?is_hex(B) -> 440 check_url_encoding(Rest); 441check_url_encoding([$% | _]) -> 442 throw({bad_request, invalid_url_encoding}); 443check_url_encoding([_ | Rest]) -> 444 check_url_encoding(Rest). 445 446fix_uri(Req, Props, Type) -> 447 case replication_uri(Type, Props) of 448 undefined -> 449 Props; 450 Uri0 -> 451 case is_http(Uri0) of 452 true -> 453 Props; 454 false -> 455 Uri = make_uri(Req, quote(Uri0)), 456 [{Type,Uri}|proplists:delete(Type,Props)] 457 end 458 end. 459 460replication_uri(Type, PostProps) -> 461 case couch_util:get_value(Type, PostProps) of 462 {Props} -> 463 couch_util:get_value(<<"url">>, Props); 464 Else -> 465 Else 466 end. 467 468is_http(<<"http://", _/binary>>) -> 469 true; 470is_http(<<"https://", _/binary>>) -> 471 true; 472is_http(_) -> 473 false. 474 475make_uri(Req, Raw) -> 476 Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)), 477 Url = list_to_binary(["http://", config:get("httpd", "bind_address"), 478 ":", Port, "/", Raw]), 479 Headers = [ 480 {<<"authorization">>, ?l2b(header_value(Req,"authorization",""))}, 481 {<<"cookie">>, ?l2b(extract_cookie(Req))} 482 ], 483 {[{<<"url">>,Url}, {<<"headers">>,{Headers}}]}. 484 485extract_cookie(#httpd{mochi_req = MochiReq}) -> 486 case MochiReq:get_cookie_value("AuthSession") of 487 undefined -> 488 ""; 489 AuthSession -> 490 "AuthSession=" ++ AuthSession 491 end. 492%%% end hack 493 494set_auth_handlers() -> 495 AuthenticationDefault = "{chttpd_auth, cookie_authentication_handler}, 496 {chttpd_auth, default_authentication_handler}", 497 AuthenticationSrcs = couch_httpd:make_fun_spec_strs( 498 config:get("chttpd", "authentication_handlers", AuthenticationDefault)), 499 AuthHandlers = lists:map( 500 fun(A) -> {auth_handler_name(A), couch_httpd:make_arity_1_fun(A)} end, AuthenticationSrcs), 501 AuthenticationFuns = AuthHandlers ++ [ 502 fun chttpd_auth:party_mode_handler/1 %% must be last 503 ], 504 ok = application:set_env(chttpd, auth_handlers, AuthenticationFuns). 505 506% SpecStr is a string like "{my_module, my_fun}" 507% Takes the first token of the function name in front '_' as auth handler name 508% e.g. 509% chttpd_auth:default_authentication_handler: default 510% chttpd_auth_cookie_authentication_handler: cookie 511% couch_http_auth:proxy_authentication_handler: proxy 512% 513% couch_http:auth_handler_name can't be used here, since it assumes the name 514% of the auth handler to be the 6th token split by [\\W_] 515% - this only works for modules with exactly two underscores in their name 516% - is not very robust (a space after the ',' is assumed) 517auth_handler_name(SpecStr) -> 518 {ok, {_, Fun}} = couch_util:parse_term(SpecStr), 519 hd(binary:split(atom_to_binary(Fun, latin1), <<"_">>)). 520 521authenticate_request(Req) -> 522 {ok, AuthenticationFuns} = application:get_env(chttpd, auth_handlers), 523 authenticate_request(Req, chttpd_auth_cache, AuthenticationFuns). 524 525authenticate_request(#httpd{} = Req0, AuthModule, AuthFuns) -> 526 Req = Req0#httpd{ 527 auth_module = AuthModule, 528 authentication_handlers = AuthFuns}, 529 authenticate_request(Req, AuthFuns). 530 531% Try authentication handlers in order until one returns a result 532authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) -> 533 Req; 534authenticate_request(#httpd{} = Req, [{Name, AuthFun}|Rest]) -> 535 authenticate_request(maybe_set_handler(AuthFun(Req), Name), Rest); 536authenticate_request(#httpd{} = Req, [AuthFun|Rest]) -> 537 authenticate_request(AuthFun(Req), Rest); 538authenticate_request(Response, _AuthFuns) -> 539 Response. 540 541maybe_set_handler(#httpd{user_ctx=#user_ctx{} = UserCtx} = Req, Name) -> 542 Req#httpd{user_ctx = UserCtx#user_ctx{handler = Name}}; 543maybe_set_handler(Else, _) -> 544 Else. 545 546increment_method_stats(Method) -> 547 couch_stats:increment_counter([couchdb, httpd_request_methods, Method]). 548 549% Utilities 550 551partition(Path) -> 552 mochiweb_util:partition(Path, "/"). 553 554header_value(#httpd{mochi_req=MochiReq}, Key) -> 555 MochiReq:get_header_value(Key). 556 557header_value(#httpd{mochi_req=MochiReq}, Key, Default) -> 558 case MochiReq:get_header_value(Key) of 559 undefined -> Default; 560 Value -> Value 561 end. 562 563primary_header_value(#httpd{mochi_req=MochiReq}, Key) -> 564 MochiReq:get_primary_header_value(Key). 565 566serve_file(Req, RelativePath, DocumentRoot) -> 567 serve_file(Req, RelativePath, DocumentRoot, []). 568 569serve_file(Req0, RelativePath0, DocumentRoot0, ExtraHeaders) -> 570 couch_httpd:serve_file(Req0, RelativePath0, DocumentRoot0, ExtraHeaders). 571 572qs_value(Req, Key) -> 573 qs_value(Req, Key, undefined). 574 575qs_value(Req, Key, Default) -> 576 couch_util:get_value(Key, qs(Req), Default). 577 578qs_json_value(Req, Key, Default) -> 579 case qs_value(Req, Key, Default) of 580 Default -> 581 Default; 582 Result -> 583 ?JSON_DECODE(Result) 584 end. 585 586qs(#httpd{mochi_req = MochiReq, qs = undefined}) -> 587 MochiReq:parse_qs(); 588qs(#httpd{qs = QS}) -> 589 QS. 590 591path(#httpd{mochi_req=MochiReq}) -> 592 MochiReq:get(path). 593 594absolute_uri(#httpd{mochi_req=MochiReq, absolute_uri = undefined}, Path) -> 595 XHost = chttpd_util:get_chttpd_config( 596 "x_forwarded_host", "X-Forwarded-Host"), 597 Host = case MochiReq:get_header_value(XHost) of 598 undefined -> 599 case MochiReq:get_header_value("Host") of 600 undefined -> 601 {ok, {Address, Port}} = case MochiReq:get(socket) of 602 {ssl, SslSocket} -> ssl:sockname(SslSocket); 603 Socket -> inet:sockname(Socket) 604 end, 605 inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port); 606 Value1 -> 607 Value1 608 end; 609 Value -> Value 610 end, 611 XSsl = chttpd_util:get_chttpd_config("x_forwarded_ssl", "X-Forwarded-Ssl"), 612 Scheme = case MochiReq:get_header_value(XSsl) of 613 "on" -> "https"; 614 _ -> 615 XProto = chttpd_util:get_chttpd_config( 616 "x_forwarded_proto", "X-Forwarded-Proto"), 617 case MochiReq:get_header_value(XProto) of 618 % Restrict to "https" and "http" schemes only 619 "https" -> "https"; 620 _ -> 621 case MochiReq:get(scheme) of 622 https -> 623 "https"; 624 http -> 625 "http" 626 end 627 end 628 end, 629 Scheme ++ "://" ++ Host ++ Path; 630absolute_uri(#httpd{absolute_uri = URI}, Path) -> 631 URI ++ Path. 632 633unquote(UrlEncodedString) -> 634 case config:get_boolean("chttpd", "decode_plus_to_space", true) of 635 true -> mochiweb_util:unquote(UrlEncodedString); 636 false -> mochiweb_util:unquote_path(UrlEncodedString) 637 end. 638 639quote(UrlDecodedString) -> 640 mochiweb_util:quote_plus(UrlDecodedString). 641 642parse_form(#httpd{mochi_req=MochiReq}) -> 643 mochiweb_multipart:parse_form(MochiReq). 644 645recv(#httpd{mochi_req=MochiReq}, Len) -> 646 MochiReq:recv(Len). 647 648recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) -> 649 % Fun is called once with each chunk 650 % Fun({Length, Binary}, State) 651 % called with Length == 0 on the last time. 652 MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState). 653 654body_length(#httpd{mochi_req=MochiReq}) -> 655 MochiReq:get(body_length). 656 657body(#httpd{mochi_req=MochiReq, req_body=ReqBody}) -> 658 case ReqBody of 659 undefined -> 660 % Maximum size of document PUT request body (4GB) 661 MaxSize = chttpd_util:get_chttpd_config_integer( 662 "max_http_request_size", 4294967296), 663 Begin = os:timestamp(), 664 try 665 MochiReq:recv_body(MaxSize) 666 after 667 T = timer:now_diff(os:timestamp(), Begin) div 1000, 668 put(body_time, T) 669 end; 670 _Else -> 671 ReqBody 672 end. 673 674validate_ctype(Req, Ctype) -> 675 couch_httpd:validate_ctype(Req, Ctype). 676 677json_body(#httpd{req_body=undefined} = Httpd) -> 678 case body(Httpd) of 679 undefined -> 680 throw({bad_request, "Missing request body"}); 681 Body -> 682 ?JSON_DECODE(maybe_decompress(Httpd, Body)) 683 end; 684 685json_body(#httpd{req_body=ReqBody}) -> 686 ReqBody. 687 688json_body_obj(Httpd) -> 689 case json_body(Httpd) of 690 {Props} -> {Props}; 691 _Else -> 692 throw({bad_request, "Request body must be a JSON object"}) 693 end. 694 695 696doc_etag(#doc{id=Id, body=Body, revs={Start, [DiskRev|_]}}) -> 697 couch_httpd:doc_etag(Id, Body, {Start, DiskRev}). 698 699make_etag(Term) -> 700 <<SigInt:128/integer>> = couch_hash:md5_hash(term_to_binary(Term)), 701 list_to_binary(io_lib:format("\"~.36B\"",[SigInt])). 702 703etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) -> 704 etag_match(Req, binary_to_list(CurrentEtag)); 705 706etag_match(Req, CurrentEtag) -> 707 EtagsToMatch0 = string:tokens( 708 chttpd:header_value(Req, "If-None-Match", ""), ", "), 709 EtagsToMatch = lists:map(fun strip_weak_prefix/1, EtagsToMatch0), 710 lists:member(CurrentEtag, EtagsToMatch). 711 712strip_weak_prefix([$W, $/ | Etag]) -> 713 Etag; 714strip_weak_prefix(Etag) -> 715 Etag. 716 717etag_respond(Req, CurrentEtag, RespFun) -> 718 case etag_match(Req, CurrentEtag) of 719 true -> 720 % the client has this in their cache. 721 Headers = [{"ETag", CurrentEtag}], 722 chttpd:send_response(Req, 304, Headers, <<>>); 723 false -> 724 % Run the function. 725 RespFun() 726 end. 727 728verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) -> 729 case lists:member(<<"_admin">>, Roles) of 730 true -> ok; 731 false -> throw({unauthorized, <<"You are not a server admin.">>}) 732 end. 733 734start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers0, Length) -> 735 Headers1 = basic_headers(Req, Headers0), 736 Resp = handle_response(Req, Code, Headers1, Length, start_response_length), 737 case MochiReq:get(method) of 738 'HEAD' -> throw({http_head_abort, Resp}); 739 _ -> ok 740 end, 741 {ok, Resp}. 742 743send(Resp, Data) -> 744 Resp:send(Data), 745 {ok, Resp}. 746 747start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers0) -> 748 Headers1 = basic_headers(Req, Headers0), 749 Resp = handle_response(Req, Code, Headers1, chunked, respond), 750 case MochiReq:get(method) of 751 'HEAD' -> throw({http_head_abort, Resp}); 752 _ -> ok 753 end, 754 {ok, Resp}. 755 756send_chunk({remote, _Pid, _Ref} = Resp, Data) -> 757 couch_httpd:send_chunk(Resp, Data); 758send_chunk(Resp, Data) -> 759 Resp:write_chunk(Data), 760 {ok, Resp}. 761 762send_response(Req, Code, Headers0, Body) -> 763 Headers1 = [timing(), reqid() | Headers0], 764 couch_httpd:send_response(Req, Code, Headers1, Body). 765 766send_response_no_cors(Req, Code, Headers0, Body) -> 767 Headers1 = [timing(), reqid() | Headers0], 768 couch_httpd:send_response_no_cors(Req, Code, Headers1, Body). 769 770send_method_not_allowed(Req, Methods) -> 771 send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, 772 ?l2b("Only " ++ Methods ++ " allowed"), []). 773 774send_json(Req, Value) -> 775 send_json(Req, 200, Value). 776 777send_json(Req, Code, Value) -> 778 send_json(Req, Code, [], Value). 779 780send_json(Req, Code, Headers0, Value) -> 781 Headers1 = [timing(), reqid() | Headers0], 782 couch_httpd:send_json(Req, Code, Headers1, Value). 783 784start_json_response(Req, Code) -> 785 start_json_response(Req, Code, []). 786 787start_json_response(Req, Code, Headers0) -> 788 Headers1 = [timing(), reqid() | Headers0], 789 couch_httpd:start_json_response(Req, Code, Headers1). 790 791end_json_response(Resp) -> 792 couch_httpd:end_json_response(Resp). 793 794 795start_delayed_json_response(Req, Code) -> 796 start_delayed_json_response(Req, Code, []). 797 798 799start_delayed_json_response(Req, Code, Headers) -> 800 start_delayed_json_response(Req, Code, Headers, ""). 801 802 803start_delayed_json_response(Req, Code, Headers, FirstChunk) -> 804 {ok, #delayed_resp{ 805 start_fun = fun start_json_response/3, 806 req = Req, 807 code = Code, 808 headers = Headers, 809 chunks = [FirstChunk], 810 buffer_response = buffer_response(Req)}}. 811 812 813start_delayed_chunked_response(Req, Code, Headers) -> 814 start_delayed_chunked_response(Req, Code, Headers, ""). 815 816 817start_delayed_chunked_response(Req, Code, Headers, FirstChunk) -> 818 {ok, #delayed_resp{ 819 start_fun = fun start_chunked_response/3, 820 req = Req, 821 code = Code, 822 headers = Headers, 823 chunks = [FirstChunk], 824 buffer_response = buffer_response(Req)}}. 825 826 827send_delayed_chunk(#delayed_resp{buffer_response=false}=DelayedResp, Chunk) -> 828 {ok, #delayed_resp{resp=Resp}=DelayedResp1} = 829 start_delayed_response(DelayedResp), 830 {ok, Resp} = send_chunk(Resp, Chunk), 831 {ok, DelayedResp1}; 832 833send_delayed_chunk(#delayed_resp{buffer_response=true}=DelayedResp, Chunk) -> 834 #delayed_resp{chunks = Chunks} = DelayedResp, 835 {ok, DelayedResp#delayed_resp{chunks = [Chunk | Chunks]}}. 836 837 838send_delayed_last_chunk(Req) -> 839 send_delayed_chunk(Req, []). 840 841 842send_delayed_error(#delayed_resp{req=Req,resp=nil}=DelayedResp, Reason) -> 843 {Code, ErrorStr, ReasonStr} = error_info(Reason), 844 {ok, Resp} = send_error(Req, Code, ErrorStr, ReasonStr), 845 {ok, DelayedResp#delayed_resp{resp=Resp}}; 846send_delayed_error(#delayed_resp{resp=Resp, req=Req}, Reason) -> 847 update_timeout_stats(Reason, Req), 848 log_error_with_stack_trace(Reason), 849 throw({http_abort, Resp, Reason}). 850 851 852close_delayed_json_object(Resp, Buffer, Terminator, 0) -> 853 % Use a separate chunk to close the streamed array to maintain strict 854 % compatibility with earlier versions. See COUCHDB-2724 855 {ok, R1} = chttpd:send_delayed_chunk(Resp, Buffer), 856 send_delayed_chunk(R1, Terminator); 857close_delayed_json_object(Resp, Buffer, Terminator, _Threshold) -> 858 send_delayed_chunk(Resp, [Buffer | Terminator]). 859 860 861end_delayed_json_response(#delayed_resp{buffer_response=false}=DelayedResp) -> 862 {ok, #delayed_resp{resp=Resp}} = 863 start_delayed_response(DelayedResp), 864 end_json_response(Resp); 865 866end_delayed_json_response(#delayed_resp{buffer_response=true}=DelayedResp) -> 867 #delayed_resp{ 868 start_fun = StartFun, 869 req = Req, 870 code = Code, 871 headers = Headers, 872 chunks = Chunks 873 } = DelayedResp, 874 {ok, Resp} = StartFun(Req, Code, Headers), 875 lists:foreach(fun 876 ([]) -> ok; 877 (Chunk) -> send_chunk(Resp, Chunk) 878 end, lists:reverse(Chunks)), 879 end_json_response(Resp). 880 881 882get_delayed_req(#delayed_resp{req=#httpd{mochi_req=MochiReq}}) -> 883 MochiReq; 884get_delayed_req(Resp) -> 885 Resp:get(request). 886 887start_delayed_response(#delayed_resp{resp=nil}=DelayedResp) -> 888 #delayed_resp{ 889 start_fun=StartFun, 890 req=Req, 891 code=Code, 892 headers=Headers, 893 chunks=[FirstChunk] 894 }=DelayedResp, 895 {ok, Resp} = StartFun(Req, Code, Headers), 896 case FirstChunk of 897 "" -> ok; 898 _ -> {ok, Resp} = send_chunk(Resp, FirstChunk) 899 end, 900 {ok, DelayedResp#delayed_resp{resp=Resp}}; 901start_delayed_response(#delayed_resp{}=DelayedResp) -> 902 {ok, DelayedResp}. 903 904 905buffer_response(Req) -> 906 case chttpd:qs_value(Req, "buffer_response") of 907 "false" -> 908 false; 909 "true" -> 910 true; 911 _ -> 912 config:get_boolean("chttpd", "buffer_response", false) 913 end. 914 915 916error_info({Error, Reason}) when is_list(Reason) -> 917 error_info({Error, couch_util:to_binary(Reason)}); 918error_info(bad_request) -> 919 {400, <<"bad_request">>, <<>>}; 920error_info({bad_request, Reason}) -> 921 {400, <<"bad_request">>, Reason}; 922error_info({bad_request, Error, Reason}) -> 923 {400, couch_util:to_binary(Error), couch_util:to_binary(Reason)}; 924error_info({query_parse_error, Reason}) -> 925 {400, <<"query_parse_error">>, Reason}; 926error_info(database_does_not_exist) -> 927 {404, <<"not_found">>, <<"Database does not exist.">>}; 928error_info(not_found) -> 929 {404, <<"not_found">>, <<"missing">>}; 930error_info({not_found, Reason}) -> 931 {404, <<"not_found">>, Reason}; 932error_info({filter_fetch_error, Reason}) -> 933 {404, <<"not_found">>, Reason}; 934error_info(ddoc_updated) -> 935 {404, <<"not_found">>, <<"Design document was updated or deleted.">>}; 936error_info({not_acceptable, Reason}) -> 937 {406, <<"not_acceptable">>, Reason}; 938error_info(conflict) -> 939 {409, <<"conflict">>, <<"Document update conflict.">>}; 940error_info({conflict, _}) -> 941 {409, <<"conflict">>, <<"Document update conflict.">>}; 942error_info({partition_overflow, DocId}) -> 943 Descr = << 944 "Partition limit exceeded due to update on '", DocId/binary, "'" 945 >>, 946 {403, <<"partition_overflow">>, Descr}; 947error_info({{not_found, missing}, {_, _}}) -> 948 {409, <<"not_found">>, <<"missing_rev">>}; 949error_info({forbidden, Error, Msg}) -> 950 {403, Error, Msg}; 951error_info({forbidden, Msg}) -> 952 {403, <<"forbidden">>, Msg}; 953error_info({unauthorized, Msg}) -> 954 {401, <<"unauthorized">>, Msg}; 955error_info(file_exists) -> 956 {412, <<"file_exists">>, <<"The database could not be " 957 "created, the file already exists.">>}; 958error_info({error, {nodedown, Reason}}) -> 959 {412, <<"nodedown">>, Reason}; 960error_info({maintenance_mode, Node}) -> 961 {412, <<"nodedown">>, Node}; 962error_info({maintenance_mode, nil, Node}) -> 963 {412, <<"nodedown">>, Node}; 964error_info({w_quorum_not_met, Reason}) -> 965 {500, <<"write_quorum_not_met">>, Reason}; 966error_info(request_uri_too_long) -> 967 {414, <<"too_long">>, <<"the request uri is too long">>}; 968error_info({bad_ctype, Reason}) -> 969 {415, <<"bad_content_type">>, Reason}; 970error_info(requested_range_not_satisfiable) -> 971 {416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>}; 972error_info({error, {illegal_database_name, Name}}) -> 973 Message = <<"Name: '", Name/binary, "'. Only lowercase characters (a-z), ", 974 "digits (0-9), and any of the characters _, $, (, ), +, -, and / ", 975 "are allowed. Must begin with a letter.">>, 976 {400, <<"illegal_database_name">>, Message}; 977error_info({illegal_docid, Reason}) -> 978 {400, <<"illegal_docid">>, Reason}; 979error_info({illegal_partition, Reason}) -> 980 {400, <<"illegal_partition">>, Reason}; 981error_info({_DocID,{illegal_docid,DocID}}) -> 982 {400, <<"illegal_docid">>,DocID}; 983error_info({error, {database_name_too_long, DbName}}) -> 984 {400, <<"database_name_too_long">>, 985 <<"At least one path segment of `", DbName/binary, "` is too long.">>}; 986error_info({doc_validation, Reason}) -> 987 {400, <<"doc_validation">>, Reason}; 988error_info({missing_stub, Reason}) -> 989 {412, <<"missing_stub">>, Reason}; 990error_info(request_entity_too_large) -> 991 {413, <<"too_large">>, <<"the request entity is too large">>}; 992error_info({request_entity_too_large, {attachment, AttName}}) -> 993 {413, <<"attachment_too_large">>, AttName}; 994error_info({request_entity_too_large, DocID}) -> 995 {413, <<"document_too_large">>, DocID}; 996error_info({error, security_migration_updates_disabled}) -> 997 {503, <<"security_migration">>, <<"Updates to security docs are disabled during " 998 "security migration.">>}; 999error_info(all_workers_died) -> 1000 {503, <<"service unvailable">>, <<"Nodes are unable to service this " 1001 "request due to overloading or maintenance mode.">>}; 1002error_info(not_implemented) -> 1003 {501, <<"not_implemented">>, <<"this feature is not yet implemented">>}; 1004error_info(timeout) -> 1005 {500, <<"timeout">>, <<"The request could not be processed in a reasonable" 1006 " amount of time.">>}; 1007error_info({service_unavailable, Reason}) -> 1008 {503, <<"service unavailable">>, Reason}; 1009error_info({timeout, _Reason}) -> 1010 error_info(timeout); 1011error_info({'EXIT', {Error, _Stack}}) -> 1012 error_info(Error); 1013error_info({Error, null}) -> 1014 error_info(Error); 1015error_info({_Error, _Reason} = Error) -> 1016 maybe_handle_error(Error); 1017error_info({Error, nil, _Stack}) -> 1018 error_info(Error); 1019error_info({Error, Reason, _Stack}) -> 1020 error_info({Error, Reason}); 1021error_info(Error) -> 1022 maybe_handle_error(Error). 1023 1024maybe_handle_error(Error) -> 1025 case chttpd_plugin:handle_error(Error) of 1026 {_Code, _Reason, _Description} = Result -> 1027 Result; 1028 {Err, Reason} -> 1029 {500, couch_util:to_binary(Err), couch_util:to_binary(Reason)}; 1030 normal -> 1031 exit(normal); 1032 Error -> 1033 {500, <<"unknown_error">>, couch_util:to_binary(Error)} 1034 end. 1035 1036 1037error_headers(#httpd{mochi_req=MochiReq}=Req, 401=Code, ErrorStr, ReasonStr) -> 1038 % this is where the basic auth popup is triggered 1039 case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of 1040 undefined -> 1041 case chttpd_util:get_chttpd_config("WWW-Authenticate") of 1042 undefined -> 1043 % If the client is a browser and the basic auth popup isn't turned on 1044 % redirect to the session page. 1045 case ErrorStr of 1046 <<"unauthorized">> -> 1047 case chttpd_util:get_chttpd_auth_config( 1048 "authentication_redirect", "/_utils/session.html") of 1049 undefined -> {Code, []}; 1050 AuthRedirect -> 1051 case chttpd_util:get_chttpd_auth_config_boolean( 1052 "require_valid_user", false) of 1053 true -> 1054 % send the browser popup header no matter what if we are require_valid_user 1055 {Code, [{"WWW-Authenticate", "Basic realm=\"server\""}]}; 1056 false -> 1057 case MochiReq:accepts_content_type("application/json") of 1058 true -> 1059 {Code, []}; 1060 false -> 1061 case MochiReq:accepts_content_type("text/html") of 1062 true -> 1063 % Redirect to the path the user requested, not 1064 % the one that is used internally. 1065 UrlReturnRaw = case MochiReq:get_header_value("x-couchdb-vhost-path") of 1066 undefined -> 1067 MochiReq:get(path); 1068 VHostPath -> 1069 VHostPath 1070 end, 1071 RedirectLocation = lists:flatten([ 1072 AuthRedirect, 1073 "?return=", couch_util:url_encode(UrlReturnRaw), 1074 "&reason=", couch_util:url_encode(ReasonStr) 1075 ]), 1076 {302, [{"Location", absolute_uri(Req, RedirectLocation)}]}; 1077 false -> 1078 {Code, []} 1079 end 1080 end 1081 end 1082 end; 1083 _Else -> 1084 {Code, []} 1085 end; 1086 Type -> 1087 {Code, [{"WWW-Authenticate", Type}]} 1088 end; 1089 Type -> 1090 {Code, [{"WWW-Authenticate", Type}]} 1091 end; 1092error_headers(_, Code, _, _) -> 1093 {Code, []}. 1094 1095send_error(#httpd{} = Req, Error) -> 1096 update_timeout_stats(Error, Req), 1097 1098 {Code, ErrorStr, ReasonStr} = error_info(Error), 1099 {Code1, Headers} = error_headers(Req, Code, ErrorStr, ReasonStr), 1100 send_error(Req, Code1, Headers, ErrorStr, ReasonStr, json_stack(Error)). 1101 1102send_error(#httpd{} = Req, Code, ErrorStr, ReasonStr) -> 1103 update_timeout_stats(ErrorStr, Req), 1104 send_error(Req, Code, [], ErrorStr, ReasonStr, []). 1105 1106send_error(Req, Code, Headers, ErrorStr, ReasonStr, []) -> 1107 send_json(Req, Code, Headers, 1108 {[{<<"error">>, ErrorStr}, 1109 {<<"reason">>, ReasonStr}]}); 1110send_error(Req, Code, Headers, ErrorStr, ReasonStr, Stack) -> 1111 log_error_with_stack_trace({ErrorStr, ReasonStr, Stack}), 1112 send_json(Req, Code, [stack_trace_id(Stack) | Headers], 1113 {[{<<"error">>, ErrorStr}, 1114 {<<"reason">>, ReasonStr} | 1115 case Stack of [] -> []; _ -> [{<<"ref">>, stack_hash(Stack)}] end 1116 ]}). 1117 1118update_timeout_stats(<<"timeout">>, #httpd{requested_path_parts = PathParts}) -> 1119 update_timeout_stats(PathParts); 1120update_timeout_stats(timeout, #httpd{requested_path_parts = PathParts}) -> 1121 update_timeout_stats(PathParts); 1122update_timeout_stats(_, _) -> 1123 ok. 1124 1125update_timeout_stats([_, <<"_partition">>, _, <<"_design">>, _, 1126 <<"_view">> | _]) -> 1127 couch_stats:increment_counter([couchdb, httpd, partition_view_timeouts]); 1128update_timeout_stats([_, <<"_partition">>, _, <<"_find">>| _]) -> 1129 couch_stats:increment_counter([couchdb, httpd, partition_find_timeouts]); 1130update_timeout_stats([_, <<"_partition">>, _, <<"_explain">>| _]) -> 1131 couch_stats:increment_counter([couchdb, httpd, partition_explain_timeouts]); 1132update_timeout_stats([_, <<"_partition">>, _, <<"_all_docs">> | _]) -> 1133 couch_stats:increment_counter([couchdb, httpd, partition_all_docs_timeouts]); 1134update_timeout_stats([_, <<"_design">>, _, <<"_view">> | _]) -> 1135 couch_stats:increment_counter([couchdb, httpd, view_timeouts]); 1136update_timeout_stats([_, <<"_find">>| _]) -> 1137 couch_stats:increment_counter([couchdb, httpd, find_timeouts]); 1138update_timeout_stats([_, <<"_explain">>| _]) -> 1139 couch_stats:increment_counter([couchdb, httpd, explain_timeouts]); 1140update_timeout_stats([_, <<"_all_docs">> | _]) -> 1141 couch_stats:increment_counter([couchdb, httpd, all_docs_timeouts]); 1142update_timeout_stats(_) -> 1143 ok. 1144 1145% give the option for list functions to output html or other raw errors 1146send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) -> 1147 send_chunk(Resp, Reason), 1148 send_chunk(Resp, []); 1149 1150send_chunked_error(Resp, Error) -> 1151 Stack = json_stack(Error), 1152 log_error_with_stack_trace(Error), 1153 {Code, ErrorStr, ReasonStr} = error_info(Error), 1154 JsonError = {[{<<"code">>, Code}, 1155 {<<"error">>, ErrorStr}, 1156 {<<"reason">>, ReasonStr} | 1157 case Stack of [] -> []; _ -> [{<<"ref">>, stack_hash(Stack)}] end 1158 ]}, 1159 send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])), 1160 send_chunk(Resp, []). 1161 1162send_redirect(Req, Path) -> 1163 Headers = [{"Location", chttpd:absolute_uri(Req, Path)}], 1164 send_response(Req, 301, Headers, <<>>). 1165 1166server_header() -> 1167 couch_httpd:server_header(). 1168 1169timing() -> 1170 case get(body_time) of 1171 undefined -> 1172 {"X-CouchDB-Body-Time", "0"}; 1173 Time -> 1174 {"X-CouchDB-Body-Time", integer_to_list(Time)} 1175 end. 1176 1177reqid() -> 1178 {"X-Couch-Request-ID", get(nonce)}. 1179 1180json_stack({bad_request, _, _}) -> 1181 []; 1182json_stack({_Error, _Reason, Stack}) when is_list(Stack) -> 1183 lists:map(fun json_stack_item/1, Stack); 1184json_stack(_) -> 1185 []. 1186 1187json_stack_item({M,F,A}) -> 1188 list_to_binary(io_lib:format("~s:~s/~B", [M, F, json_stack_arity(A)])); 1189json_stack_item({M,F,A,L}) -> 1190 case proplists:get_value(line, L) of 1191 undefined -> json_stack_item({M,F,A}); 1192 Line -> list_to_binary(io_lib:format("~s:~s/~B L~B", 1193 [M, F, json_stack_arity(A), Line])) 1194 end; 1195json_stack_item(_) -> 1196 <<"bad entry in stacktrace">>. 1197 1198json_stack_arity(A) -> 1199 if is_integer(A) -> A; is_list(A) -> length(A); true -> 0 end. 1200 1201maybe_decompress(Httpd, Body) -> 1202 case header_value(Httpd, "Content-Encoding", "identity") of 1203 "gzip" -> 1204 try 1205 zlib:gunzip(Body) 1206 catch error:data_error -> 1207 throw({bad_request, "Request body is not properly gzipped."}) 1208 end; 1209 "identity" -> 1210 Body; 1211 Else -> 1212 throw({bad_ctype, [Else, " is not a supported content encoding."]}) 1213 end. 1214 1215log_error_with_stack_trace({bad_request, _, _}) -> 1216 ok; 1217log_error_with_stack_trace({Error, Reason, Stack}) -> 1218 EFmt = if is_binary(Error) -> "~s"; true -> "~w" end, 1219 RFmt = if is_binary(Reason) -> "~s"; true -> "~w" end, 1220 Fmt = "req_err(~w) " ++ EFmt ++ " : " ++ RFmt ++ "~n ~p", 1221 couch_log:error(Fmt, [stack_hash(Stack), Error, Reason, Stack]); 1222log_error_with_stack_trace(_) -> 1223 ok. 1224 1225stack_trace_id(Stack) -> 1226 {"X-Couch-Stack-Hash", stack_hash(Stack)}. 1227 1228stack_hash(Stack) -> 1229 erlang:crc32(term_to_binary(Stack)). 1230 1231%% @doc CouchDB uses a chunked transfer-encoding to stream responses to 1232%% _all_docs, _changes, _view and other similar requests. This configuration 1233%% value sets the maximum size of a chunk; the system will buffer rows in the 1234%% response until it reaches this threshold and then send all the rows in one 1235%% chunk to improve network efficiency. The default value is chosen so that 1236%% the assembled chunk fits into the default Ethernet frame size (some reserved 1237%% padding is necessary to accommodate the reporting of the chunk length). Set 1238%% this value to 0 to restore the older behavior of sending each row in a 1239%% dedicated chunk. 1240chunked_response_buffer_size() -> 1241 chttpd_util:get_chttpd_config_integer("chunked_response_buffer", 1490). 1242 1243basic_headers(Req, Headers0) -> 1244 Headers = Headers0 1245 ++ server_header() 1246 ++ couch_httpd_auth:cookie_auth_header(Req, Headers0), 1247 Headers1 = chttpd_cors:headers(Req, Headers), 1248 Headers2 = chttpd_xframe_options:header(Req, Headers1), 1249 Headers3 = [reqid(), timing() | Headers2], 1250 chttpd_prefer_header:maybe_return_minimal(Req, Headers3). 1251 1252handle_response(Req0, Code0, Headers0, Args0, Type) -> 1253 {ok, {Req1, Code1, Headers1, Args1}} = 1254 chttpd_plugin:before_response(Req0, Code0, Headers0, Args0), 1255 couch_stats:increment_counter([couchdb, httpd_status_codes, Code1]), 1256 respond_(Req1, Code1, Headers1, Args1, Type). 1257 1258respond_(#httpd{mochi_req = MochiReq}, Code, Headers, _Args, start_response) -> 1259 MochiReq:start_response({Code, Headers}); 1260respond_(#httpd{mochi_req = MochiReq}, Code, Headers, Args, Type) -> 1261 MochiReq:Type({Code, Headers, Args}). 1262 1263get_user(#httpd{user_ctx = #user_ctx{name = null}}) -> 1264 % admin party 1265 "undefined"; 1266get_user(#httpd{user_ctx = #user_ctx{name = User}}) -> 1267 couch_util:url_encode(User); 1268get_user(#httpd{user_ctx = undefined}) -> 1269 "undefined". 1270 1271-ifdef(TEST). 1272 1273-include_lib("eunit/include/eunit.hrl"). 1274 1275check_url_encoding_pass_test_() -> 1276 [ 1277 ?_assertEqual(ok, check_url_encoding("/dbname")), 1278 ?_assertEqual(ok, check_url_encoding("/dbname/doc_id")), 1279 ?_assertEqual(ok, check_url_encoding("/dbname/doc_id?rev=1-abcdefgh")), 1280 ?_assertEqual(ok, check_url_encoding("/dbname%25")), 1281 ?_assertEqual(ok, check_url_encoding("/dbname/doc_id%25")), 1282 ?_assertEqual(ok, check_url_encoding("/dbname%25%3a")), 1283 ?_assertEqual(ok, check_url_encoding("/dbname/doc_id%25%3a")), 1284 ?_assertEqual(ok, check_url_encoding("/user%2Fdbname")), 1285 ?_assertEqual(ok, check_url_encoding("/user%2Fdbname/doc_id")), 1286 ?_assertEqual(ok, check_url_encoding("/dbname/escaped%25doc_id")), 1287 ?_assertEqual(ok, check_url_encoding("/dbname/doc%2eid")), 1288 ?_assertEqual(ok, check_url_encoding("/dbname/doc%2Eid")), 1289 ?_assertEqual(ok, check_url_encoding("/dbname-with-dash")), 1290 ?_assertEqual(ok, check_url_encoding("/dbname/doc_id-with-dash")) 1291 ]. 1292 1293check_url_encoding_fail_test_() -> 1294 [ 1295 ?_assertThrow({bad_request, invalid_url_encoding}, 1296 check_url_encoding("/dbname%")), 1297 ?_assertThrow({bad_request, invalid_url_encoding}, 1298 check_url_encoding("/dbname/doc_id%")), 1299 ?_assertThrow({bad_request, invalid_url_encoding}, 1300 check_url_encoding("/dbname/doc_id%?rev=1-abcdefgh")), 1301 ?_assertThrow({bad_request, invalid_url_encoding}, 1302 check_url_encoding("/dbname%2")), 1303 ?_assertThrow({bad_request, invalid_url_encoding}, 1304 check_url_encoding("/dbname/doc_id%2")), 1305 ?_assertThrow({bad_request, invalid_url_encoding}, 1306 check_url_encoding("/user%2Fdbname%")), 1307 ?_assertThrow({bad_request, invalid_url_encoding}, 1308 check_url_encoding("/user%2Fdbname/doc_id%")), 1309 ?_assertThrow({bad_request, invalid_url_encoding}, 1310 check_url_encoding("%")), 1311 ?_assertThrow({bad_request, invalid_url_encoding}, 1312 check_url_encoding("/%")), 1313 ?_assertThrow({bad_request, invalid_url_encoding}, 1314 check_url_encoding("/%2")), 1315 ?_assertThrow({bad_request, invalid_url_encoding}, 1316 check_url_encoding("/dbname%2%3A")), 1317 ?_assertThrow({bad_request, invalid_url_encoding}, 1318 check_url_encoding("/dbname%%3Ae")), 1319 ?_assertThrow({bad_request, invalid_url_encoding}, 1320 check_url_encoding("/dbname%2g")), 1321 ?_assertThrow({bad_request, invalid_url_encoding}, 1322 check_url_encoding("/dbname%g2")) 1323 ]. 1324 1325log_format_test() -> 1326 ?assertEqual( 1327 "127.0.0.1:15984 127.0.0.1 undefined " 1328 "GET /_cluster_setup 201 ok 10000", 1329 test_log_request("/_cluster_setup", undefined)), 1330 ?assertEqual( 1331 "127.0.0.1:15984 127.0.0.1 user_foo " 1332 "GET /_all_dbs 201 ok 10000", 1333 test_log_request("/_all_dbs", #user_ctx{name = <<"user_foo">>})), 1334 1335 %% Utf8Name = unicode:characters_to_binary(Something), 1336 Utf8User = <<227,130,136,227,129,134,227,129,147,227,129,157>>, 1337 ?assertEqual( 1338 "127.0.0.1:15984 127.0.0.1 %E3%82%88%E3%81%86%E3%81%93%E3%81%9D " 1339 "GET /_all_dbs 201 ok 10000", 1340 test_log_request("/_all_dbs", #user_ctx{name = Utf8User})), 1341 ok. 1342 1343test_log_request(RawPath, UserCtx) -> 1344 Headers = mochiweb_headers:make([{"HOST", "127.0.0.1:15984"}]), 1345 MochiReq = mochiweb_request:new(socket, [], 'POST', RawPath, version, Headers), 1346 Req = #httpd{ 1347 mochi_req = MochiReq, 1348 begin_ts = {1458,588713,124003}, 1349 original_method = 'GET', 1350 peer = "127.0.0.1", 1351 nonce = "nonce", 1352 user_ctx = UserCtx 1353 }, 1354 Resp = #httpd_resp{ 1355 end_ts = {1458,588723,124303}, 1356 code = 201, 1357 status = ok 1358 }, 1359 ok = meck:new(couch_log, [passthrough]), 1360 ok = meck:expect(couch_log, notice, fun(Format, Args) -> 1361 lists:flatten(io_lib:format(Format, Args)) 1362 end), 1363 Message = maybe_log(Req, Resp), 1364 ok = meck:unload(couch_log), 1365 Message. 1366 1367handle_req_after_auth_test() -> 1368 Headers = mochiweb_headers:make([{"HOST", "127.0.0.1:15984"}]), 1369 MochiReq = mochiweb_request:new(socket, [], 'PUT', "/newdb", version, 1370 Headers), 1371 UserCtx = #user_ctx{name = <<"retain_user">>}, 1372 Roles = [<<"_reader">>], 1373 AuthorizedCtx = #user_ctx{name = <<"retain_user">>, roles = Roles}, 1374 Req = #httpd{ 1375 mochi_req = MochiReq, 1376 begin_ts = {1458,588713,124003}, 1377 original_method = 'PUT', 1378 peer = "127.0.0.1", 1379 nonce = "nonce", 1380 user_ctx = UserCtx 1381 }, 1382 AuthorizedReq = Req#httpd{user_ctx = AuthorizedCtx}, 1383 ok = meck:new(chttpd_handlers, [passthrough]), 1384 ok = meck:new(chttpd_auth, [passthrough]), 1385 ok = meck:expect(chttpd_handlers, url_handler, fun(_Key, _Fun) -> 1386 fun(_Req) -> handled_authorized_req end 1387 end), 1388 ok = meck:expect(chttpd_auth, authorize, fun(_Req, _Fun) -> 1389 AuthorizedReq 1390 end), 1391 ?assertEqual({AuthorizedReq, handled_authorized_req}, 1392 handle_req_after_auth(foo_key, Req)), 1393 ok = meck:expect(chttpd_auth, authorize, fun(_Req, _Fun) -> 1394 meck:exception(throw, {http_abort, resp, some_reason}) 1395 end), 1396 ?assertEqual({Req, {aborted, resp, some_reason}}, 1397 handle_req_after_auth(foo_key, Req)), 1398 ok = meck:unload(chttpd_handlers), 1399 ok = meck:unload(chttpd_auth). 1400 1401-endif. 1402