You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2014/02/06 18:40:05 UTC

[23/50] [abbrv] inital move to rebar compilation

http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/a6816bff/src/couch_file.erl
----------------------------------------------------------------------
diff --git a/src/couch_file.erl b/src/couch_file.erl
new file mode 100644
index 0000000..ee5dafb
--- /dev/null
+++ b/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 = <<Md5/binary, Bin/binary>>,
+    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,
+    <<Prefix:1/integer, Len:31/integer, RestRawData/binary>> =
+        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 ->
+        <<RawBin:TotalBytes/binary, _/binary>> = RestBlock;
+    true ->
+        {ok, Missing} = file:pread(
+            Fd, (Block * ?SIZE_BLOCK) + 5 + byte_size(RestBlock),
+            TotalBytes - byte_size(RestBlock)),
+        RawBin = <<RestBlock/binary, Missing/binary>>
+    end,
+    <<Md5Sig:16/binary, HeaderBin/binary>> =
+        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) ->
+    <<Data:DataSize/binary, _/binary>> = 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, <<RawBin:TotalBytes/binary>>} = 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 ->
+        <<DataBlock:BlockBytesAvailable/binary,Rest/binary>> = 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([<<Bin/binary>> | Rest], SplitAt, BeginAcc) when SplitAt > byte_size(Bin) ->
+    split_iolist(Rest, SplitAt - byte_size(Bin), [Bin | BeginAcc]);
+split_iolist([<<Bin/binary>> | Rest], SplitAt, BeginAcc) ->
+    <<Begin:SplitAt/binary,End/binary>> = 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-couch/blob/a6816bff/src/couch_httpd.erl
----------------------------------------------------------------------
diff --git a/src/couch_httpd.erl b/src/couch_httpd.erl
new file mode 100644
index 0000000..28932ba
--- /dev/null
+++ b/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) ->
+    <<SigInt:128/integer>> = 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} ->
+        <<DataChunk:Skip/binary, Rest/binary>> = 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),
+        <<DataChunk:Skip/binary, _:PatternLen/binary, Rest/binary>> = 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= <<Buffer/binary, Data/binary>>,
+                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-couch/blob/a6816bff/src/couch_httpd_auth.erl
----------------------------------------------------------------------
diff --git a/src/couch_httpd_auth.erl b/src/couch_httpd_auth.erl
new file mode 100644
index 0000000..b8c4e26
--- /dev/null
+++ b/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 = <<Secret/binary, UserSalt/binary>>,
+                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), <<Secret/binary, UserSalt/binary>>, 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-couch/blob/a6816bff/src/couch_httpd_cors.erl
----------------------------------------------------------------------
diff --git a/src/couch_httpd_cors.erl b/src/couch_httpd_cors.erl
new file mode 100644
index 0000000..d9462d1
--- /dev/null
+++ b/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.