diff options
author | Paul Joseph Davis <davisp@apache.org> | 2009-12-16 00:05:35 +0000 |
---|---|---|
committer | Paul Joseph Davis <davisp@apache.org> | 2009-12-16 00:05:35 +0000 |
commit | 22c551bb103072826c0299265670d1483c753dde (patch) | |
tree | da3ede0f1b8e784bcd5f519fa3927f49e06e4ccd /test | |
parent | 04404e2dbc2fcdb08dbd967fb99d674d12b85c75 (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-x | test/etap/130-attachments-md5.t | 252 |
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. + |