diff options
author | Jan Lehnardt <jan@apache.org> | 2018-09-15 13:17:17 +0200 |
---|---|---|
committer | Jan Lehnardt <jan@apache.org> | 2018-11-09 14:51:33 +0100 |
commit | aba14001c740df3c15d4d4cebfd8a759c3f0b284 (patch) | |
tree | b113cbd016df0740ef53e153f1dc9f023f9937cc | |
parent | c357ff395339bc81eedfd41817054db1de0b03de (diff) | |
download | couchdb-aba14001c740df3c15d4d4cebfd8a759c3f0b284.tar.gz |
remove deprecated http proxy code and tests from couch_httpd
-rw-r--r-- | src/couch/src/couch_httpd_proxy.erl | 428 | ||||
-rw-r--r-- | src/couch/test/couchdb_http_proxy_tests.erl | 456 |
2 files changed, 0 insertions, 884 deletions
diff --git a/src/couch/src/couch_httpd_proxy.erl b/src/couch/src/couch_httpd_proxy.erl deleted file mode 100644 index d2c7acc3a..000000000 --- a/src/couch/src/couch_httpd_proxy.erl +++ /dev/null @@ -1,428 +0,0 @@ -% 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_proxy). - --compile(tuple_calls). - --export([handle_proxy_req/2]). - --include_lib("couch/include/couch_db.hrl"). --include_lib("ibrowse/include/ibrowse.hrl"). - --define(TIMEOUT, infinity). --define(PKT_SIZE, 4096). - - -handle_proxy_req(Req, ProxyDest) -> - Method = get_method(Req), - Url = get_url(Req, ProxyDest), - Version = get_version(Req), - Headers = get_headers(Req), - Body = get_body(Req), - Options = [ - {http_vsn, Version}, - {headers_as_is, true}, - {response_format, binary}, - {stream_to, {self(), once}} - ], - case ibrowse:send_req(Url, Headers, Method, Body, Options, ?TIMEOUT) of - {ibrowse_req_id, ReqId} -> - stream_response(Req, ProxyDest, ReqId); - {error, Reason} -> - throw({error, Reason}) - end. - - -get_method(#httpd{mochi_req=MochiReq}) -> - case MochiReq:get(method) of - Method when is_atom(Method) -> - list_to_atom(string:to_lower(atom_to_list(Method))); - Method when is_list(Method) -> - list_to_atom(string:to_lower(Method)); - Method when is_binary(Method) -> - list_to_atom(string:to_lower(?b2l(Method))) - end. - - -get_url(Req, ProxyDest) when is_binary(ProxyDest) -> - get_url(Req, ?b2l(ProxyDest)); -get_url(#httpd{mochi_req=MochiReq}=Req, ProxyDest) -> - BaseUrl = case mochiweb_util:partition(ProxyDest, "/") of - {[], "/", _} -> couch_httpd:absolute_uri(Req, ProxyDest); - _ -> ProxyDest - end, - ProxyPrefix = "/" ++ ?b2l(hd(Req#httpd.path_parts)), - RequestedPath = MochiReq:get(raw_path), - case mochiweb_util:partition(RequestedPath, ProxyPrefix) of - {[], ProxyPrefix, []} -> - BaseUrl; - {[], ProxyPrefix, [$/ | DestPath]} -> - remove_trailing_slash(BaseUrl) ++ "/" ++ DestPath; - {[], ProxyPrefix, DestPath} -> - remove_trailing_slash(BaseUrl) ++ "/" ++ DestPath; - _Else -> - throw({invalid_url_path, {ProxyPrefix, RequestedPath}}) - end. - -get_version(#httpd{mochi_req=MochiReq}) -> - MochiReq:get(version). - - -get_headers(#httpd{mochi_req=MochiReq}) -> - to_ibrowse_headers(mochiweb_headers:to_list(MochiReq:get(headers)), []). - -to_ibrowse_headers([], Acc) -> - lists:reverse(Acc); -to_ibrowse_headers([{K, V} | Rest], Acc) when is_atom(K) -> - to_ibrowse_headers([{atom_to_list(K), V} | Rest], Acc); -to_ibrowse_headers([{K, V} | Rest], Acc) when is_list(K) -> - case string:to_lower(K) of - "content-length" -> - to_ibrowse_headers(Rest, [{content_length, V} | Acc]); - % This appears to make ibrowse too smart. - %"transfer-encoding" -> - % to_ibrowse_headers(Rest, [{transfer_encoding, V} | Acc]); - _ -> - to_ibrowse_headers(Rest, [{K, V} | Acc]) - end. - -get_body(#httpd{method='GET'}) -> - fun() -> eof end; -get_body(#httpd{method='HEAD'}) -> - fun() -> eof end; -get_body(#httpd{method='DELETE'}) -> - fun() -> eof end; -get_body(#httpd{mochi_req=MochiReq}) -> - case MochiReq:get(body_length) of - undefined -> - <<>>; - {unknown_transfer_encoding, Unknown} -> - exit({unknown_transfer_encoding, Unknown}); - chunked -> - {fun stream_chunked_body/1, {init, MochiReq, 0}}; - 0 -> - <<>>; - Length when is_integer(Length) andalso Length > 0 -> - {fun stream_length_body/1, {init, MochiReq, Length}}; - Length -> - exit({invalid_body_length, Length}) - end. - - -remove_trailing_slash(Url) -> - rem_slash(lists:reverse(Url)). - -rem_slash([]) -> - []; -rem_slash([$\s | RevUrl]) -> - rem_slash(RevUrl); -rem_slash([$\t | RevUrl]) -> - rem_slash(RevUrl); -rem_slash([$\r | RevUrl]) -> - rem_slash(RevUrl); -rem_slash([$\n | RevUrl]) -> - rem_slash(RevUrl); -rem_slash([$/ | RevUrl]) -> - rem_slash(RevUrl); -rem_slash(RevUrl) -> - lists:reverse(RevUrl). - - -stream_chunked_body({init, MReq, 0}) -> - % First chunk, do expect-continue dance. - init_body_stream(MReq), - stream_chunked_body({stream, MReq, 0, [], ?PKT_SIZE}); -stream_chunked_body({stream, MReq, 0, Buf, BRem}) -> - % Finished a chunk, get next length. If next length - % is 0, its time to try and read trailers. - {CRem, Data} = read_chunk_length(MReq), - case CRem of - 0 -> - BodyData = lists:reverse(Buf, Data), - {ok, BodyData, {trailers, MReq, [], ?PKT_SIZE}}; - _ -> - stream_chunked_body( - {stream, MReq, CRem, [Data | Buf], BRem-size(Data)} - ) - end; -stream_chunked_body({stream, MReq, CRem, Buf, BRem}) when BRem =< 0 -> - % Time to empty our buffers to the upstream socket. - BodyData = lists:reverse(Buf), - {ok, BodyData, {stream, MReq, CRem, [], ?PKT_SIZE}}; -stream_chunked_body({stream, MReq, CRem, Buf, BRem}) -> - % Buffer some more data from the client. - Length = lists:min([CRem, BRem]), - Socket = MReq:get(socket), - NewState = case mochiweb_socket:recv(Socket, Length, ?TIMEOUT) of - {ok, Data} when size(Data) == CRem -> - case mochiweb_socket:recv(Socket, 2, ?TIMEOUT) of - {ok, <<"\r\n">>} -> - {stream, MReq, 0, [<<"\r\n">>, Data | Buf], BRem-Length-2}; - _ -> - exit(normal) - end; - {ok, Data} -> - {stream, MReq, CRem-Length, [Data | Buf], BRem-Length}; - _ -> - exit(normal) - end, - stream_chunked_body(NewState); -stream_chunked_body({trailers, MReq, Buf, BRem}) when BRem =< 0 -> - % Empty our buffers and send data upstream. - BodyData = lists:reverse(Buf), - {ok, BodyData, {trailers, MReq, [], ?PKT_SIZE}}; -stream_chunked_body({trailers, MReq, Buf, BRem}) -> - % Read another trailer into the buffer or stop on an - % empty line. - Socket = MReq:get(socket), - mochiweb_socket:setopts(Socket, [{packet, line}]), - case mochiweb_socket:recv(Socket, 0, ?TIMEOUT) of - {ok, <<"\r\n">>} -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), - BodyData = lists:reverse(Buf, <<"\r\n">>), - {ok, BodyData, eof}; - {ok, Footer} -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), - NewState = {trailers, MReq, [Footer | Buf], BRem-size(Footer)}, - stream_chunked_body(NewState); - _ -> - exit(normal) - end; -stream_chunked_body(eof) -> - % Tell ibrowse we're done sending data. - eof. - - -stream_length_body({init, MochiReq, Length}) -> - % Do the expect-continue dance - init_body_stream(MochiReq), - stream_length_body({stream, MochiReq, Length}); -stream_length_body({stream, _MochiReq, 0}) -> - % Finished streaming. - eof; -stream_length_body({stream, MochiReq, Length}) -> - BufLen = lists:min([Length, ?PKT_SIZE]), - case MochiReq:recv(BufLen) of - <<>> -> eof; - Bin -> {ok, Bin, {stream, MochiReq, Length-BufLen}} - end. - - -init_body_stream(MochiReq) -> - Expect = case MochiReq:get_header_value("expect") of - undefined -> - undefined; - Value when is_list(Value) -> - string:to_lower(Value) - end, - case Expect of - "100-continue" -> - MochiReq:start_raw_response({100, gb_trees:empty()}); - _Else -> - ok - end. - - -read_chunk_length(MochiReq) -> - Socket = MochiReq:get(socket), - mochiweb_socket:setopts(Socket, [{packet, line}]), - case mochiweb_socket:recv(Socket, 0, ?TIMEOUT) of - {ok, Header} -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), - Splitter = fun(C) -> - C =/= $\r andalso C =/= $\n andalso C =/= $\s - end, - {Hex, _Rest} = lists:splitwith(Splitter, ?b2l(Header)), - {mochihex:to_int(Hex), Header}; - _ -> - exit(normal) - end. - - -stream_response(Req, ProxyDest, ReqId) -> - receive - {ibrowse_async_headers, ReqId, "100", _} -> - % ibrowse doesn't handle 100 Continue responses which - % means we have to discard them so the proxy client - % doesn't get confused. - ibrowse:stream_next(ReqId), - stream_response(Req, ProxyDest, ReqId); - {ibrowse_async_headers, ReqId, Status, Headers} -> - {Source, Dest} = get_urls(Req, ProxyDest), - FixedHeaders = fix_headers(Source, Dest, Headers, []), - case body_length(FixedHeaders) of - chunked -> - {ok, Resp} = couch_httpd:start_chunked_response( - Req, list_to_integer(Status), FixedHeaders - ), - ibrowse:stream_next(ReqId), - stream_chunked_response(Req, ReqId, Resp), - {ok, Resp}; - Length when is_integer(Length) -> - {ok, Resp} = couch_httpd:start_response_length( - Req, list_to_integer(Status), FixedHeaders, Length - ), - ibrowse:stream_next(ReqId), - stream_length_response(Req, ReqId, Resp), - {ok, Resp}; - _ -> - {ok, Resp} = couch_httpd:start_response( - Req, list_to_integer(Status), FixedHeaders - ), - ibrowse:stream_next(ReqId), - stream_length_response(Req, ReqId, Resp), - % XXX: MochiWeb apparently doesn't look at the - % response to see if it must force close the - % connection. So we help it out here. - erlang:put(mochiweb_request_force_close, true), - {ok, Resp} - end - end. - - -stream_chunked_response(Req, ReqId, Resp) -> - receive - {ibrowse_async_response, ReqId, {error, Reason}} -> - throw({error, Reason}); - {ibrowse_async_response, ReqId, Chunk} -> - couch_httpd:send_chunk(Resp, Chunk), - ibrowse:stream_next(ReqId), - stream_chunked_response(Req, ReqId, Resp); - {ibrowse_async_response_end, ReqId} -> - couch_httpd:last_chunk(Resp) - end. - - -stream_length_response(Req, ReqId, Resp) -> - receive - {ibrowse_async_response, ReqId, {error, Reason}} -> - throw({error, Reason}); - {ibrowse_async_response, ReqId, Chunk} -> - couch_httpd:send(Resp, Chunk), - ibrowse:stream_next(ReqId), - stream_length_response(Req, ReqId, Resp); - {ibrowse_async_response_end, ReqId} -> - ok - end. - - -get_urls(Req, ProxyDest) -> - SourceUrl = couch_httpd:absolute_uri(Req, "/" ++ hd(Req#httpd.path_parts)), - Source = parse_url(?b2l(iolist_to_binary(SourceUrl))), - case (catch parse_url(ProxyDest)) of - Dest when is_record(Dest, url) -> - {Source, Dest}; - _ -> - DestUrl = couch_httpd:absolute_uri(Req, ProxyDest), - {Source, parse_url(DestUrl)} - end. - - -fix_headers(_, _, [], Acc) -> - lists:reverse(Acc); -fix_headers(Source, Dest, [{K, V} | Rest], Acc) -> - Fixed = case string:to_lower(K) of - "location" -> rewrite_location(Source, Dest, V); - "content-location" -> rewrite_location(Source, Dest, V); - "uri" -> rewrite_location(Source, Dest, V); - "destination" -> rewrite_location(Source, Dest, V); - "set-cookie" -> rewrite_cookie(Source, Dest, V); - _ -> V - end, - fix_headers(Source, Dest, Rest, [{K, Fixed} | Acc]). - - -rewrite_location(Source, #url{host=Host, port=Port, protocol=Proto}, Url) -> - case (catch parse_url(Url)) of - #url{host=Host, port=Port, protocol=Proto} = Location -> - DestLoc = #url{ - protocol=Source#url.protocol, - host=Source#url.host, - port=Source#url.port, - path=join_url_path(Source#url.path, Location#url.path) - }, - url_to_url(DestLoc); - #url{} -> - Url; - _ -> - url_to_url(Source#url{path=join_url_path(Source#url.path, Url)}) - end. - - -rewrite_cookie(_Source, _Dest, Cookie) -> - Cookie. - - -parse_url(Url) when is_binary(Url) -> - ibrowse_lib:parse_url(?b2l(Url)); -parse_url(Url) when is_list(Url) -> - ibrowse_lib:parse_url(?b2l(iolist_to_binary(Url))). - - -join_url_path(Src, Dst) -> - Src2 = case lists:reverse(Src) of - "/" ++ RestSrc -> lists:reverse(RestSrc); - _ -> Src - end, - Dst2 = case Dst of - "/" ++ RestDst -> RestDst; - _ -> Dst - end, - Src2 ++ "/" ++ Dst2. - - -url_to_url(#url{host=Host, port=Port, path=Path, protocol=Proto} = Url) -> - LPort = case {Proto, Port} of - {http, 80} -> ""; - {https, 443} -> ""; - _ -> ":" ++ integer_to_list(Port) - end, - LPath = case Path of - "/" ++ _RestPath -> Path; - _ -> "/" ++ Path - end, - HostPart = case Url#url.host_type of - ipv6_address -> - "[" ++ Host ++ "]"; - _ -> - Host - end, - atom_to_list(Proto) ++ "://" ++ HostPart ++ LPort ++ LPath. - - -body_length(Headers) -> - case is_chunked(Headers) of - true -> chunked; - _ -> content_length(Headers) - end. - - -is_chunked([]) -> - false; -is_chunked([{K, V} | Rest]) -> - case string:to_lower(K) of - "transfer-encoding" -> - string:to_lower(V) == "chunked"; - _ -> - is_chunked(Rest) - end. - -content_length([]) -> - undefined; -content_length([{K, V} | Rest]) -> - case string:to_lower(K) of - "content-length" -> - list_to_integer(V); - _ -> - content_length(Rest) - end. - diff --git a/src/couch/test/couchdb_http_proxy_tests.erl b/src/couch/test/couchdb_http_proxy_tests.erl deleted file mode 100644 index f60ba3b08..000000000 --- a/src/couch/test/couchdb_http_proxy_tests.erl +++ /dev/null @@ -1,456 +0,0 @@ -% 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(couchdb_http_proxy_tests). - --compile(tuple_calls). - --include_lib("couch/include/couch_eunit.hrl"). - --record(req, {method=get, path="", headers=[], body="", opts=[]}). - --define(CONFIG_FIXTURE_TEMP, - begin - FileName = filename:join([?TEMPDIR, ?tempfile() ++ ".ini"]), - {ok, Fd} = file:open(FileName, write), - ok = file:truncate(Fd), - ok = file:close(Fd), - FileName - end). --define(TIMEOUT, 5000). - - -start() -> - % we have to write any config changes to temp ini file to not loose them - % when supervisor will kill all children due to reaching restart threshold - % (each httpd_global_handlers changes causes couch_httpd restart) - Ctx = test_util:start_couch(?CONFIG_CHAIN ++ [?CONFIG_FIXTURE_TEMP], []), - % 49151 is IANA Reserved, let's assume no one is listening there - test_util:with_process_restart(couch_httpd, fun() -> - config:set("httpd_global_handlers", "_error", - "{couch_httpd_proxy, handle_proxy_req, <<\"http://127.0.0.1:49151/\">>}" - ) - end), - Ctx. - -setup() -> - {ok, Pid} = test_web:start_link(), - Value = lists:flatten(io_lib:format( - "{couch_httpd_proxy, handle_proxy_req, ~p}", - [list_to_binary(proxy_url())])), - test_util:with_process_restart(couch_httpd, fun() -> - config:set("httpd_global_handlers", "_test", Value) - end), - Pid. - -teardown(Pid) -> - test_util:stop_sync_throw(Pid, fun() -> - test_web:stop() - end, {timeout, test_web_stop}, ?TIMEOUT). - -http_proxy_test_() -> - { - "HTTP Proxy handler tests", - { - setup, - fun start/0, fun test_util:stop_couch/1, - { - foreach, - fun setup/0, fun teardown/1, - [ - fun should_proxy_basic_request/1, - fun should_return_alternative_status/1, - fun should_respect_trailing_slash/1, - fun should_proxy_headers/1, - fun should_proxy_host_header/1, - fun should_pass_headers_back/1, - fun should_use_same_protocol_version/1, - fun should_proxy_body/1, - fun should_proxy_body_back/1, - fun should_proxy_chunked_body/1, - fun should_proxy_chunked_body_back/1, - fun should_rewrite_location_header/1, - fun should_not_rewrite_external_locations/1, - fun should_rewrite_relative_location/1, - fun should_refuse_connection_to_backend/1 - ] - } - - } - }. - - -should_proxy_basic_request(_) -> - Remote = fun(Req) -> - 'GET' = Req:get(method), - "/" = Req:get(path), - 0 = Req:get(body_length), - <<>> = Req:recv_body(), - {ok, {200, [{"Content-Type", "text/plain"}], "ok"}} - end, - Local = fun - ({ok, "200", _, "ok"}) -> - true; - (_) -> - false - end, - ?_test(check_request(#req{}, Remote, Local)). - -should_return_alternative_status(_) -> - Remote = fun(Req) -> - "/alternate_status" = Req:get(path), - {ok, {201, [], "ok"}} - end, - Local = fun - ({ok, "201", _, "ok"}) -> - true; - (_) -> - false - end, - Req = #req{path = "/alternate_status"}, - ?_test(check_request(Req, Remote, Local)). - -should_respect_trailing_slash(_) -> - Remote = fun(Req) -> - "/trailing_slash/" = Req:get(path), - {ok, {200, [], "ok"}} - end, - Local = fun - ({ok, "200", _, "ok"}) -> - true; - (_) -> - false - end, - Req = #req{path="/trailing_slash/"}, - ?_test(check_request(Req, Remote, Local)). - -should_proxy_headers(_) -> - Remote = fun(Req) -> - "/passes_header" = Req:get(path), - "plankton" = Req:get_header_value("X-CouchDB-Ralph"), - {ok, {200, [], "ok"}} - end, - Local = fun - ({ok, "200", _, "ok"}) -> - true; - (_) -> - false - end, - Req = #req{ - path="/passes_header", - headers=[{"X-CouchDB-Ralph", "plankton"}] - }, - ?_test(check_request(Req, Remote, Local)). - -should_proxy_host_header(_) -> - Remote = fun(Req) -> - "/passes_host_header" = Req:get(path), - "www.google.com" = Req:get_header_value("Host"), - {ok, {200, [], "ok"}} - end, - Local = fun - ({ok, "200", _, "ok"}) -> - true; - (_) -> - false - end, - Req = #req{ - path="/passes_host_header", - headers=[{"Host", "www.google.com"}] - }, - ?_test(check_request(Req, Remote, Local)). - -should_pass_headers_back(_) -> - Remote = fun(Req) -> - "/passes_header_back" = Req:get(path), - {ok, {200, [{"X-CouchDB-Plankton", "ralph"}], "ok"}} - end, - Local = fun - ({ok, "200", Headers, "ok"}) -> - lists:member({"X-CouchDB-Plankton", "ralph"}, Headers); - (_) -> - false - end, - Req = #req{path="/passes_header_back"}, - ?_test(check_request(Req, Remote, Local)). - -should_use_same_protocol_version(_) -> - Remote = fun(Req) -> - "/uses_same_version" = Req:get(path), - {1, 0} = Req:get(version), - {ok, {200, [], "ok"}} - end, - Local = fun - ({ok, "200", _, "ok"}) -> - true; - (_) -> - false - end, - Req = #req{ - path="/uses_same_version", - opts=[{http_vsn, {1, 0}}] - }, - ?_test(check_request(Req, Remote, Local)). - -should_proxy_body(_) -> - Remote = fun(Req) -> - 'PUT' = Req:get(method), - "/passes_body" = Req:get(path), - <<"Hooray!">> = Req:recv_body(), - {ok, {201, [], "ok"}} - end, - Local = fun - ({ok, "201", _, "ok"}) -> - true; - (_) -> - false - end, - Req = #req{ - method=put, - path="/passes_body", - body="Hooray!" - }, - ?_test(check_request(Req, Remote, Local)). - -should_proxy_body_back(_) -> - BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], - Remote = fun(Req) -> - 'GET' = Req:get(method), - "/passes_eof_body" = Req:get(path), - {raw, {200, [{"Connection", "close"}], BodyChunks}} - end, - Local = fun - ({ok, "200", _, "foobarbazinga"}) -> - true; - (_) -> - false - end, - Req = #req{path="/passes_eof_body"}, - ?_test(check_request(Req, Remote, Local)). - -should_proxy_chunked_body(_) -> - BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], - Remote = fun(Req) -> - 'POST' = Req:get(method), - "/passes_chunked_body" = Req:get(path), - RecvBody = fun - ({Length, Chunk}, [Chunk | Rest]) -> - Length = size(Chunk), - Rest; - ({0, []}, []) -> - ok - end, - ok = Req:stream_body(1024 * 1024, RecvBody, BodyChunks), - {ok, {201, [], "ok"}} - end, - Local = fun - ({ok, "201", _, "ok"}) -> - true; - (_) -> - false - end, - Req = #req{ - method=post, - path="/passes_chunked_body", - headers=[{"Transfer-Encoding", "chunked"}], - body=chunked_body(BodyChunks) - }, - ?_test(check_request(Req, Remote, Local)). - -should_proxy_chunked_body_back(_) -> - ?_test(begin - Remote = fun(Req) -> - 'GET' = Req:get(method), - "/passes_chunked_body_back" = Req:get(path), - BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], - {chunked, {200, [{"Transfer-Encoding", "chunked"}], BodyChunks}} - end, - Req = #req{ - path="/passes_chunked_body_back", - opts=[{stream_to, self()}] - }, - - Resp = check_request(Req, Remote, no_local), - ?assertMatch({ibrowse_req_id, _}, Resp), - {_, ReqId} = Resp, - - % Grab headers from response - receive - {ibrowse_async_headers, ReqId, "200", Headers} -> - ?assertEqual("chunked", - proplists:get_value("Transfer-Encoding", Headers)), - ibrowse:stream_next(ReqId) - after 1000 -> - throw({error, timeout}) - end, - - ?assertEqual(<<"foobarbazinga">>, recv_body(ReqId, [])), - ?assertEqual(was_ok, test_web:check_last()) - end). - -should_refuse_connection_to_backend(_) -> - Local = fun - ({ok, "500", _, _}) -> - true; - (_) -> - false - end, - Req = #req{opts=[{url, server_url("/_error")}]}, - ?_test(check_request(Req, no_remote, Local)). - -should_rewrite_location_header(_) -> - { - "Testing location header rewrites", - do_rewrite_tests([ - {"Location", proxy_url() ++ "/foo/bar", - server_url() ++ "/foo/bar"}, - {"Content-Location", proxy_url() ++ "/bing?q=2", - server_url() ++ "/bing?q=2"}, - {"Uri", proxy_url() ++ "/zip#frag", - server_url() ++ "/zip#frag"}, - {"Destination", proxy_url(), - server_url() ++ "/"} - ]) - }. - -should_not_rewrite_external_locations(_) -> - { - "Testing no rewrite of external locations", - do_rewrite_tests([ - {"Location", external_url() ++ "/search", - external_url() ++ "/search"}, - {"Content-Location", external_url() ++ "/s?q=2", - external_url() ++ "/s?q=2"}, - {"Uri", external_url() ++ "/f#f", - external_url() ++ "/f#f"}, - {"Destination", external_url() ++ "/f?q=2#f", - external_url() ++ "/f?q=2#f"} - ]) - }. - -should_rewrite_relative_location(_) -> - { - "Testing relative rewrites", - do_rewrite_tests([ - {"Location", "/foo", - server_url() ++ "/foo"}, - {"Content-Location", "bar", - server_url() ++ "/bar"}, - {"Uri", "/zing?q=3", - server_url() ++ "/zing?q=3"}, - {"Destination", "bing?q=stuff#yay", - server_url() ++ "/bing?q=stuff#yay"} - ]) - }. - - -do_rewrite_tests(Tests) -> - lists:map(fun({Header, Location, Url}) -> - should_rewrite_header(Header, Location, Url) - end, Tests). - -should_rewrite_header(Header, Location, Url) -> - Remote = fun(Req) -> - "/rewrite_test" = Req:get(path), - {ok, {302, [{Header, Location}], "ok"}} - end, - Local = fun - ({ok, "302", Headers, "ok"}) -> - ?assertEqual(Url, couch_util:get_value(Header, Headers)), - true; - (E) -> - ?debugFmt("~p", [E]), - false - end, - Req = #req{path="/rewrite_test"}, - {Header, ?_test(check_request(Req, Remote, Local))}. - - -server_url() -> - server_url("/_test"). - -server_url(Resource) -> - Addr = config:get("httpd", "bind_address"), - Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), - lists:concat(["http://", Addr, ":", Port, Resource]). - -proxy_url() -> - "http://127.0.0.1:" ++ integer_to_list(test_web:get_port()). - -external_url() -> - "https://google.com". - -check_request(Req, Remote, Local) -> - case Remote of - no_remote -> - ok; - _ -> - test_web:set_assert(Remote) - end, - Url = case proplists:lookup(url, Req#req.opts) of - none -> - server_url() ++ Req#req.path; - {url, DestUrl} -> - DestUrl - end, - Opts = [{headers_as_is, true} | Req#req.opts], - Resp =ibrowse:send_req( - Url, Req#req.headers, Req#req.method, Req#req.body, Opts - ), - %?debugFmt("ibrowse response: ~p", [Resp]), - case Local of - no_local -> - ok; - _ -> - ?assert(Local(Resp)) - end, - case {Remote, Local} of - {no_remote, _} -> - ok; - {_, no_local} -> - ok; - _ -> - ?assertEqual(was_ok, test_web:check_last()) - end, - Resp. - -chunked_body(Chunks) -> - chunked_body(Chunks, []). - -chunked_body([], Acc) -> - iolist_to_binary(lists:reverse(Acc, "0\r\n\r\n")); -chunked_body([Chunk | Rest], Acc) -> - Size = to_hex(size(Chunk)), - chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]). - -to_hex(Val) -> - to_hex(Val, []). - -to_hex(0, Acc) -> - Acc; -to_hex(Val, Acc) -> - to_hex(Val div 16, [hex_char(Val rem 16) | Acc]). - -hex_char(V) when V < 10 -> $0 + V; -hex_char(V) -> $A + V - 10. - -recv_body(ReqId, Acc) -> - receive - {ibrowse_async_response, ReqId, Data} -> - recv_body(ReqId, [Data | Acc]); - {ibrowse_async_response_end, ReqId} -> - iolist_to_binary(lists:reverse(Acc)); - Else -> - throw({error, unexpected_mesg, Else}) - after ?TIMEOUT -> - throw({error, timeout}) - end. |