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