summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorPaul Joseph Davis <davisp@apache.org>2009-12-16 00:05:35 +0000
committerPaul Joseph Davis <davisp@apache.org>2009-12-16 00:05:35 +0000
commit22c551bb103072826c0299265670d1483c753dde (patch)
treeda3ede0f1b8e784bcd5f519fa3927f49e06e4ccd /test
parent04404e2dbc2fcdb08dbd967fb99d674d12b85c75 (diff)
Provide Content-MD5 header support for attachments.
Fixes COUCHDB-558. Thanks to Filipe Manana we now have checks for attachment transfer integrity using the Content-MD5 header (or trailer). Use of this integrity check is triggered by specifying a Content-MD5 header in your request with a value that is a base64 encoded md5. For requests that are using a chunked Transfer-Encoding it is also possible to use a trailer so that the Content-MD5 doesn't need to be known before transfer. This works by specifying a header "Trailer: Content-MD5" and then in the final chunk (the one with a size of zero) you can specify a Content-MD5 with exactly the same format as in the request headers. See the ETap test 130-attachments-md5.t for explicit examples of the request messages. git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@891077 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'test')
-rwxr-xr-xtest/etap/130-attachments-md5.t252
1 files changed, 252 insertions, 0 deletions
diff --git a/test/etap/130-attachments-md5.t b/test/etap/130-attachments-md5.t
new file mode 100755
index 00000000..fe6732d6
--- /dev/null
+++ b/test/etap/130-attachments-md5.t
@@ -0,0 +1,252 @@
+#!/usr/bin/env escript
+% 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.
+
+default_config() ->
+ test_util:build_file("etc/couchdb/default_dev.ini").
+
+test_db_name() ->
+ <<"etap-test-db">>.
+
+docid() ->
+ case get(docid) of
+ undefined ->
+ put(docid, 1),
+ "1";
+ Count ->
+ put(docid, Count+1),
+ integer_to_list(Count+1)
+ end.
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(16),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ couch_server_sup:start_link([default_config()]),
+ Addr = couch_config:get("httpd", "bind_address", any),
+ Port = list_to_integer(couch_config:get("httpd", "port", "5984")),
+ put(addr, Addr),
+ put(port, Port),
+ timer:sleep(1000),
+
+ couch_server:delete(test_db_name(), []),
+ couch_db:create(test_db_name(), []),
+
+ test_identity_without_md5(),
+ test_chunked_without_md5(),
+
+ test_identity_with_valid_md5(),
+ test_chunked_with_valid_md5_header(),
+ test_chunked_with_valid_md5_trailer(),
+
+ test_identity_with_invalid_md5(),
+ test_chunked_with_invalid_md5_header(),
+ test_chunked_with_invalid_md5_trailer(),
+
+ couch_server:delete(test_db_name(), []),
+ couch_server_sup:stop(),
+ ok.
+
+test_identity_without_md5() ->
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Content-Length: 34\r\n",
+ "\r\n",
+ "We all live in a yellow submarine!"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with identity encoding and no MD5"),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_chunked_without_md5() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n"
+ "0\r\n"
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with chunked encoding and no MD5"),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_identity_with_valid_md5() ->
+ AttData = "We all live in a yellow submarine!",
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Content-Length: 34\r\n",
+ "Content-MD5: ", base64:encode(erlang:md5(AttData)), "\r\n",
+ "\r\n",
+ AttData],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with identity encoding and valid MD5"),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_chunked_with_valid_md5_header() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Content-MD5: ", base64:encode(erlang:md5(AttData)), "\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with chunked encoding and valid MD5 header."),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_chunked_with_valid_md5_trailer() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Trailer: Content-MD5\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "Content-MD5: ", base64:encode(erlang:md5(AttData)), "\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with chunked encoding and valid MD5 trailer."),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_identity_with_invalid_md5() ->
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Content-Length: 34\r\n",
+ "Content-MD5: ", base64:encode(<<"foobar!">>), "\r\n",
+ "\r\n",
+ "We all live in a yellow submarine!"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 400, "Invalid MD5 header causes an error: identity"),
+ etap:is(
+ get_json(Json, [<<"error">>]),
+ <<"content_md5_mismatch">>,
+ "Body indicates reason for failure."
+ ).
+
+test_chunked_with_invalid_md5_header() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Content-MD5: ", base64:encode(<<"so sneaky...">>), "\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 400, "Invalid MD5 header causes an error: chunked"),
+ etap:is(
+ get_json(Json, [<<"error">>]),
+ <<"content_md5_mismatch">>,
+ "Body indicates reason for failure."
+ ).
+
+test_chunked_with_invalid_md5_trailer() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Trailer: Content-MD5\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "Content-MD5: ", base64:encode(<<"Kool-Aid Fountain!">>), "\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 400, "Invalid MD5 Trailer causes an error"),
+ etap:is(
+ get_json(Json, [<<"error">>]),
+ <<"content_md5_mismatch">>,
+ "Body indicates reason for failure."
+ ).
+
+
+get_socket() ->
+ Options = [binary, {packet, 0}, {active, false}],
+ {ok, Sock} = gen_tcp:connect(get(addr), get(port), Options),
+ Sock.
+
+do_request(Request) ->
+ Sock = get_socket(),
+ gen_tcp:send(Sock, list_to_binary(lists:flatten(Request))),
+ timer:sleep(100),
+ {ok, R} = gen_tcp:recv(Sock, 0),
+ gen_tcp:close(Sock),
+ [Header, Body] = re:split(R, "\r\n\r\n", [{return, binary}]),
+ {ok, {http_response, _, Code, _}, _} =
+ erlang:decode_packet(http, Header, []),
+ Json = couch_util:json_decode(Body),
+ {Code, Json}.
+
+get_json(Json, Path) ->
+ couch_util:get_nested_json_value(Json, Path).
+
+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.
+