Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 8733010A17 for ; Tue, 7 Jan 2014 00:36:43 +0000 (UTC) Received: (qmail 4082 invoked by uid 500); 7 Jan 2014 00:36:32 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 3472 invoked by uid 500); 7 Jan 2014 00:36:28 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 2116 invoked by uid 99); 7 Jan 2014 00:36:23 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 07 Jan 2014 00:36:23 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 69E43383FD; Tue, 7 Jan 2014 00:36:23 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit From: benoitc@apache.org To: commits@couchdb.apache.org Date: Tue, 07 Jan 2014 00:37:06 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [46/57] [abbrv] [partial] inital move to rebar compilation http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/couch/src/couch_file.erl ---------------------------------------------------------------------- diff --git a/apps/couch/src/couch_file.erl b/apps/couch/src/couch_file.erl new file mode 100644 index 0000000..ee5dafb --- /dev/null +++ b/apps/couch/src/couch_file.erl @@ -0,0 +1,532 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_file). +-behaviour(gen_server). + +-include("couch_db.hrl"). + +-define(SIZE_BLOCK, 4096). + +-record(file, { + fd, + eof = 0 +}). + +% public API +-export([open/1, open/2, close/1, bytes/1, sync/1, truncate/2]). +-export([pread_term/2, pread_iolist/2, pread_binary/2]). +-export([append_binary/2, append_binary_md5/2]). +-export([append_raw_chunk/2, assemble_file_chunk/1, assemble_file_chunk/2]). +-export([append_term/2, append_term/3, append_term_md5/2, append_term_md5/3]). +-export([write_header/2, read_header/1]). +-export([delete/2, delete/3, nuke_dir/2, init_delete_dir/1]). + +% gen_server callbacks +-export([init/1, terminate/2, code_change/3]). +-export([handle_call/3, handle_cast/2, handle_info/2]). + +%%---------------------------------------------------------------------- +%% Args: Valid Options are [create] and [create,overwrite]. +%% Files are opened in read/write mode. +%% Returns: On success, {ok, Fd} +%% or {error, Reason} if the file could not be opened. +%%---------------------------------------------------------------------- + +open(Filepath) -> + open(Filepath, []). + +open(Filepath, Options) -> + case gen_server:start_link(couch_file, + {Filepath, Options, self(), Ref = make_ref()}, []) of + {ok, Fd} -> + {ok, Fd}; + ignore -> + % get the error + receive + {Ref, Pid, {error, Reason} = Error} -> + case process_info(self(), trap_exit) of + {trap_exit, true} -> receive {'EXIT', Pid, _} -> ok end; + {trap_exit, false} -> ok + end, + case {lists:member(nologifmissing, Options), Reason} of + {true, enoent} -> ok; + _ -> + ?LOG_ERROR("Could not open file ~s: ~s", + [Filepath, file:format_error(Reason)]) + end, + Error + end; + Error -> + % We can't say much here, because it could be any kind of error. + % Just let it bubble and an encapsulating subcomponent can perhaps + % be more informative. It will likely appear in the SASL log, anyway. + Error + end. + + +%%---------------------------------------------------------------------- +%% Purpose: To append an Erlang term to the end of the file. +%% Args: Erlang term to serialize and append to the file. +%% Returns: {ok, Pos, NumBytesWritten} where Pos is the file offset to +%% the beginning the serialized term. Use pread_term to read the term +%% back. +%% or {error, Reason}. +%%---------------------------------------------------------------------- + +append_term(Fd, Term) -> + append_term(Fd, Term, []). + +append_term(Fd, Term, Options) -> + Comp = couch_util:get_value(compression, Options, ?DEFAULT_COMPRESSION), + append_binary(Fd, couch_compress:compress(Term, Comp)). + +append_term_md5(Fd, Term) -> + append_term_md5(Fd, Term, []). + +append_term_md5(Fd, Term, Options) -> + Comp = couch_util:get_value(compression, Options, ?DEFAULT_COMPRESSION), + append_binary_md5(Fd, couch_compress:compress(Term, Comp)). + +%%---------------------------------------------------------------------- +%% Purpose: To append an Erlang binary to the end of the file. +%% Args: Erlang term to serialize and append to the file. +%% Returns: {ok, Pos, NumBytesWritten} where Pos is the file offset to the +%% beginning the serialized term. Use pread_term to read the term back. +%% or {error, Reason}. +%%---------------------------------------------------------------------- + +append_binary(Fd, Bin) -> + gen_server:call(Fd, {append_bin, assemble_file_chunk(Bin)}, infinity). + +append_binary_md5(Fd, Bin) -> + gen_server:call(Fd, + {append_bin, assemble_file_chunk(Bin, couch_util:md5(Bin))}, infinity). + +append_raw_chunk(Fd, Chunk) -> + gen_server:call(Fd, {append_bin, Chunk}, infinity). + + +assemble_file_chunk(Bin) -> + [<<0:1/integer, (iolist_size(Bin)):31/integer>>, Bin]. + +assemble_file_chunk(Bin, Md5) -> + [<<1:1/integer, (iolist_size(Bin)):31/integer>>, Md5, Bin]. + +%%---------------------------------------------------------------------- +%% Purpose: Reads a term from a file that was written with append_term +%% Args: Pos, the offset into the file where the term is serialized. +%% Returns: {ok, Term} +%% or {error, Reason}. +%%---------------------------------------------------------------------- + + +pread_term(Fd, Pos) -> + {ok, Bin} = pread_binary(Fd, Pos), + {ok, couch_compress:decompress(Bin)}. + + +%%---------------------------------------------------------------------- +%% Purpose: Reads a binrary from a file that was written with append_binary +%% Args: Pos, the offset into the file where the term is serialized. +%% Returns: {ok, Term} +%% or {error, Reason}. +%%---------------------------------------------------------------------- + +pread_binary(Fd, Pos) -> + {ok, L} = pread_iolist(Fd, Pos), + {ok, iolist_to_binary(L)}. + + +pread_iolist(Fd, Pos) -> + case gen_server:call(Fd, {pread_iolist, Pos}, infinity) of + {ok, IoList, <<>>} -> + {ok, IoList}; + {ok, IoList, Md5} -> + case couch_util:md5(IoList) of + Md5 -> + {ok, IoList}; + _ -> + exit({file_corruption, <<"file corruption">>}) + end; + Error -> + Error + end. + +%%---------------------------------------------------------------------- +%% Purpose: The length of a file, in bytes. +%% Returns: {ok, Bytes} +%% or {error, Reason}. +%%---------------------------------------------------------------------- + +% length in bytes +bytes(Fd) -> + gen_server:call(Fd, bytes, infinity). + +%%---------------------------------------------------------------------- +%% Purpose: Truncate a file to the number of bytes. +%% Returns: ok +%% or {error, Reason}. +%%---------------------------------------------------------------------- + +truncate(Fd, Pos) -> + gen_server:call(Fd, {truncate, Pos}, infinity). + +%%---------------------------------------------------------------------- +%% Purpose: Ensure all bytes written to the file are flushed to disk. +%% Returns: ok +%% or {error, Reason}. +%%---------------------------------------------------------------------- + +sync(Filepath) when is_list(Filepath) -> + {ok, Fd} = file:open(Filepath, [append, raw]), + try ok = file:sync(Fd) after ok = file:close(Fd) end; +sync(Fd) -> + gen_server:call(Fd, sync, infinity). + +%%---------------------------------------------------------------------- +%% Purpose: Close the file. +%% Returns: ok +%%---------------------------------------------------------------------- +close(Fd) -> + couch_util:shutdown_sync(Fd). + + +delete(RootDir, Filepath) -> + delete(RootDir, Filepath, true). + + +delete(RootDir, Filepath, Async) -> + DelFile = filename:join([RootDir,".delete", ?b2l(couch_uuids:random())]), + case file:rename(Filepath, DelFile) of + ok -> + if (Async) -> + spawn(file, delete, [DelFile]), + ok; + true -> + file:delete(DelFile) + end; + Error -> + Error + end. + + +nuke_dir(RootDelDir, Dir) -> + FoldFun = fun(File) -> + Path = Dir ++ "/" ++ File, + case filelib:is_dir(Path) of + true -> + ok = nuke_dir(RootDelDir, Path), + file:del_dir(Path); + false -> + delete(RootDelDir, Path, false) + end + end, + case file:list_dir(Dir) of + {ok, Files} -> + lists:foreach(FoldFun, Files), + ok = file:del_dir(Dir); + {error, enoent} -> + ok + end. + + +init_delete_dir(RootDir) -> + Dir = filename:join(RootDir,".delete"), + % note: ensure_dir requires an actual filename companent, which is the + % reason for "foo". + filelib:ensure_dir(filename:join(Dir,"foo")), + filelib:fold_files(Dir, ".*", true, + fun(Filename, _) -> + ok = file:delete(Filename) + end, ok). + + +read_header(Fd) -> + case gen_server:call(Fd, find_header, infinity) of + {ok, Bin} -> + {ok, binary_to_term(Bin)}; + Else -> + Else + end. + +write_header(Fd, Data) -> + Bin = term_to_binary(Data), + Md5 = couch_util:md5(Bin), + % now we assemble the final header binary and write to disk + FinalBin = <>, + gen_server:call(Fd, {write_header, FinalBin}, infinity). + + + + +init_status_error(ReturnPid, Ref, Error) -> + ReturnPid ! {Ref, self(), Error}, + ignore. + +% server functions + +init({Filepath, Options, ReturnPid, Ref}) -> + process_flag(trap_exit, true), + OpenOptions = file_open_options(Options), + case lists:member(create, Options) of + true -> + filelib:ensure_dir(Filepath), + case file:open(Filepath, OpenOptions) of + {ok, Fd} -> + {ok, Length} = file:position(Fd, eof), + case Length > 0 of + true -> + % this means the file already exists and has data. + % FYI: We don't differentiate between empty files and non-existant + % files here. + case lists:member(overwrite, Options) of + true -> + {ok, 0} = file:position(Fd, 0), + ok = file:truncate(Fd), + ok = file:sync(Fd), + maybe_track_open_os_files(Options), + {ok, #file{fd=Fd}}; + false -> + ok = file:close(Fd), + init_status_error(ReturnPid, Ref, {error, eexist}) + end; + false -> + maybe_track_open_os_files(Options), + {ok, #file{fd=Fd}} + end; + Error -> + init_status_error(ReturnPid, Ref, Error) + end; + false -> + % open in read mode first, so we don't create the file if it doesn't exist. + case file:open(Filepath, [read, raw]) of + {ok, Fd_Read} -> + {ok, Fd} = file:open(Filepath, OpenOptions), + ok = file:close(Fd_Read), + maybe_track_open_os_files(Options), + {ok, Eof} = file:position(Fd, eof), + {ok, #file{fd=Fd, eof=Eof}}; + Error -> + init_status_error(ReturnPid, Ref, Error) + end + end. + +file_open_options(Options) -> + [read, raw, binary] ++ case lists:member(read_only, Options) of + true -> + []; + false -> + [append] + end. + +maybe_track_open_os_files(FileOptions) -> + case lists:member(sys_db, FileOptions) of + true -> + ok; + false -> + couch_stats_collector:track_process_count({couchdb, open_os_files}) + end. + +terminate(_Reason, #file{fd = Fd}) -> + ok = file:close(Fd). + + +handle_call({pread_iolist, Pos}, _From, File) -> + {RawData, NextPos} = try + % up to 8Kbs of read ahead + read_raw_iolist_int(File, Pos, 2 * ?SIZE_BLOCK - (Pos rem ?SIZE_BLOCK)) + catch + _:_ -> + read_raw_iolist_int(File, Pos, 4) + end, + <> = + iolist_to_binary(RawData), + case Prefix of + 1 -> + {Md5, IoList} = extract_md5( + maybe_read_more_iolist(RestRawData, 16 + Len, NextPos, File)), + {reply, {ok, IoList, Md5}, File}; + 0 -> + IoList = maybe_read_more_iolist(RestRawData, Len, NextPos, File), + {reply, {ok, IoList, <<>>}, File} + end; + +handle_call(bytes, _From, #file{fd = Fd} = File) -> + {reply, file:position(Fd, eof), File}; + +handle_call(sync, _From, #file{fd=Fd}=File) -> + {reply, file:sync(Fd), File}; + +handle_call({truncate, Pos}, _From, #file{fd=Fd}=File) -> + {ok, Pos} = file:position(Fd, Pos), + case file:truncate(Fd) of + ok -> + {reply, ok, File#file{eof = Pos}}; + Error -> + {reply, Error, File} + end; + +handle_call({append_bin, Bin}, _From, #file{fd = Fd, eof = Pos} = File) -> + Blocks = make_blocks(Pos rem ?SIZE_BLOCK, Bin), + Size = iolist_size(Blocks), + case file:write(Fd, Blocks) of + ok -> + {reply, {ok, Pos, Size}, File#file{eof = Pos + Size}}; + Error -> + {reply, Error, File} + end; + +handle_call({write_header, Bin}, _From, #file{fd = Fd, eof = Pos} = File) -> + BinSize = byte_size(Bin), + case Pos rem ?SIZE_BLOCK of + 0 -> + Padding = <<>>; + BlockOffset -> + Padding = <<0:(8*(?SIZE_BLOCK-BlockOffset))>> + end, + FinalBin = [Padding, <<1, BinSize:32/integer>> | make_blocks(5, [Bin])], + case file:write(Fd, FinalBin) of + ok -> + {reply, ok, File#file{eof = Pos + iolist_size(FinalBin)}}; + Error -> + {reply, Error, File} + end; + +handle_call(find_header, _From, #file{fd = Fd, eof = Pos} = File) -> + {reply, find_header(Fd, Pos div ?SIZE_BLOCK), File}. + +handle_cast(close, Fd) -> + {stop,normal,Fd}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +handle_info({'EXIT', _, normal}, Fd) -> + {noreply, Fd}; +handle_info({'EXIT', _, Reason}, Fd) -> + {stop, Reason, Fd}. + + +find_header(_Fd, -1) -> + no_valid_header; +find_header(Fd, Block) -> + case (catch load_header(Fd, Block)) of + {ok, Bin} -> + {ok, Bin}; + _Error -> + find_header(Fd, Block -1) + end. + +load_header(Fd, Block) -> + {ok, <<1, HeaderLen:32/integer, RestBlock/binary>>} = + file:pread(Fd, Block * ?SIZE_BLOCK, ?SIZE_BLOCK), + TotalBytes = calculate_total_read_len(5, HeaderLen), + case TotalBytes > byte_size(RestBlock) of + false -> + <> = RestBlock; + true -> + {ok, Missing} = file:pread( + Fd, (Block * ?SIZE_BLOCK) + 5 + byte_size(RestBlock), + TotalBytes - byte_size(RestBlock)), + RawBin = <> + end, + <> = + iolist_to_binary(remove_block_prefixes(5, RawBin)), + Md5Sig = couch_util:md5(HeaderBin), + {ok, HeaderBin}. + +maybe_read_more_iolist(Buffer, DataSize, _, _) + when DataSize =< byte_size(Buffer) -> + <> = Buffer, + [Data]; +maybe_read_more_iolist(Buffer, DataSize, NextPos, File) -> + {Missing, _} = + read_raw_iolist_int(File, NextPos, DataSize - byte_size(Buffer)), + [Buffer, Missing]. + +-spec read_raw_iolist_int(#file{}, Pos::non_neg_integer(), Len::non_neg_integer()) -> + {Data::iolist(), CurPos::non_neg_integer()}. +read_raw_iolist_int(Fd, {Pos, _Size}, Len) -> % 0110 UPGRADE CODE + read_raw_iolist_int(Fd, Pos, Len); +read_raw_iolist_int(#file{fd = Fd}, Pos, Len) -> + BlockOffset = Pos rem ?SIZE_BLOCK, + TotalBytes = calculate_total_read_len(BlockOffset, Len), + {ok, <>} = file:pread(Fd, Pos, TotalBytes), + {remove_block_prefixes(BlockOffset, RawBin), Pos + TotalBytes}. + +-spec extract_md5(iolist()) -> {binary(), iolist()}. +extract_md5(FullIoList) -> + {Md5List, IoList} = split_iolist(FullIoList, 16, []), + {iolist_to_binary(Md5List), IoList}. + +calculate_total_read_len(0, FinalLen) -> + calculate_total_read_len(1, FinalLen) + 1; +calculate_total_read_len(BlockOffset, FinalLen) -> + case ?SIZE_BLOCK - BlockOffset of + BlockLeft when BlockLeft >= FinalLen -> + FinalLen; + BlockLeft -> + FinalLen + ((FinalLen - BlockLeft) div (?SIZE_BLOCK -1)) + + if ((FinalLen - BlockLeft) rem (?SIZE_BLOCK -1)) =:= 0 -> 0; + true -> 1 end + end. + +remove_block_prefixes(_BlockOffset, <<>>) -> + []; +remove_block_prefixes(0, <<_BlockPrefix,Rest/binary>>) -> + remove_block_prefixes(1, Rest); +remove_block_prefixes(BlockOffset, Bin) -> + BlockBytesAvailable = ?SIZE_BLOCK - BlockOffset, + case size(Bin) of + Size when Size > BlockBytesAvailable -> + <> = Bin, + [DataBlock | remove_block_prefixes(0, Rest)]; + _Size -> + [Bin] + end. + +make_blocks(_BlockOffset, []) -> + []; +make_blocks(0, IoList) -> + [<<0>> | make_blocks(1, IoList)]; +make_blocks(BlockOffset, IoList) -> + case split_iolist(IoList, (?SIZE_BLOCK - BlockOffset), []) of + {Begin, End} -> + [Begin | make_blocks(0, End)]; + _SplitRemaining -> + IoList + end. + +%% @doc Returns a tuple where the first element contains the leading SplitAt +%% bytes of the original iolist, and the 2nd element is the tail. If SplitAt +%% is larger than byte_size(IoList), return the difference. +-spec split_iolist(IoList::iolist(), SplitAt::non_neg_integer(), Acc::list()) -> + {iolist(), iolist()} | non_neg_integer(). +split_iolist(List, 0, BeginAcc) -> + {lists:reverse(BeginAcc), List}; +split_iolist([], SplitAt, _BeginAcc) -> + SplitAt; +split_iolist([<> | Rest], SplitAt, BeginAcc) when SplitAt > byte_size(Bin) -> + split_iolist(Rest, SplitAt - byte_size(Bin), [Bin | BeginAcc]); +split_iolist([<> | Rest], SplitAt, BeginAcc) -> + <> = Bin, + split_iolist([End | Rest], 0, [Begin | BeginAcc]); +split_iolist([Sublist| Rest], SplitAt, BeginAcc) when is_list(Sublist) -> + case split_iolist(Sublist, SplitAt, BeginAcc) of + {Begin, End} -> + {Begin, [End | Rest]}; + SplitRemaining -> + split_iolist(Rest, SplitAt - (SplitAt - SplitRemaining), [Sublist | BeginAcc]) + end; +split_iolist([Byte | Rest], SplitAt, BeginAcc) when is_integer(Byte) -> + split_iolist(Rest, SplitAt - 1, [Byte | BeginAcc]). http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/couch/src/couch_httpd.erl ---------------------------------------------------------------------- diff --git a/apps/couch/src/couch_httpd.erl b/apps/couch/src/couch_httpd.erl new file mode 100644 index 0000000..28932ba --- /dev/null +++ b/apps/couch/src/couch_httpd.erl @@ -0,0 +1,1114 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_httpd). +-include("couch_db.hrl"). + +-export([start_link/0, start_link/1, stop/0, config_change/2, + handle_request/5]). + +-export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,qs_json_value/3]). +-export([path/1,absolute_uri/2,body_length/1]). +-export([verify_is_server_admin/1,unquote/1,quote/1,recv/2,recv_chunked/4,error_info/1]). +-export([make_fun_spec_strs/1]). +-export([make_arity_1_fun/1, make_arity_2_fun/1, make_arity_3_fun/1]). +-export([parse_form/1,json_body/1,json_body_obj/1,body/1]). +-export([doc_etag/1, make_etag/1, etag_match/2, etag_respond/3, etag_maybe/2]). +-export([primary_header_value/2,partition/1,serve_file/3,serve_file/4, server_header/0]). +-export([start_chunked_response/3,send_chunk/2,log_request/2]). +-export([start_response_length/4, start_response/3, send/2]). +-export([start_json_response/2, start_json_response/3, end_json_response/1]). +-export([send_response/4,send_method_not_allowed/2,send_error/4, send_redirect/2,send_chunked_error/2]). +-export([send_json/2,send_json/3,send_json/4,last_chunk/1,parse_multipart_request/3]). +-export([accepted_encodings/1,handle_request_int/5,validate_referer/1,validate_ctype/2]). +-export([http_1_0_keep_alive/2]). + +start_link() -> + start_link(http). +start_link(http) -> + Port = couch_config:get("httpd", "port", "5984"), + start_link(?MODULE, [{port, Port}]); +start_link(https) -> + Port = couch_config:get("ssl", "port", "6984"), + CertFile = couch_config:get("ssl", "cert_file", nil), + KeyFile = couch_config:get("ssl", "key_file", nil), + Options = case CertFile /= nil andalso KeyFile /= nil of + true -> + SslOpts = [{certfile, CertFile}, {keyfile, KeyFile}], + + %% set password if one is needed for the cert + SslOpts1 = case couch_config:get("ssl", "password", nil) of + nil -> SslOpts; + Password -> + SslOpts ++ [{password, Password}] + end, + % do we verify certificates ? + FinalSslOpts = case couch_config:get("ssl", + "verify_ssl_certificates", "false") of + "false" -> SslOpts1; + "true" -> + case couch_config:get("ssl", + "cacert_file", nil) of + nil -> + io:format("Verify SSL certificate " + ++"enabled but file containing " + ++"PEM encoded CA certificates is " + ++"missing", []), + throw({error, missing_cacerts}); + CaCertFile -> + Depth = list_to_integer(couch_config:get("ssl", + "ssl_certificate_max_depth", + "1")), + FinalOpts = [ + {cacertfile, CaCertFile}, + {depth, Depth}, + {verify, verify_peer}], + % allows custom verify fun. + case couch_config:get("ssl", + "verify_fun", nil) of + nil -> FinalOpts; + SpecStr -> + FinalOpts + ++ [{verify_fun, make_arity_3_fun(SpecStr)}] + end + end + end, + + [{port, Port}, + {ssl, true}, + {ssl_opts, FinalSslOpts}]; + false -> + io:format("SSL enabled but PEM certificates are missing.", []), + throw({error, missing_certs}) + end, + start_link(https, Options). +start_link(Name, Options) -> + % read config and register for configuration changes + + % just stop if one of the config settings change. couch_server_sup + % will restart us and then we will pick up the new settings. + + BindAddress = couch_config:get("httpd", "bind_address", any), + validate_bind_address(BindAddress), + DefaultSpec = "{couch_httpd_db, handle_request}", + DefaultFun = make_arity_1_fun( + couch_config:get("httpd", "default_handler", DefaultSpec) + ), + + UrlHandlersList = lists:map( + fun({UrlKey, SpecStr}) -> + {?l2b(UrlKey), make_arity_1_fun(SpecStr)} + end, couch_config:get("httpd_global_handlers")), + + DbUrlHandlersList = lists:map( + fun({UrlKey, SpecStr}) -> + {?l2b(UrlKey), make_arity_2_fun(SpecStr)} + end, couch_config:get("httpd_db_handlers")), + + DesignUrlHandlersList = lists:map( + fun({UrlKey, SpecStr}) -> + {?l2b(UrlKey), make_arity_3_fun(SpecStr)} + end, couch_config:get("httpd_design_handlers")), + + UrlHandlers = dict:from_list(UrlHandlersList), + DbUrlHandlers = dict:from_list(DbUrlHandlersList), + DesignUrlHandlers = dict:from_list(DesignUrlHandlersList), + {ok, ServerOptions} = couch_util:parse_term( + couch_config:get("httpd", "server_options", "[]")), + {ok, SocketOptions} = couch_util:parse_term( + couch_config:get("httpd", "socket_options", "[]")), + + set_auth_handlers(), + + % ensure uuid is set so that concurrent replications + % get the same value. + couch_server:get_uuid(), + + Loop = fun(Req)-> + case SocketOptions of + [] -> + ok; + _ -> + ok = mochiweb_socket:setopts(Req:get(socket), SocketOptions) + end, + apply(?MODULE, handle_request, [ + Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers + ]) + end, + + % set mochiweb options + FinalOptions = lists:append([Options, ServerOptions, [ + {loop, Loop}, + {name, Name}, + {ip, BindAddress}]]), + + % launch mochiweb + {ok, Pid} = case mochiweb_http:start(FinalOptions) of + {ok, MochiPid} -> + {ok, MochiPid}; + {error, Reason} -> + io:format("Failure to start Mochiweb: ~s~n",[Reason]), + throw({error, Reason}) + end, + + ok = couch_config:register(fun ?MODULE:config_change/2, Pid), + {ok, Pid}. + + +stop() -> + mochiweb_http:stop(couch_httpd), + mochiweb_http:stop(https). + +config_change("httpd", "bind_address") -> + ?MODULE:stop(); +config_change("httpd", "port") -> + ?MODULE:stop(); +config_change("httpd", "default_handler") -> + ?MODULE:stop(); +config_change("httpd", "server_options") -> + ?MODULE:stop(); +config_change("httpd", "socket_options") -> + ?MODULE:stop(); +config_change("httpd", "authentication_handlers") -> + set_auth_handlers(); +config_change("httpd_global_handlers", _) -> + ?MODULE:stop(); +config_change("httpd_db_handlers", _) -> + ?MODULE:stop(); +config_change("ssl", _) -> + ?MODULE:stop(). + +set_auth_handlers() -> + AuthenticationSrcs = make_fun_spec_strs( + couch_config:get("httpd", "authentication_handlers", "")), + AuthHandlers = lists:map( + fun(A) -> {make_arity_1_fun(A), ?l2b(A)} end, AuthenticationSrcs), + ok = application:set_env(couch, auth_handlers, AuthHandlers). + +% SpecStr is a string like "{my_module, my_fun}" +% or "{my_module, my_fun, <<"my_arg">>}" +make_arity_1_fun(SpecStr) -> + case couch_util:parse_term(SpecStr) of + {ok, {Mod, Fun, SpecArg}} -> + fun(Arg) -> Mod:Fun(Arg, SpecArg) end; + {ok, {Mod, Fun}} -> + fun(Arg) -> Mod:Fun(Arg) end + end. + +make_arity_2_fun(SpecStr) -> + case couch_util:parse_term(SpecStr) of + {ok, {Mod, Fun, SpecArg}} -> + fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2, SpecArg) end; + {ok, {Mod, Fun}} -> + fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2) end + end. + +make_arity_3_fun(SpecStr) -> + case couch_util:parse_term(SpecStr) of + {ok, {Mod, Fun, SpecArg}} -> + fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3, SpecArg) end; + {ok, {Mod, Fun}} -> + fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3) end + end. + +% SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}" +make_fun_spec_strs(SpecStr) -> + re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}]). + +handle_request(MochiReq, DefaultFun, UrlHandlers, DbUrlHandlers, + DesignUrlHandlers) -> + %% reset rewrite count for new request + erlang:put(?REWRITE_COUNT, 0), + + MochiReq1 = couch_httpd_vhost:dispatch_host(MochiReq), + + handle_request_int(MochiReq1, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers). + +handle_request_int(MochiReq, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers) -> + Begin = now(), + % for the path, use the raw path with the query string and fragment + % removed, but URL quoting left intact + RawUri = MochiReq:get(raw_path), + {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri), + + Headers = MochiReq:get(headers), + + % get requested path + RequestedPath = case MochiReq:get_header_value("x-couchdb-vhost-path") of + undefined -> + case MochiReq:get_header_value("x-couchdb-requested-path") of + undefined -> RawUri; + R -> R + end; + P -> P + end, + + HandlerKey = + case mochiweb_util:partition(Path, "/") of + {"", "", ""} -> + <<"/">>; % Special case the root url handler + {FirstPart, _, _} -> + list_to_binary(FirstPart) + end, + ?LOG_DEBUG("~p ~s ~p from ~p~nHeaders: ~p", [ + MochiReq:get(method), + RawUri, + MochiReq:get(version), + MochiReq:get(peer), + mochiweb_headers:to_list(MochiReq:get(headers)) + ]), + + Method1 = + case MochiReq:get(method) of + % already an atom + Meth when is_atom(Meth) -> Meth; + + % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when + % possible (if any module references the atom, then it's existing). + Meth -> couch_util:to_existing_atom(Meth) + end, + increment_method_stats(Method1), + + % allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header + MethodOverride = MochiReq:get_primary_header_value("X-HTTP-Method-Override"), + Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST", + "PUT", "DELETE", + "TRACE", "CONNECT", + "COPY"]) of + true -> + ?LOG_INFO("MethodOverride: ~s (real method was ~s)", [MethodOverride, Method1]), + case Method1 of + 'POST' -> couch_util:to_existing_atom(MethodOverride); + _ -> + % Ignore X-HTTP-Method-Override when the original verb isn't POST. + % I'd like to send a 406 error to the client, but that'd require a nasty refactor. + % throw({not_acceptable, <<"X-HTTP-Method-Override may only be used with POST requests.">>}) + Method1 + end; + _ -> Method1 + end, + + % alias HEAD to GET as mochiweb takes care of stripping the body + Method = case Method2 of + 'HEAD' -> 'GET'; + Other -> Other + end, + + HttpReq = #httpd{ + mochi_req = MochiReq, + peer = MochiReq:get(peer), + method = Method, + requested_path_parts = + [?l2b(unquote(Part)) || Part <- string:tokens(RequestedPath, "/")], + path_parts = [?l2b(unquote(Part)) || Part <- string:tokens(Path, "/")], + db_url_handlers = DbUrlHandlers, + design_url_handlers = DesignUrlHandlers, + default_fun = DefaultFun, + url_handlers = UrlHandlers, + user_ctx = erlang:erase(pre_rewrite_user_ctx), + auth = erlang:erase(pre_rewrite_auth) + }, + + HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun), + {ok, AuthHandlers} = application:get_env(couch, auth_handlers), + + {ok, Resp} = + try + case couch_httpd_cors:is_preflight_request(HttpReq) of + #httpd{} -> + case authenticate_request(HttpReq, AuthHandlers) of + #httpd{} = Req -> + HandlerFun(Req); + Response -> + Response + end; + Response -> + Response + end + catch + throw:{http_head_abort, Resp0} -> + {ok, Resp0}; + throw:{invalid_json, S} -> + ?LOG_ERROR("attempted upload of invalid JSON (set log_level to debug to log it)", []), + ?LOG_DEBUG("Invalid JSON: ~p",[S]), + send_error(HttpReq, {bad_request, invalid_json}); + throw:unacceptable_encoding -> + ?LOG_ERROR("unsupported encoding method for the response", []), + send_error(HttpReq, {not_acceptable, "unsupported encoding"}); + throw:bad_accept_encoding_value -> + ?LOG_ERROR("received invalid Accept-Encoding header", []), + send_error(HttpReq, bad_request); + exit:normal -> + exit(normal); + exit:snappy_nif_not_loaded -> + ErrorReason = "To access the database or view index, Apache CouchDB" + " must be built with Erlang OTP R13B04 or higher.", + ?LOG_ERROR("~s", [ErrorReason]), + send_error(HttpReq, {bad_otp_release, ErrorReason}); + exit:{body_too_large, _} -> + send_error(HttpReq, request_entity_too_large); + throw:Error -> + Stack = erlang:get_stacktrace(), + ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]), + ?LOG_DEBUG("Stacktrace: ~p",[Stack]), + send_error(HttpReq, Error); + error:badarg -> + Stack = erlang:get_stacktrace(), + ?LOG_ERROR("Badarg error in HTTP request",[]), + ?LOG_INFO("Stacktrace: ~p",[Stack]), + send_error(HttpReq, badarg); + error:function_clause -> + Stack = erlang:get_stacktrace(), + ?LOG_ERROR("function_clause error in HTTP request",[]), + ?LOG_INFO("Stacktrace: ~p",[Stack]), + send_error(HttpReq, function_clause); + Tag:Error -> + Stack = erlang:get_stacktrace(), + ?LOG_ERROR("Uncaught error in HTTP request: ~p",[{Tag, Error}]), + ?LOG_INFO("Stacktrace: ~p",[Stack]), + send_error(HttpReq, Error) + end, + RequestTime = round(timer:now_diff(now(), Begin)/1000), + couch_stats_collector:record({couchdb, request_time}, RequestTime), + couch_stats_collector:increment({httpd, requests}), + {ok, Resp}. + +% Try authentication handlers in order until one sets a user_ctx +% the auth funs also have the option of returning a response +% move this to couch_httpd_auth? +authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthHandlers) -> + Req; +authenticate_request(#httpd{} = Req, []) -> + case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of + "true" -> + throw({unauthorized, <<"Authentication required.">>}); + "false" -> + Req#httpd{user_ctx=#user_ctx{}} + end; +authenticate_request(#httpd{} = Req, [{AuthFun, AuthSrc} | RestAuthHandlers]) -> + R = case AuthFun(Req) of + #httpd{user_ctx=#user_ctx{}=UserCtx}=Req2 -> + Req2#httpd{user_ctx=UserCtx#user_ctx{handler=AuthSrc}}; + Else -> Else + end, + authenticate_request(R, RestAuthHandlers); +authenticate_request(Response, _AuthSrcs) -> + Response. + +increment_method_stats(Method) -> + couch_stats_collector:increment({httpd_request_methods, Method}). + +validate_referer(Req) -> + Host = host_for_request(Req), + Referer = header_value(Req, "Referer", fail), + case Referer of + fail -> + throw({bad_request, <<"Referer header required.">>}); + Referer -> + {_,RefererHost,_,_,_} = mochiweb_util:urlsplit(Referer), + if + RefererHost =:= Host -> ok; + true -> throw({bad_request, <<"Referer header must match host.">>}) + end + end. + +validate_ctype(Req, Ctype) -> + case header_value(Req, "Content-Type") of + undefined -> + throw({bad_ctype, "Content-Type must be "++Ctype}); + ReqCtype -> + case string:tokens(ReqCtype, ";") of + [Ctype] -> ok; + [Ctype, _Rest] -> ok; + _Else -> + throw({bad_ctype, "Content-Type must be "++Ctype}) + end + end. + +% Utilities + +partition(Path) -> + mochiweb_util:partition(Path, "/"). + +header_value(#httpd{mochi_req=MochiReq}, Key) -> + MochiReq:get_header_value(Key). + +header_value(#httpd{mochi_req=MochiReq}, Key, Default) -> + case MochiReq:get_header_value(Key) of + undefined -> Default; + Value -> Value + end. + +primary_header_value(#httpd{mochi_req=MochiReq}, Key) -> + MochiReq:get_primary_header_value(Key). + +accepted_encodings(#httpd{mochi_req=MochiReq}) -> + case MochiReq:accepted_encodings(["gzip", "identity"]) of + bad_accept_encoding_value -> + throw(bad_accept_encoding_value); + [] -> + throw(unacceptable_encoding); + EncList -> + EncList + end. + +serve_file(Req, RelativePath, DocumentRoot) -> + serve_file(Req, RelativePath, DocumentRoot, []). + +serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot, + ExtraHeaders) -> + log_request(Req, 200), + ResponseHeaders = server_header() + ++ couch_httpd_auth:cookie_auth_header(Req, []) + ++ ExtraHeaders, + {ok, MochiReq:serve_file(RelativePath, DocumentRoot, + couch_httpd_cors:cors_headers(Req, ResponseHeaders))}. + +qs_value(Req, Key) -> + qs_value(Req, Key, undefined). + +qs_value(Req, Key, Default) -> + couch_util:get_value(Key, qs(Req), Default). + +qs_json_value(Req, Key, Default) -> + case qs_value(Req, Key, Default) of + Default -> + Default; + Result -> + ?JSON_DECODE(Result) + end. + +qs(#httpd{mochi_req=MochiReq}) -> + MochiReq:parse_qs(). + +path(#httpd{mochi_req=MochiReq}) -> + MochiReq:get(path). + +host_for_request(#httpd{mochi_req=MochiReq}) -> + XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"), + case MochiReq:get_header_value(XHost) of + undefined -> + case MochiReq:get_header_value("Host") of + undefined -> + {ok, {Address, Port}} = case MochiReq:get(socket) of + {ssl, SslSocket} -> ssl:sockname(SslSocket); + Socket -> inet:sockname(Socket) + end, + inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port); + Value1 -> + Value1 + end; + Value -> Value + end. + +absolute_uri(#httpd{mochi_req=MochiReq}=Req, Path) -> + Host = host_for_request(Req), + XSsl = couch_config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"), + Scheme = case MochiReq:get_header_value(XSsl) of + "on" -> "https"; + _ -> + XProto = couch_config:get("httpd", "x_forwarded_proto", "X-Forwarded-Proto"), + case MochiReq:get_header_value(XProto) of + %% Restrict to "https" and "http" schemes only + "https" -> "https"; + _ -> case MochiReq:get(scheme) of + https -> "https"; + http -> "http" + end + end + end, + Scheme ++ "://" ++ Host ++ Path. + +unquote(UrlEncodedString) -> + mochiweb_util:unquote(UrlEncodedString). + +quote(UrlDecodedString) -> + mochiweb_util:quote_plus(UrlDecodedString). + +parse_form(#httpd{mochi_req=MochiReq}) -> + mochiweb_multipart:parse_form(MochiReq). + +recv(#httpd{mochi_req=MochiReq}, Len) -> + MochiReq:recv(Len). + +recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) -> + % Fun is called once with each chunk + % Fun({Length, Binary}, State) + % called with Length == 0 on the last time. + MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState). + +body_length(#httpd{mochi_req=MochiReq}) -> + MochiReq:get(body_length). + +body(#httpd{mochi_req=MochiReq, req_body=undefined}) -> + MaxSize = list_to_integer( + couch_config:get("couchdb", "max_document_size", "4294967296")), + MochiReq:recv_body(MaxSize); +body(#httpd{req_body=ReqBody}) -> + ReqBody. + +json_body(Httpd) -> + ?JSON_DECODE(body(Httpd)). + +json_body_obj(Httpd) -> + case json_body(Httpd) of + {Props} -> {Props}; + _Else -> + throw({bad_request, "Request body must be a JSON object"}) + end. + + + +doc_etag(#doc{revs={Start, [DiskRev|_]}}) -> + "\"" ++ ?b2l(couch_doc:rev_to_str({Start, DiskRev})) ++ "\"". + +make_etag(Term) -> + <> = couch_util:md5(term_to_binary(Term)), + iolist_to_binary([$", io_lib:format("~.36B", [SigInt]), $"]). + +etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) -> + etag_match(Req, binary_to_list(CurrentEtag)); + +etag_match(Req, CurrentEtag) -> + EtagsToMatch = string:tokens( + header_value(Req, "If-None-Match", ""), ", "), + lists:member(CurrentEtag, EtagsToMatch). + +etag_respond(Req, CurrentEtag, RespFun) -> + case etag_match(Req, CurrentEtag) of + true -> + % the client has this in their cache. + send_response(Req, 304, [{"ETag", CurrentEtag}], <<>>); + false -> + % Run the function. + RespFun() + end. + +etag_maybe(Req, RespFun) -> + try + RespFun() + catch + throw:{etag_match, ETag} -> + send_response(Req, 304, [{"ETag", ETag}], <<>>) + end. + +verify_is_server_admin(#httpd{user_ctx=UserCtx}) -> + verify_is_server_admin(UserCtx); +verify_is_server_admin(#user_ctx{roles=Roles}) -> + case lists:member(<<"_admin">>, Roles) of + true -> ok; + false -> throw({unauthorized, <<"You are not a server admin.">>}) + end. + +log_request(#httpd{mochi_req=MochiReq,peer=Peer}=Req, Code) -> + ?LOG_INFO("~s - - ~s ~s ~B", [ + Peer, + MochiReq:get(method), + MochiReq:get(raw_path), + Code + ]), + gen_event:notify(couch_plugin, {log_request, Req, Code}). + + +start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> + log_request(Req, Code), + couch_stats_collector:increment({httpd_status_codes, Code}), + Headers1 = Headers ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers), + Headers2 = couch_httpd_cors:cors_headers(Req, Headers1), + Resp = MochiReq:start_response_length({Code, Headers2, Length}), + case MochiReq:get(method) of + 'HEAD' -> throw({http_head_abort, Resp}); + _ -> ok + end, + {ok, Resp}. + +start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> + log_request(Req, Code), + couch_stats_collector:increment({httpd_status_codes, Code}), + CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers), + Headers1 = Headers ++ server_header() ++ CookieHeader, + Headers2 = couch_httpd_cors:cors_headers(Req, Headers1), + Resp = MochiReq:start_response({Code, Headers2}), + case MochiReq:get(method) of + 'HEAD' -> throw({http_head_abort, Resp}); + _ -> ok + end, + {ok, Resp}. + +send(Resp, Data) -> + Resp:send(Data), + {ok, Resp}. + +no_resp_conn_header([]) -> + true; +no_resp_conn_header([{Hdr, _}|Rest]) -> + case string:to_lower(Hdr) of + "connection" -> false; + _ -> no_resp_conn_header(Rest) + end. + +http_1_0_keep_alive(Req, Headers) -> + KeepOpen = Req:should_close() == false, + IsHttp10 = Req:get(version) == {1, 0}, + NoRespHeader = no_resp_conn_header(Headers), + case KeepOpen andalso IsHttp10 andalso NoRespHeader of + true -> [{"Connection", "Keep-Alive"} | Headers]; + false -> Headers + end. + +start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> + log_request(Req, Code), + couch_stats_collector:increment({httpd_status_codes, Code}), + Headers1 = http_1_0_keep_alive(MochiReq, Headers), + Headers2 = Headers1 ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers1), + Headers3 = couch_httpd_cors:cors_headers(Req, Headers2), + Resp = MochiReq:respond({Code, Headers3, chunked}), + case MochiReq:get(method) of + 'HEAD' -> throw({http_head_abort, Resp}); + _ -> ok + end, + {ok, Resp}. + +send_chunk(Resp, Data) -> + case iolist_size(Data) of + 0 -> ok; % do nothing + _ -> Resp:write_chunk(Data) + end, + {ok, Resp}. + +last_chunk(Resp) -> + Resp:write_chunk([]), + {ok, Resp}. + +send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> + log_request(Req, Code), + couch_stats_collector:increment({httpd_status_codes, Code}), + Headers1 = http_1_0_keep_alive(MochiReq, Headers), + if Code >= 500 -> + ?LOG_ERROR("httpd ~p error response:~n ~s", [Code, Body]); + Code >= 400 -> + ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); + true -> ok + end, + Headers2 = Headers1 ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers1), + Headers3 = couch_httpd_cors:cors_headers(Req, Headers2), + + {ok, MochiReq:respond({Code, Headers3, Body})}. + +send_method_not_allowed(Req, Methods) -> + send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")). + +send_json(Req, Value) -> + send_json(Req, 200, Value). + +send_json(Req, Code, Value) -> + send_json(Req, Code, [], Value). + +send_json(Req, Code, Headers, Value) -> + initialize_jsonp(Req), + DefaultHeaders = [ + {"Content-Type", negotiate_content_type(Req)}, + {"Cache-Control", "must-revalidate"} + ], + Body = [start_jsonp(), ?JSON_ENCODE(Value), end_jsonp(), $\n], + send_response(Req, Code, DefaultHeaders ++ Headers, Body). + +start_json_response(Req, Code) -> + start_json_response(Req, Code, []). + +start_json_response(Req, Code, Headers) -> + initialize_jsonp(Req), + DefaultHeaders = [ + {"Content-Type", negotiate_content_type(Req)}, + {"Cache-Control", "must-revalidate"} + ], + {ok, Resp} = start_chunked_response(Req, Code, DefaultHeaders ++ Headers), + case start_jsonp() of + [] -> ok; + Start -> send_chunk(Resp, Start) + end, + {ok, Resp}. + +end_json_response(Resp) -> + send_chunk(Resp, end_jsonp() ++ [$\n]), + last_chunk(Resp). + +initialize_jsonp(Req) -> + case get(jsonp) of + undefined -> put(jsonp, qs_value(Req, "callback", no_jsonp)); + _ -> ok + end, + case get(jsonp) of + no_jsonp -> []; + [] -> []; + CallBack -> + try + % make sure jsonp is configured on (default off) + case couch_config:get("httpd", "allow_jsonp", "false") of + "true" -> + validate_callback(CallBack); + _Else -> + put(jsonp, no_jsonp) + end + catch + Error -> + put(jsonp, no_jsonp), + throw(Error) + end + end. + +start_jsonp() -> + case get(jsonp) of + no_jsonp -> []; + [] -> []; + CallBack -> ["/* CouchDB */", CallBack, "("] + end. + +end_jsonp() -> + case erlang:erase(jsonp) of + no_jsonp -> []; + [] -> []; + _ -> ");" + end. + +validate_callback(CallBack) when is_binary(CallBack) -> + validate_callback(binary_to_list(CallBack)); +validate_callback([]) -> + ok; +validate_callback([Char | Rest]) -> + case Char of + _ when Char >= $a andalso Char =< $z -> ok; + _ when Char >= $A andalso Char =< $Z -> ok; + _ when Char >= $0 andalso Char =< $9 -> ok; + _ when Char == $. -> ok; + _ when Char == $_ -> ok; + _ when Char == $[ -> ok; + _ when Char == $] -> ok; + _ -> + throw({bad_request, invalid_callback}) + end, + validate_callback(Rest). + + +error_info({Error, Reason}) when is_list(Reason) -> + error_info({Error, ?l2b(Reason)}); +error_info(bad_request) -> + {400, <<"bad_request">>, <<>>}; +error_info({bad_request, Reason}) -> + {400, <<"bad_request">>, Reason}; +error_info({query_parse_error, Reason}) -> + {400, <<"query_parse_error">>, Reason}; +% Prior art for md5 mismatch resulting in a 400 is from AWS S3 +error_info(md5_mismatch) -> + {400, <<"content_md5_mismatch">>, <<"Possible message corruption.">>}; +error_info(not_found) -> + {404, <<"not_found">>, <<"missing">>}; +error_info({not_found, Reason}) -> + {404, <<"not_found">>, Reason}; +error_info({not_acceptable, Reason}) -> + {406, <<"not_acceptable">>, Reason}; +error_info(conflict) -> + {409, <<"conflict">>, <<"Document update conflict.">>}; +error_info({forbidden, Msg}) -> + {403, <<"forbidden">>, Msg}; +error_info({unauthorized, Msg}) -> + {401, <<"unauthorized">>, Msg}; +error_info(file_exists) -> + {412, <<"file_exists">>, <<"The database could not be " + "created, the file already exists.">>}; +error_info(request_entity_too_large) -> + {413, <<"too_large">>, <<"the request entity is too large">>}; +error_info({bad_ctype, Reason}) -> + {415, <<"bad_content_type">>, Reason}; +error_info(requested_range_not_satisfiable) -> + {416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>}; +error_info({error, illegal_database_name, Name}) -> + Message = "Name: '" ++ Name ++ "'. Only lowercase characters (a-z), " + ++ "digits (0-9), and any of the characters _, $, (, ), +, -, and / " + ++ "are allowed. Must begin with a letter.", + {400, <<"illegal_database_name">>, couch_util:to_binary(Message)}; +error_info({missing_stub, Reason}) -> + {412, <<"missing_stub">>, Reason}; +error_info({Error, Reason}) -> + {500, couch_util:to_binary(Error), couch_util:to_binary(Reason)}; +error_info(Error) -> + {500, <<"unknown_error">>, couch_util:to_binary(Error)}. + +error_headers(#httpd{mochi_req=MochiReq}=Req, Code, ErrorStr, ReasonStr) -> + if Code == 401 -> + % this is where the basic auth popup is triggered + case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of + undefined -> + case couch_config:get("httpd", "WWW-Authenticate", nil) of + nil -> + % If the client is a browser and the basic auth popup isn't turned on + % redirect to the session page. + case ErrorStr of + <<"unauthorized">> -> + case couch_config:get("couch_httpd_auth", "authentication_redirect", nil) of + nil -> {Code, []}; + AuthRedirect -> + case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of + "true" -> + % send the browser popup header no matter what if we are require_valid_user + {Code, [{"WWW-Authenticate", "Basic realm=\"server\""}]}; + _False -> + case MochiReq:accepts_content_type("application/json") of + true -> + {Code, []}; + false -> + case MochiReq:accepts_content_type("text/html") of + true -> + % Redirect to the path the user requested, not + % the one that is used internally. + UrlReturnRaw = case MochiReq:get_header_value("x-couchdb-vhost-path") of + undefined -> + MochiReq:get(path); + VHostPath -> + VHostPath + end, + RedirectLocation = lists:flatten([ + AuthRedirect, + "?return=", couch_util:url_encode(UrlReturnRaw), + "&reason=", couch_util:url_encode(ReasonStr) + ]), + {302, [{"Location", absolute_uri(Req, RedirectLocation)}]}; + false -> + {Code, []} + end + end + end + end; + _Else -> + {Code, []} + end; + Type -> + {Code, [{"WWW-Authenticate", Type}]} + end; + Type -> + {Code, [{"WWW-Authenticate", Type}]} + end; + true -> + {Code, []} + end. + +send_error(_Req, {already_sent, Resp, _Error}) -> + {ok, Resp}; + +send_error(Req, Error) -> + {Code, ErrorStr, ReasonStr} = error_info(Error), + {Code1, Headers} = error_headers(Req, Code, ErrorStr, ReasonStr), + send_error(Req, Code1, Headers, ErrorStr, ReasonStr). + +send_error(Req, Code, ErrorStr, ReasonStr) -> + send_error(Req, Code, [], ErrorStr, ReasonStr). + +send_error(Req, Code, Headers, ErrorStr, ReasonStr) -> + send_json(Req, Code, Headers, + {[{<<"error">>, ErrorStr}, + {<<"reason">>, ReasonStr}]}). + +% give the option for list functions to output html or other raw errors +send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) -> + send_chunk(Resp, Reason), + last_chunk(Resp); + +send_chunked_error(Resp, Error) -> + {Code, ErrorStr, ReasonStr} = error_info(Error), + JsonError = {[{<<"code">>, Code}, + {<<"error">>, ErrorStr}, + {<<"reason">>, ReasonStr}]}, + send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])), + last_chunk(Resp). + +send_redirect(Req, Path) -> + send_response(Req, 301, [{"Location", absolute_uri(Req, Path)}], <<>>). + +negotiate_content_type(Req) -> + case get(jsonp) of + no_jsonp -> negotiate_content_type1(Req); + [] -> negotiate_content_type1(Req); + _Callback -> "text/javascript" + end. + +negotiate_content_type1(#httpd{mochi_req=MochiReq}) -> + %% Determine the appropriate Content-Type header for a JSON response + %% depending on the Accept header in the request. A request that explicitly + %% lists the correct JSON MIME type will get that type, otherwise the + %% response will have the generic MIME type "text/plain" + AcceptedTypes = case MochiReq:get_header_value("Accept") of + undefined -> []; + AcceptHeader -> string:tokens(AcceptHeader, ", ") + end, + case lists:member("application/json", AcceptedTypes) of + true -> "application/json"; + false -> "text/plain; charset=utf-8" + end. + +server_header() -> + [{"Server", "CouchDB/" ++ couch_server:get_version() ++ + " (Erlang OTP/" ++ erlang:system_info(otp_release) ++ ")"}]. + + +-record(mp, {boundary, buffer, data_fun, callback}). + + +parse_multipart_request(ContentType, DataFun, Callback) -> + Boundary0 = iolist_to_binary(get_boundary(ContentType)), + Boundary = <<"\r\n--", Boundary0/binary>>, + Mp = #mp{boundary= Boundary, + buffer= <<>>, + data_fun=DataFun, + callback=Callback}, + {Mp2, _NilCallback} = read_until(Mp, <<"--", Boundary0/binary>>, + fun nil_callback/1), + #mp{buffer=Buffer, data_fun=DataFun2, callback=Callback2} = + parse_part_header(Mp2), + {Buffer, DataFun2, Callback2}. + +nil_callback(_Data)-> + fun nil_callback/1. + +get_boundary({"multipart/" ++ _, Opts}) -> + case couch_util:get_value("boundary", Opts) of + S when is_list(S) -> + S + end; +get_boundary(ContentType) -> + {"multipart/" ++ _ , Opts} = mochiweb_util:parse_header(ContentType), + get_boundary({"multipart/", Opts}). + + + +split_header(<<>>) -> + []; +split_header(Line) -> + {Name, [$: | Value]} = lists:splitwith(fun (C) -> C =/= $: end, + binary_to_list(Line)), + [{string:to_lower(string:strip(Name)), + mochiweb_util:parse_header(Value)}]. + +read_until(#mp{data_fun=DataFun, buffer=Buffer}=Mp, Pattern, Callback) -> + case find_in_binary(Pattern, Buffer) of + not_found -> + Callback2 = Callback(Buffer), + {Buffer2, DataFun2} = DataFun(), + Buffer3 = iolist_to_binary(Buffer2), + read_until(Mp#mp{data_fun=DataFun2,buffer=Buffer3}, Pattern, Callback2); + {partial, 0} -> + {NewData, DataFun2} = DataFun(), + read_until(Mp#mp{data_fun=DataFun2, + buffer= iolist_to_binary([Buffer,NewData])}, + Pattern, Callback); + {partial, Skip} -> + <> = Buffer, + Callback2 = Callback(DataChunk), + {NewData, DataFun2} = DataFun(), + read_until(Mp#mp{data_fun=DataFun2, + buffer= iolist_to_binary([Rest | NewData])}, + Pattern, Callback2); + {exact, 0} -> + PatternLen = size(Pattern), + <<_:PatternLen/binary, Rest/binary>> = Buffer, + {Mp#mp{buffer= Rest}, Callback}; + {exact, Skip} -> + PatternLen = size(Pattern), + <> = Buffer, + Callback2 = Callback(DataChunk), + {Mp#mp{buffer= Rest}, Callback2} + end. + + +parse_part_header(#mp{callback=UserCallBack}=Mp) -> + {Mp2, AccCallback} = read_until(Mp, <<"\r\n\r\n">>, + fun(Next) -> acc_callback(Next, []) end), + HeaderData = AccCallback(get_data), + + Headers = + lists:foldl(fun(Line, Acc) -> + split_header(Line) ++ Acc + end, [], re:split(HeaderData,<<"\r\n">>, [])), + NextCallback = UserCallBack({headers, Headers}), + parse_part_body(Mp2#mp{callback=NextCallback}). + +parse_part_body(#mp{boundary=Prefix, callback=Callback}=Mp) -> + {Mp2, WrappedCallback} = read_until(Mp, Prefix, + fun(Data) -> body_callback_wrapper(Data, Callback) end), + Callback2 = WrappedCallback(get_callback), + Callback3 = Callback2(body_end), + case check_for_last(Mp2#mp{callback=Callback3}) of + {last, #mp{callback=Callback3}=Mp3} -> + Mp3#mp{callback=Callback3(eof)}; + {more, Mp3} -> + parse_part_header(Mp3) + end. + +acc_callback(get_data, Acc)-> + iolist_to_binary(lists:reverse(Acc)); +acc_callback(Data, Acc)-> + fun(Next) -> acc_callback(Next, [Data | Acc]) end. + +body_callback_wrapper(get_callback, Callback) -> + Callback; +body_callback_wrapper(Data, Callback) -> + Callback2 = Callback({body, Data}), + fun(Next) -> body_callback_wrapper(Next, Callback2) end. + + +check_for_last(#mp{buffer=Buffer, data_fun=DataFun}=Mp) -> + case Buffer of + <<"--",_/binary>> -> {last, Mp}; + <<_, _, _/binary>> -> {more, Mp}; + _ -> % not long enough + {Data, DataFun2} = DataFun(), + check_for_last(Mp#mp{buffer= <>, + data_fun = DataFun2}) + end. + +find_in_binary(_B, <<>>) -> + not_found; + +find_in_binary(B, Data) -> + case binary:match(Data, [B], []) of + nomatch -> + partial_find(binary:part(B, {0, byte_size(B) - 1}), + binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), 1); + {Pos, _Len} -> + {exact, Pos} + end. + +partial_find(<<>>, _Data, _Pos) -> + not_found; + +partial_find(B, Data, N) when byte_size(Data) > 0 -> + case binary:match(Data, [B], []) of + nomatch -> + partial_find(binary:part(B, {0, byte_size(B) - 1}), + binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), N + 1); + {Pos, _Len} -> + {partial, N + Pos} + end; + +partial_find(_B, _Data, _N) -> + not_found. + + +validate_bind_address(Address) -> + case inet_parse:address(Address) of + {ok, _} -> ok; + _ -> throw({error, invalid_bind_address}) + end. http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/couch/src/couch_httpd_auth.erl ---------------------------------------------------------------------- diff --git a/apps/couch/src/couch_httpd_auth.erl b/apps/couch/src/couch_httpd_auth.erl new file mode 100644 index 0000000..b8c4e26 --- /dev/null +++ b/apps/couch/src/couch_httpd_auth.erl @@ -0,0 +1,380 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_httpd_auth). +-include("couch_db.hrl"). + +-export([default_authentication_handler/1,special_test_authentication_handler/1]). +-export([cookie_authentication_handler/1]). +-export([null_authentication_handler/1]). +-export([proxy_authentication_handler/1, proxy_authentification_handler/1]). +-export([cookie_auth_header/2]). +-export([handle_session_req/1]). + +-import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]). + +special_test_authentication_handler(Req) -> + case header_value(Req, "WWW-Authenticate") of + "X-Couch-Test-Auth " ++ NamePass -> + % NamePass is a colon separated string: "joe schmoe:a password". + [Name, Pass] = re:split(NamePass, ":", [{return, list}, {parts, 2}]), + case {Name, Pass} of + {"Jan Lehnardt", "apple"} -> ok; + {"Christopher Lenz", "dog food"} -> ok; + {"Noah Slater", "biggiesmalls endian"} -> ok; + {"Chris Anderson", "mp3"} -> ok; + {"Damien Katz", "pecan pie"} -> ok; + {_, _} -> + throw({unauthorized, <<"Name or password is incorrect.">>}) + end, + Req#httpd{user_ctx=#user_ctx{name=?l2b(Name)}}; + _ -> + % No X-Couch-Test-Auth credentials sent, give admin access so the + % previous authentication can be restored after the test + Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}} + end. + +basic_name_pw(Req) -> + AuthorizationHeader = header_value(Req, "Authorization"), + case AuthorizationHeader of + "Basic " ++ Base64Value -> + case re:split(base64:decode(Base64Value), ":", + [{return, list}, {parts, 2}]) of + ["_", "_"] -> + % special name and pass to be logged out + nil; + [User, Pass] -> + {User, Pass}; + _ -> + nil + end; + _ -> + nil + end. + +default_authentication_handler(Req) -> + case basic_name_pw(Req) of + {User, Pass} -> + case couch_auth_cache:get_user_creds(User) of + nil -> + throw({unauthorized, <<"Name or password is incorrect.">>}); + UserProps -> + case authenticate(?l2b(Pass), UserProps) of + true -> + Req#httpd{user_ctx=#user_ctx{ + name=?l2b(User), + roles=couch_util:get_value(<<"roles">>, UserProps, []) + }}; + _Else -> + throw({unauthorized, <<"Name or password is incorrect.">>}) + end + end; + nil -> + case couch_server:has_admins() of + true -> + Req; + false -> + case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of + "true" -> Req; + % If no admins, and no user required, then everyone is admin! + % Yay, admin party! + _ -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}} + end + end + end. + +null_authentication_handler(Req) -> + Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}. + +%% @doc proxy auth handler. +% +% This handler allows creation of a userCtx object from a user authenticated remotly. +% The client just pass specific headers to CouchDB and the handler create the userCtx. +% Headers name can be defined in local.ini. By thefault they are : +% +% * X-Auth-CouchDB-UserName : contain the username, (x_auth_username in +% couch_httpd_auth section) +% * X-Auth-CouchDB-Roles : contain the user roles, list of roles separated by a +% comma (x_auth_roles in couch_httpd_auth section) +% * X-Auth-CouchDB-Token : token to authenticate the authorization (x_auth_token +% in couch_httpd_auth section). This token is an hmac-sha1 created from secret key +% and username. The secret key should be the same in the client and couchdb node. s +% ecret key is the secret key in couch_httpd_auth section of ini. This token is optional +% if value of proxy_use_secret key in couch_httpd_auth section of ini isn't true. +% +proxy_authentication_handler(Req) -> + case proxy_auth_user(Req) of + nil -> Req; + Req2 -> Req2 + end. + +%% @deprecated +proxy_authentification_handler(Req) -> + proxy_authentication_handler(Req). + +proxy_auth_user(Req) -> + XHeaderUserName = couch_config:get("couch_httpd_auth", "x_auth_username", + "X-Auth-CouchDB-UserName"), + XHeaderRoles = couch_config:get("couch_httpd_auth", "x_auth_roles", + "X-Auth-CouchDB-Roles"), + XHeaderToken = couch_config:get("couch_httpd_auth", "x_auth_token", + "X-Auth-CouchDB-Token"), + case header_value(Req, XHeaderUserName) of + undefined -> nil; + UserName -> + Roles = case header_value(Req, XHeaderRoles) of + undefined -> []; + Else -> + [?l2b(R) || R <- string:tokens(Else, ",")] + end, + case couch_config:get("couch_httpd_auth", "proxy_use_secret", "false") of + "true" -> + case couch_config:get("couch_httpd_auth", "secret", nil) of + nil -> + Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName), roles=Roles}}; + Secret -> + ExpectedToken = couch_util:to_hex(crypto:sha_mac(Secret, UserName)), + case header_value(Req, XHeaderToken) of + Token when Token == ExpectedToken -> + Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName), + roles=Roles}}; + _ -> nil + end + end; + _ -> + Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName), roles=Roles}} + end + end. + + +cookie_authentication_handler(#httpd{mochi_req=MochiReq}=Req) -> + case MochiReq:get_cookie_value("AuthSession") of + undefined -> Req; + [] -> Req; + Cookie -> + [User, TimeStr, HashStr] = try + AuthSession = couch_util:decodeBase64Url(Cookie), + [_A, _B, _Cs] = re:split(?b2l(AuthSession), ":", + [{return, list}, {parts, 3}]) + catch + _:_Error -> + Reason = <<"Malformed AuthSession cookie. Please clear your cookies.">>, + throw({bad_request, Reason}) + end, + % Verify expiry and hash + CurrentTime = make_cookie_time(), + case couch_config:get("couch_httpd_auth", "secret", nil) of + nil -> + ?LOG_DEBUG("cookie auth secret is not set",[]), + Req; + SecretStr -> + Secret = ?l2b(SecretStr), + case couch_auth_cache:get_user_creds(User) of + nil -> Req; + UserProps -> + UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<"">>), + FullSecret = <>, + ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr), + Hash = ?l2b(HashStr), + Timeout = list_to_integer( + couch_config:get("couch_httpd_auth", "timeout", "600")), + ?LOG_DEBUG("timeout ~p", [Timeout]), + case (catch erlang:list_to_integer(TimeStr, 16)) of + TimeStamp when CurrentTime < TimeStamp + Timeout -> + case couch_passwords:verify(ExpectedHash, Hash) of + true -> + TimeLeft = TimeStamp + Timeout - CurrentTime, + ?LOG_DEBUG("Successful cookie auth as: ~p", [User]), + Req#httpd{user_ctx=#user_ctx{ + name=?l2b(User), + roles=couch_util:get_value(<<"roles">>, UserProps, []) + }, auth={FullSecret, TimeLeft < Timeout*0.9}}; + _Else -> + Req + end; + _Else -> + Req + end + end + end + end. + +cookie_auth_header(#httpd{user_ctx=#user_ctx{name=null}}, _Headers) -> []; +cookie_auth_header(#httpd{user_ctx=#user_ctx{name=User}, auth={Secret, true}}=Req, Headers) -> + % Note: we only set the AuthSession cookie if: + % * a valid AuthSession cookie has been received + % * we are outside a 10% timeout window + % * and if an AuthSession cookie hasn't already been set e.g. by a login + % or logout handler. + % The login and logout handlers need to set the AuthSession cookie + % themselves. + CookieHeader = couch_util:get_value("Set-Cookie", Headers, ""), + Cookies = mochiweb_cookies:parse_cookie(CookieHeader), + AuthSession = couch_util:get_value("AuthSession", Cookies), + if AuthSession == undefined -> + TimeStamp = make_cookie_time(), + [cookie_auth_cookie(Req, ?b2l(User), Secret, TimeStamp)]; + true -> + [] + end; +cookie_auth_header(_Req, _Headers) -> []. + +cookie_auth_cookie(Req, User, Secret, TimeStamp) -> + SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16), + Hash = crypto:sha_mac(Secret, SessionData), + mochiweb_cookies:cookie("AuthSession", + couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)), + [{path, "/"}] ++ cookie_scheme(Req) ++ max_age()). + +ensure_cookie_auth_secret() -> + case couch_config:get("couch_httpd_auth", "secret", nil) of + nil -> + NewSecret = ?b2l(couch_uuids:random()), + couch_config:set("couch_httpd_auth", "secret", NewSecret), + NewSecret; + Secret -> Secret + end. + +% session handlers +% Login handler with user db +handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) -> + ReqBody = MochiReq:recv_body(), + Form = case MochiReq:get_primary_header_value("content-type") of + % content type should be json + "application/x-www-form-urlencoded" ++ _ -> + mochiweb_util:parse_qs(ReqBody); + "application/json" ++ _ -> + {Pairs} = ?JSON_DECODE(ReqBody), + lists:map(fun({Key, Value}) -> + {?b2l(Key), ?b2l(Value)} + end, Pairs); + _ -> + [] + end, + UserName = ?l2b(couch_util:get_value("name", Form, "")), + Password = ?l2b(couch_util:get_value("password", Form, "")), + ?LOG_DEBUG("Attempt Login: ~s",[UserName]), + User = case couch_auth_cache:get_user_creds(UserName) of + nil -> []; + Result -> Result + end, + UserSalt = couch_util:get_value(<<"salt">>, User, <<>>), + case authenticate(Password, User) of + true -> + % setup the session cookie + Secret = ?l2b(ensure_cookie_auth_secret()), + CurrentTime = make_cookie_time(), + Cookie = cookie_auth_cookie(Req, ?b2l(UserName), <>, CurrentTime), + % TODO document the "next" feature in Futon + {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of + nil -> + {200, [Cookie]}; + Redirect -> + {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]} + end, + send_json(Req#httpd{req_body=ReqBody}, Code, Headers, + {[ + {ok, true}, + {name, couch_util:get_value(<<"name">>, User, null)}, + {roles, couch_util:get_value(<<"roles">>, User, [])} + ]}); + _Else -> + % clear the session + Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}] ++ cookie_scheme(Req)), + {Code, Headers} = case couch_httpd:qs_value(Req, "fail", nil) of + nil -> + {401, [Cookie]}; + Redirect -> + {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]} + end, + send_json(Req, Code, Headers, {[{error, <<"unauthorized">>},{reason, <<"Name or password is incorrect.">>}]}) + end; +% get user info +% GET /_session +handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) -> + Name = UserCtx#user_ctx.name, + ForceLogin = couch_httpd:qs_value(Req, "basic", "false"), + case {Name, ForceLogin} of + {null, "true"} -> + throw({unauthorized, <<"Please login.">>}); + {Name, _} -> + send_json(Req, {[ + % remove this ok + {ok, true}, + {<<"userCtx">>, {[ + {name, Name}, + {roles, UserCtx#user_ctx.roles} + ]}}, + {info, {[ + {authentication_db, ?l2b(couch_config:get("couch_httpd_auth", "authentication_db"))}, + {authentication_handlers, [auth_name(H) || H <- couch_httpd:make_fun_spec_strs( + couch_config:get("httpd", "authentication_handlers"))]} + ] ++ maybe_value(authenticated, UserCtx#user_ctx.handler, fun(Handler) -> + auth_name(?b2l(Handler)) + end)}} + ]}) + end; +% logout by deleting the session +handle_session_req(#httpd{method='DELETE'}=Req) -> + Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}] ++ cookie_scheme(Req)), + {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of + nil -> + {200, [Cookie]}; + Redirect -> + {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]} + end, + send_json(Req, Code, Headers, {[{ok, true}]}); +handle_session_req(Req) -> + send_method_not_allowed(Req, "GET,HEAD,POST,DELETE"). + +maybe_value(_Key, undefined, _Fun) -> []; +maybe_value(Key, Else, Fun) -> + [{Key, Fun(Else)}]. + +authenticate(Pass, UserProps) -> + UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<>>), + {PasswordHash, ExpectedHash} = + case couch_util:get_value(<<"password_scheme">>, UserProps, <<"simple">>) of + <<"simple">> -> + {couch_passwords:simple(Pass, UserSalt), + couch_util:get_value(<<"password_sha">>, UserProps, nil)}; + <<"pbkdf2">> -> + Iterations = couch_util:get_value(<<"iterations">>, UserProps, 10000), + {couch_passwords:pbkdf2(Pass, UserSalt, Iterations), + couch_util:get_value(<<"derived_key">>, UserProps, nil)} + end, + couch_passwords:verify(PasswordHash, ExpectedHash). + +auth_name(String) when is_list(String) -> + [_,_,_,_,_,Name|_] = re:split(String, "[\\W_]", [{return, list}]), + ?l2b(Name). + +make_cookie_time() -> + {NowMS, NowS, _} = erlang:now(), + NowMS * 1000000 + NowS. + +cookie_scheme(#httpd{mochi_req=MochiReq}) -> + [{http_only, true}] ++ + case MochiReq:get(scheme) of + http -> []; + https -> [{secure, true}] + end. + +max_age() -> + case couch_config:get("couch_httpd_auth", "allow_persistent_cookies", "false") of + "false" -> + []; + "true" -> + Timeout = list_to_integer( + couch_config:get("couch_httpd_auth", "timeout", "600")), + [{max_age, Timeout}] + end. http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/couch/src/couch_httpd_cors.erl ---------------------------------------------------------------------- diff --git a/apps/couch/src/couch_httpd_cors.erl b/apps/couch/src/couch_httpd_cors.erl new file mode 100644 index 0000000..d9462d1 --- /dev/null +++ b/apps/couch/src/couch_httpd_cors.erl @@ -0,0 +1,351 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +%% @doc module to handle Cross-Origin Resource Sharing +%% +%% This module handles CORS requests and preflight request for +%% CouchDB. The configuration is done in the ini file. +%% +%% This implements http://www.w3.org/TR/cors/ + + +-module(couch_httpd_cors). + +-include("couch_db.hrl"). + +-export([is_preflight_request/1, cors_headers/2]). + +-define(SUPPORTED_HEADERS, "Accept, Accept-Language, Content-Type," ++ + "Expires, Last-Modified, Pragma, Origin, Content-Length," ++ + "If-Match, Destination, X-Requested-With, " ++ + "X-Http-Method-Override, Content-Range"). + +-define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE," ++ + "TRACE, CONNECT, COPY, OPTIONS"). + +% as defined in http://www.w3.org/TR/cors/#terminology +-define(SIMPLE_HEADERS, ["Cache-Control", "Content-Language", + "Content-Type", "Expires", "Last-Modified", "Pragma"]). +-define(ALLOWED_HEADERS, lists:sort(["Server", "Etag", + "Accept-Ranges" | ?SIMPLE_HEADERS])). +-define(SIMPLE_CONTENT_TYPE_VALUES, ["application/x-www-form-urlencoded", + "multipart/form-data", "text/plain"]). + +% TODO: - pick a sane default +-define(CORS_DEFAULT_MAX_AGE, 12345). + +%% is_preflight_request/1 + +% http://www.w3.org/TR/cors/#resource-preflight-requests + +is_preflight_request(#httpd{method=Method}=Req) when Method /= 'OPTIONS' -> + Req; +is_preflight_request(Req) -> + EnableCors = enable_cors(), + is_preflight_request(Req, EnableCors). + +is_preflight_request(Req, false) -> + Req; +is_preflight_request(#httpd{mochi_req=MochiReq}=Req, true) -> + case preflight_request(MochiReq) of + {ok, PreflightHeaders} -> + send_preflight_response(Req, PreflightHeaders); + _ -> + Req + end. + + +preflight_request(MochiReq) -> + Origin = MochiReq:get_header_value("Origin"), + preflight_request(MochiReq, Origin). + +preflight_request(MochiReq, undefined) -> + % If the Origin header is not present terminate this set of + % steps. The request is outside the scope of this specification. + % http://www.w3.org/TR/cors/#resource-preflight-requests + MochiReq; +preflight_request(MochiReq, Origin) -> + Host = couch_httpd_vhost:host(MochiReq), + AcceptedOrigins = get_accepted_origins(Host), + AcceptAll = lists:member("*", AcceptedOrigins), + + HandlerFun = fun() -> + OriginList = couch_util:to_list(Origin), + handle_preflight_request(OriginList, Host, MochiReq) + end, + + case AcceptAll of + true -> + % Always matching is acceptable since the list of + % origins can be unbounded. + % http://www.w3.org/TR/cors/#resource-preflight-requests + HandlerFun(); + false -> + case lists:member(Origin, AcceptedOrigins) of + % The Origin header can only contain a single origin as + % the user agent will not follow redirects. + % http://www.w3.org/TR/cors/#resource-preflight-requests + % TODO: Square against multi origin thinger in Security Considerations + true -> + HandlerFun(); + false -> + % If the value of the Origin header is not a + % case-sensitive match for any of the values + % in list of origins do not set any additional + % headers and terminate this set of steps. + % http://www.w3.org/TR/cors/#resource-preflight-requests + false + end + end. + + +handle_preflight_request(Origin, Host, MochiReq) -> + %% get supported methods + SupportedMethods = split_list(cors_config(Host, "methods", + ?SUPPORTED_METHODS)), + + % get supported headers + AllSupportedHeaders = split_list(cors_config(Host, "headers", + ?SUPPORTED_HEADERS)), + + SupportedHeaders = [string:to_lower(H) || H <- AllSupportedHeaders], + + % get max age + MaxAge = cors_config(Host, "max_age", ?CORS_DEFAULT_MAX_AGE), + + PreflightHeaders0 = maybe_add_credentials(Origin, Host, [ + {"Access-Control-Allow-Origin", Origin}, + {"Access-Control-Max-Age", MaxAge}, + {"Access-Control-Allow-Methods", + string:join(SupportedMethods, ", ")}]), + + case MochiReq:get_header_value("Access-Control-Request-Method") of + undefined -> + % If there is no Access-Control-Request-Method header + % or if parsing failed, do not set any additional headers + % and terminate this set of steps. The request is outside + % the scope of this specification. + % http://www.w3.org/TR/cors/#resource-preflight-requests + {ok, PreflightHeaders0}; + Method -> + case lists:member(Method, SupportedMethods) of + true -> + % method ok , check headers + AccessHeaders = MochiReq:get_header_value( + "Access-Control-Request-Headers"), + {FinalReqHeaders, ReqHeaders} = case AccessHeaders of + undefined -> {"", []}; + Headers -> + % transform header list in something we + % could check. make sure everything is a + % list + RH = [string:to_lower(H) + || H <- split_headers(Headers)], + {Headers, RH} + end, + % check if headers are supported + case ReqHeaders -- SupportedHeaders of + [] -> + PreflightHeaders = PreflightHeaders0 ++ + [{"Access-Control-Allow-Headers", + FinalReqHeaders}], + {ok, PreflightHeaders}; + _ -> + false + end; + false -> + % If method is not a case-sensitive match for any of + % the values in list of methods do not set any additional + % headers and terminate this set of steps. + % http://www.w3.org/TR/cors/#resource-preflight-requests + false + end + end. + + +send_preflight_response(#httpd{mochi_req=MochiReq}=Req, Headers) -> + couch_httpd:log_request(Req, 204), + couch_stats_collector:increment({httpd_status_codes, 204}), + Headers1 = couch_httpd:http_1_0_keep_alive(MochiReq, Headers), + Headers2 = Headers1 ++ couch_httpd:server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers1), + {ok, MochiReq:respond({204, Headers2, <<>>})}. + + +% cors_headers/1 + +cors_headers(MochiReq, RequestHeaders) -> + EnableCors = enable_cors(), + CorsHeaders = do_cors_headers(MochiReq, EnableCors), + maybe_apply_cors_headers(CorsHeaders, RequestHeaders). + +do_cors_headers(#httpd{mochi_req=MochiReq}, true) -> + Host = couch_httpd_vhost:host(MochiReq), + AcceptedOrigins = get_accepted_origins(Host), + case MochiReq:get_header_value("Origin") of + undefined -> + % If the Origin header is not present terminate + % this set of steps. The request is outside the scope + % of this specification. + % http://www.w3.org/TR/cors/#resource-processing-model + []; + Origin -> + handle_cors_headers(couch_util:to_list(Origin), + Host, AcceptedOrigins) + end; +do_cors_headers(_MochiReq, false) -> + []. + +maybe_apply_cors_headers([], RequestHeaders) -> + RequestHeaders; +maybe_apply_cors_headers(CorsHeaders, RequestHeaders0) -> + % for each RequestHeader that isn't in SimpleHeaders, + % (or Content-Type with SIMPLE_CONTENT_TYPE_VALUES) + % append to Access-Control-Expose-Headers + % return: RequestHeaders ++ CorsHeaders ++ ACEH + + RequestHeaders = [K || {K,_V} <- RequestHeaders0], + ExposedHeaders0 = reduce_headers(RequestHeaders, ?ALLOWED_HEADERS), + + % here we may have not moved Content-Type into ExposedHeaders, + % now we need to check whether the Content-Type valus is + % in ?SIMPLE_CONTENT_TYPE_VALUES and if it isn’t add Content- + % Type to to ExposedHeaders + ContentType = proplists:get_value("Content-Type", RequestHeaders0), + IncludeContentType = case ContentType of + undefined -> + false; + _ -> + ContentType_ = string:to_lower(ContentType), + lists:member(ContentType_, ?SIMPLE_CONTENT_TYPE_VALUES) + end, + ExposedHeaders = case IncludeContentType of + false -> + lists:umerge(ExposedHeaders0, ["Content-Type"]); + true -> + ExposedHeaders0 + end, + CorsHeaders + ++ RequestHeaders0 + ++ [{"Access-Control-Expose-Headers", + string:join(ExposedHeaders, ", ")}]. + + +reduce_headers(A, B) -> + reduce_headers0(A, B, []). + +reduce_headers0([], _B, Result) -> + lists:sort(Result); +reduce_headers0([ElmA|RestA], B, Result) -> + R = case member_nocase(ElmA, B) of + false -> Result; + _Else -> [ElmA | Result] + end, + reduce_headers0(RestA, B, R). + +member_nocase(ElmA, List) -> + lists:any(fun(ElmB) -> + string:to_lower(ElmA) =:= string:to_lower(ElmB) + end, List). + +handle_cors_headers(_Origin, _Host, []) -> + []; +handle_cors_headers(Origin, Host, AcceptedOrigins) -> + AcceptAll = lists:member("*", AcceptedOrigins), + case {AcceptAll, lists:member(Origin, AcceptedOrigins)} of + {true, _} -> + make_cors_header(Origin, Host); + {false, true} -> + make_cors_header(Origin, Host); + _ -> + % If the value of the Origin header is not a + % case-sensitive match for any of the values + % in list of origins, do not set any additional + % headers and terminate this set of steps. + % http://www.w3.org/TR/cors/#resource-requests + [] + end. + + +make_cors_header(Origin, Host) -> + Headers = [{"Access-Control-Allow-Origin", Origin}], + maybe_add_credentials(Origin, Host, Headers). + + +%% util + +maybe_add_credentials(Origin, Host, Headers) -> + maybe_add_credentials(Headers, allow_credentials(Origin, Host)). + +maybe_add_credentials(Headers, false) -> + Headers; +maybe_add_credentials(Headers, true) -> + Headers ++ [{"Access-Control-Allow-Credentials", "true"}]. + + +allow_credentials("*", _Host) -> + false; +allow_credentials(_Origin, Host) -> + Default = get_bool_config("cors", "credentials", false), + get_bool_config(cors_section(Host), "credentials", Default). + + + +cors_config(Host, Key, Default) -> + couch_config:get(cors_section(Host), Key, + couch_config:get("cors", Key, Default)). + +cors_section(Host0) -> + {Host, _Port} = split_host_port(Host0), + "cors:" ++ Host. + +enable_cors() -> + get_bool_config("httpd", "enable_cors", false). + +get_bool_config(Section, Key, Default) -> + case couch_config:get(Section, Key) of + undefined -> + Default; + "true" -> + true; + "false" -> + false + end. + +get_accepted_origins(Host) -> + split_list(cors_config(Host, "origins", [])). + +split_list(S) -> + re:split(S, "\\s*,\\s*", [trim, {return, list}]). + +split_headers(H) -> + re:split(H, ",\\s*", [{return,list}, trim]). + +split_host_port(HostAsString) -> + % split at semicolon ":" + Split = string:rchr(HostAsString, $:), + split_host_port(HostAsString, Split). + +split_host_port(HostAsString, 0) -> + % no semicolon + {HostAsString, '*'}; +split_host_port(HostAsString, N) -> + HostPart = string:substr(HostAsString, 1, N-1), + % parse out port + % is there a nicer way? + case (catch erlang:list_to_integer(string:substr(HostAsString, + N+1, length(HostAsString)))) of + {'EXIT', _} -> + {HostAsString, '*'}; + Port -> + {HostPart, Port} + end.