From 3bb7855a5658c3c093d9c2efbfc8e3417cd55cfd Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Tue, 2 Mar 2010 15:14:39 +0000 Subject: [PATCH] 2010-03-03 Tatsuhiro Tsujikawa In Metalink4, if size element contains invalid size, discard whole document. Added strict hash value check for metalink3/4. * src/MetalinkParserController.cc * src/MetalinkParserStateMachine.cc * src/MetalinkParserStateMachine.h * src/MetalinkParserStateV3Impl.cc * src/MetalinkParserStateV4Impl.cc * src/util.cc * src/util.h * test/MetalinkParserControllerTest.cc * test/MetalinkProcessorTest.cc * test/metalink4.xml --- ChangeLog | 15 +++ src/MetalinkParserController.cc | 31 +++++- src/MetalinkParserStateMachine.cc | 5 + src/MetalinkParserStateMachine.h | 2 + src/MetalinkParserStateV3Impl.cc | 2 +- src/MetalinkParserStateV4Impl.cc | 5 +- src/util.cc | 10 ++ src/util.h | 6 +- test/MetalinkParserControllerTest.cc | 83 +++++++++++----- test/MetalinkProcessorTest.cc | 140 ++++++++++++++++++--------- test/metalink4.xml | 16 +-- 11 files changed, 229 insertions(+), 86 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8d7a0893..163f454e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,18 @@ +2010-03-03 Tatsuhiro Tsujikawa + + In Metalink4, if size element contains invalid size, discard whole + document. Added strict hash value check for metalink3/4. + * src/MetalinkParserController.cc + * src/MetalinkParserStateMachine.cc + * src/MetalinkParserStateMachine.h + * src/MetalinkParserStateV3Impl.cc + * src/MetalinkParserStateV4Impl.cc + * src/util.cc + * src/util.h + * test/MetalinkParserControllerTest.cc + * test/MetalinkProcessorTest.cc + * test/metalink4.xml + 2010-03-02 Tatsuhiro Tsujikawa Added strict attribute validation for metalink4. When diff --git a/src/MetalinkParserController.cc b/src/MetalinkParserController.cc index d3045b97..b340af49 100644 --- a/src/MetalinkParserController.cc +++ b/src/MetalinkParserController.cc @@ -53,6 +53,13 @@ namespace aria2 { +static bool isValidHash(const std::string& algo, const std::string& hash) +{ + return util::isHexDigit(hash) && + MessageDigestContext::supports(algo) && + MessageDigestContext::digestLength(algo)*2 == hash.size(); +} + MetalinkParserController::MetalinkParserController(): _metalinker(new Metalinker()) {} @@ -281,7 +288,11 @@ void MetalinkParserController::setHashOfChecksum(const std::string& md) if(_tChecksum.isNull()) { return; } - _tChecksum->setMessageDigest(md); + if(isValidHash(_tChecksum->getAlgo(), md)) { + _tChecksum->setMessageDigest(md); + } else { + cancelChecksumTransaction(); + } #endif // ENABLE_MESSAGE_DIGEST } @@ -352,7 +363,11 @@ void MetalinkParserController::addHashOfChunkChecksumV4(const std::string& md) if(_tChunkChecksumV4.isNull()) { return; } - _tempChunkChecksumsV4.push_back(md); + if(isValidHash(_tChunkChecksumV4->getAlgo(), md)) { + _tempChunkChecksumsV4.push_back(md); + } else { + cancelChunkChecksumTransactionV4(); + } #endif // ENABLE_MESSAGE_DIGEST } @@ -426,7 +441,11 @@ void MetalinkParserController::addHashOfChunkChecksum(size_t order, const std::s if(_tChunkChecksum.isNull()) { return; } - _tempChunkChecksums.push_back(std::pair(order, md)); + if(isValidHash(_tChunkChecksum->getAlgo(), md)) { + _tempChunkChecksums.push_back(std::make_pair(order, md)); + } else { + cancelChunkChecksumTransaction(); + } #endif // ENABLE_MESSAGE_DIGEST } @@ -446,7 +465,11 @@ void MetalinkParserController::setMessageDigestOfChunkChecksum(const std::string if(_tChunkChecksum.isNull()) { return; } - _tempHashPair.second = md; + if(isValidHash(_tChunkChecksum->getAlgo(), md)) { + _tempHashPair.second = md; + } else { + cancelChunkChecksumTransaction(); + } #endif // ENABLE_MESSAGE_DIGEST } diff --git a/src/MetalinkParserStateMachine.cc b/src/MetalinkParserStateMachine.cc index 2f114d4a..5d28ef3c 100644 --- a/src/MetalinkParserStateMachine.cc +++ b/src/MetalinkParserStateMachine.cc @@ -290,6 +290,11 @@ void MetalinkParserStateMachine::commitEntryTransaction() _ctrl->commitEntryTransaction(); } +void MetalinkParserStateMachine::cancelEntryTransaction() +{ + _ctrl->cancelEntryTransaction(); +} + void MetalinkParserStateMachine::newResourceTransaction() { _ctrl->newResourceTransaction(); diff --git a/src/MetalinkParserStateMachine.h b/src/MetalinkParserStateMachine.h index 472b05da..1eea88b5 100644 --- a/src/MetalinkParserStateMachine.h +++ b/src/MetalinkParserStateMachine.h @@ -166,6 +166,8 @@ public: void commitEntryTransaction(); + void cancelEntryTransaction(); + void newResourceTransaction(); void setURLOfResource(const std::string& url); diff --git a/src/MetalinkParserStateV3Impl.cc b/src/MetalinkParserStateV3Impl.cc index c4068836..98a931dd 100644 --- a/src/MetalinkParserStateV3Impl.cc +++ b/src/MetalinkParserStateV3Impl.cc @@ -183,7 +183,7 @@ void SizeMetalinkParserState::endElement const std::string& characters) { try { - stm->setFileLengthOfEntry(util::parseLLInt(characters)); + stm->setFileLengthOfEntry(util::parseULLInt(characters)); } catch(RecoverableException& e) { // current metalink specification doesn't require size element. } diff --git a/src/MetalinkParserStateV4Impl.cc b/src/MetalinkParserStateV4Impl.cc index d95b2772..fc81e89a 100644 --- a/src/MetalinkParserStateV4Impl.cc +++ b/src/MetalinkParserStateV4Impl.cc @@ -281,9 +281,10 @@ void SizeMetalinkParserStateV4::endElement const std::string& characters) { try { - stm->setFileLengthOfEntry(util::parseLLInt(characters)); + stm->setFileLengthOfEntry(util::parseULLInt(characters)); } catch(RecoverableException& e) { - // current metalink specification doesn't require size element. + stm->cancelEntryTransaction(); + stm->logError("Bad size"); } } diff --git a/src/util.cc b/src/util.cc index 5b6b6832..01e4bb96 100644 --- a/src/util.cc +++ b/src/util.cc @@ -216,6 +216,16 @@ bool isHexDigit(const char c) return isDigit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); } +bool isHexDigit(const std::string& s) +{ + for(std::string::const_iterator i = s.begin(), eoi = s.end(); i != eoi; ++i) { + if(!isHexDigit(*i)) { + return false; + } + } + return true; +} + bool inRFC3986ReservedChars(const char c) { static const char reserved[] = { diff --git a/src/util.h b/src/util.h index d1985209..4aefe6b0 100644 --- a/src/util.h +++ b/src/util.h @@ -254,11 +254,15 @@ void sleep(long seconds); void usleep(long microseconds); bool isNumber(const std::string& what); + +bool isHexDigit(const char c); + +bool isHexDigit(const std::string& s); bool isLowercase(const std::string& what); bool isUppercase(const std::string& what); - + /** * Converts alphabets to unsigned int, assuming alphabets as a base 26 * integer and 'a' or 'A' is 0. diff --git a/test/MetalinkParserControllerTest.cc b/test/MetalinkParserControllerTest.cc index 95708b18..904ccc22 100644 --- a/test/MetalinkParserControllerTest.cc +++ b/test/MetalinkParserControllerTest.cc @@ -150,19 +150,27 @@ void MetalinkParserControllerTest::testChecksumTransaction() ctrl.newEntryTransaction(); ctrl.newChecksumTransaction(); ctrl.setTypeOfChecksum("md5"); - ctrl.setHashOfChecksum("hash"); + ctrl.setHashOfChecksum("acbd18db4cc2f85cedef654fccc4a4d8"); ctrl.commitEntryTransaction(); { SharedHandle m = ctrl.getResult(); SharedHandle md = m->entries.front()->checksum; CPPUNIT_ASSERT_EQUAL(std::string("md5"), md->getAlgo()); - CPPUNIT_ASSERT_EQUAL(std::string("hash"), md->getMessageDigest()); + CPPUNIT_ASSERT_EQUAL(std::string("acbd18db4cc2f85cedef654fccc4a4d8"), + md->getMessageDigest()); } + ctrl.newEntryTransaction(); + ctrl.newChecksumTransaction(); + ctrl.setTypeOfChecksum("md5"); + ctrl.setHashOfChecksum("badhash"); + ctrl.commitEntryTransaction(); + CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->checksum.isNull()); + ctrl.newEntryTransaction(); ctrl.newChecksumTransaction(); ctrl.cancelChecksumTransaction(); ctrl.commitEntryTransaction(); - CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->checksum.isNull()); + CPPUNIT_ASSERT(ctrl.getResult()->entries[2]->checksum.isNull()); } void MetalinkParserControllerTest::testChunkChecksumTransaction() @@ -172,11 +180,11 @@ void MetalinkParserControllerTest::testChunkChecksumTransaction() ctrl.newChunkChecksumTransaction(); ctrl.setTypeOfChunkChecksum("md5"); ctrl.setLengthOfChunkChecksum(256*1024); - ctrl.addHashOfChunkChecksum(4, "hash4"); - ctrl.addHashOfChunkChecksum(1, "hash1"); - ctrl.addHashOfChunkChecksum(3, "hash3"); - ctrl.addHashOfChunkChecksum(2, "hash2"); - ctrl.addHashOfChunkChecksum(5, "hash5"); + ctrl.addHashOfChunkChecksum(4, "4cbd18db4cc2f85cedef654fccc4a4d8"); + ctrl.addHashOfChunkChecksum(1, "1cbd18db4cc2f85cedef654fccc4a4d8"); + ctrl.addHashOfChunkChecksum(3, "3cbd18db4cc2f85cedef654fccc4a4d8"); + ctrl.addHashOfChunkChecksum(2, "2cbd18db4cc2f85cedef654fccc4a4d8"); + ctrl.addHashOfChunkChecksum(5, "5cbd18db4cc2f85cedef654fccc4a4d8"); ctrl.commitEntryTransaction(); { SharedHandle m = ctrl.getResult(); @@ -184,17 +192,30 @@ void MetalinkParserControllerTest::testChunkChecksumTransaction() CPPUNIT_ASSERT_EQUAL(std::string("md5"), md->getAlgo()); CPPUNIT_ASSERT_EQUAL((size_t)256*1024, md->getChecksumLength()); CPPUNIT_ASSERT_EQUAL((size_t)5, md->countChecksum()); - CPPUNIT_ASSERT_EQUAL(std::string("hash1"), md->getChecksums()[0]); - CPPUNIT_ASSERT_EQUAL(std::string("hash2"), md->getChecksums()[1]); - CPPUNIT_ASSERT_EQUAL(std::string("hash3"), md->getChecksums()[2]); - CPPUNIT_ASSERT_EQUAL(std::string("hash4"), md->getChecksums()[3]); - CPPUNIT_ASSERT_EQUAL(std::string("hash5"), md->getChecksums()[4]); + CPPUNIT_ASSERT_EQUAL(std::string("1cbd18db4cc2f85cedef654fccc4a4d8"), + md->getChecksums()[0]); + CPPUNIT_ASSERT_EQUAL(std::string("2cbd18db4cc2f85cedef654fccc4a4d8"), + md->getChecksums()[1]); + CPPUNIT_ASSERT_EQUAL(std::string("3cbd18db4cc2f85cedef654fccc4a4d8"), + md->getChecksums()[2]); + CPPUNIT_ASSERT_EQUAL(std::string("4cbd18db4cc2f85cedef654fccc4a4d8"), + md->getChecksums()[3]); + CPPUNIT_ASSERT_EQUAL(std::string("5cbd18db4cc2f85cedef654fccc4a4d8"), + md->getChecksums()[4]); } + ctrl.newEntryTransaction(); + ctrl.newChunkChecksumTransaction(); + ctrl.setTypeOfChunkChecksum("md5"); + ctrl.setLengthOfChunkChecksum(256*1024); + ctrl.addHashOfChunkChecksum(1, "badhash"); + ctrl.commitEntryTransaction(); + CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull()); + ctrl.newEntryTransaction(); ctrl.newChunkChecksumTransaction(); ctrl.cancelChunkChecksumTransaction(); ctrl.commitEntryTransaction(); - CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull()); + CPPUNIT_ASSERT(ctrl.getResult()->entries[2]->chunkChecksum.isNull()); } void MetalinkParserControllerTest::testChunkChecksumTransactionV4() @@ -202,27 +223,43 @@ void MetalinkParserControllerTest::testChunkChecksumTransactionV4() MetalinkParserController ctrl; ctrl.newEntryTransaction(); ctrl.newChunkChecksumTransactionV4(); - ctrl.setTypeOfChunkChecksumV4("md5"); + ctrl.setTypeOfChunkChecksumV4("sha-1"); ctrl.setLengthOfChunkChecksumV4(256*1024); - ctrl.addHashOfChunkChecksumV4("hash1"); - ctrl.addHashOfChunkChecksumV4("hash2"); - ctrl.addHashOfChunkChecksumV4("hash3"); + + ctrl.addHashOfChunkChecksumV4("5bd9f7248df0f3a6a86ab6c95f48787d546efa14"); + ctrl.addHashOfChunkChecksumV4("9413ee70957a09d55704123687478e07f18c7b29"); + ctrl.addHashOfChunkChecksumV4("44213f9f4d59b557314fadcd233232eebcac8012"); ctrl.commitEntryTransaction(); { SharedHandle m = ctrl.getResult(); SharedHandle md = m->entries.front()->chunkChecksum; - CPPUNIT_ASSERT_EQUAL(std::string("md5"), md->getAlgo()); + CPPUNIT_ASSERT_EQUAL(std::string("sha-1"), md->getAlgo()); CPPUNIT_ASSERT_EQUAL((size_t)256*1024, md->getChecksumLength()); CPPUNIT_ASSERT_EQUAL((size_t)3, md->countChecksum()); - CPPUNIT_ASSERT_EQUAL(std::string("hash1"), md->getChecksums()[0]); - CPPUNIT_ASSERT_EQUAL(std::string("hash2"), md->getChecksums()[1]); - CPPUNIT_ASSERT_EQUAL(std::string("hash3"), md->getChecksums()[2]); + CPPUNIT_ASSERT_EQUAL + (std::string("5bd9f7248df0f3a6a86ab6c95f48787d546efa14"), + md->getChecksums()[0]); + CPPUNIT_ASSERT_EQUAL + (std::string("9413ee70957a09d55704123687478e07f18c7b29"), + md->getChecksums()[1]); + CPPUNIT_ASSERT_EQUAL + (std::string("44213f9f4d59b557314fadcd233232eebcac8012"), + md->getChecksums()[2]); } + ctrl.newEntryTransaction(); + ctrl.newChunkChecksumTransactionV4(); + ctrl.setTypeOfChunkChecksumV4("sha-1"); + ctrl.setLengthOfChunkChecksumV4(256*1024); + ctrl.addHashOfChunkChecksumV4("5bd9f7248df0f3a6a86ab6c95f48787d546efa14"); + ctrl.addHashOfChunkChecksumV4("badhash"); + ctrl.commitEntryTransaction(); + CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull()); + ctrl.newEntryTransaction(); ctrl.newChunkChecksumTransactionV4(); ctrl.cancelChunkChecksumTransactionV4(); ctrl.commitEntryTransaction(); - CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull()); + CPPUNIT_ASSERT(ctrl.getResult()->entries[2]->chunkChecksum.isNull()); } #endif // ENABLE_MESSAGE_DIGEST diff --git a/test/MetalinkProcessorTest.cc b/test/MetalinkProcessorTest.cc index ea28bcdc..f95e900a 100644 --- a/test/MetalinkProcessorTest.cc +++ b/test/MetalinkProcessorTest.cc @@ -33,6 +33,7 @@ class MetalinkProcessorTest:public CppUnit::TestFixture { CPPUNIT_TEST(testMalformedXML); CPPUNIT_TEST(testMalformedXML2); CPPUNIT_TEST(testBadSize); + CPPUNIT_TEST(testBadSizeV4); CPPUNIT_TEST(testBadMaxConn); CPPUNIT_TEST(testNoName); CPPUNIT_TEST(testBadURLPrefs); @@ -58,6 +59,7 @@ public: void testMalformedXML(); void testMalformedXML2(); void testBadSize(); + void testBadSizeV4(); void testBadMaxConn(); void testNoName(); void testBadURLPrefs(); @@ -90,7 +92,7 @@ void MetalinkProcessorTest::testParseFileV4() CPPUNIT_ASSERT_EQUAL((uint64_t)786430LL, e->getLength()); CPPUNIT_ASSERT_EQUAL(-1, e->maxConnections); #ifdef ENABLE_MESSAGE_DIGEST - CPPUNIT_ASSERT_EQUAL(std::string("80bc95fd391772fa61c91ed68567f0980bb45fd9"), + CPPUNIT_ASSERT_EQUAL(std::string("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"), e->checksum->getMessageDigest()); CPPUNIT_ASSERT(!e->checksum.isNull()); CPPUNIT_ASSERT_EQUAL(std::string("sha-1"), e->checksum->getAlgo()); @@ -98,11 +100,11 @@ void MetalinkProcessorTest::testParseFileV4() CPPUNIT_ASSERT_EQUAL(std::string("sha-256"), e->chunkChecksum->getAlgo()); CPPUNIT_ASSERT_EQUAL((size_t)262144, e->chunkChecksum->getChecksumLength()); CPPUNIT_ASSERT_EQUAL((size_t)3, e->chunkChecksum->countChecksum()); - CPPUNIT_ASSERT_EQUAL(std::string("metalinkhash1"), + CPPUNIT_ASSERT_EQUAL(std::string("0245178074fd042e19b7c3885b360fc21064b30e73f5626c7e3b005d048069c5"), e->chunkChecksum->getChecksum(0)); - CPPUNIT_ASSERT_EQUAL(std::string("metalinkhash2"), + CPPUNIT_ASSERT_EQUAL(std::string("487ba2299be7f759d7c7bf6a4ac3a32cee81f1bb9332fc485947e32918864fb2"), e->chunkChecksum->getChecksum(1)); - CPPUNIT_ASSERT_EQUAL(std::string("metalinkhash3"), + CPPUNIT_ASSERT_EQUAL(std::string("37290d74ac4d186e3a8e5785d259d2ec04fac91ae28092e7620ec8bc99e830aa"), e->chunkChecksum->getChecksum(2)); #endif // ENABLE_MESSAGE_DIGEST CPPUNIT_ASSERT(!e->getSignature().isNull()); @@ -202,6 +204,11 @@ void MetalinkProcessorTest::testParseFileV4_attrs() } catch(RecoverableException& e) { // success } + dw->setString(StringFormat(tmpl, "A").str()); + try { + m = proc.parseFromBinaryStream(dw); + CPPUNIT_FAIL("exception must be thrown."); + } catch(RecoverableException& e) {} } { // Testing metaurl@priority @@ -239,6 +246,11 @@ void MetalinkProcessorTest::testParseFileV4_attrs() } catch(RecoverableException& e) { // success } + dw->setString(StringFormat(tmpl, "A").str()); + try { + m = proc.parseFromBinaryStream(dw); + CPPUNIT_FAIL("exception must be thrown."); + } catch(RecoverableException& e) {} } { // Testing metaurl@mediatype @@ -346,6 +358,12 @@ void MetalinkProcessorTest::testParseFileV4_attrs() m = proc.parseFromBinaryStream(dw); CPPUNIT_FAIL("exception must be thrown."); } catch(RecoverableException& e) {} + // not a number + try { + dw->setString(StringFormat(tmpl, "A").str()); + m = proc.parseFromBinaryStream(dw); + CPPUNIT_FAIL("exception must be thrown."); + } catch(RecoverableException& e) {} } { // Testing pieces@type @@ -622,6 +640,31 @@ void MetalinkProcessorTest::testMalformedXML2() } } +void MetalinkProcessorTest::testBadSizeV4() +{ + MetalinkProcessor proc; + SharedHandle m; + SharedHandle dw(new ByteArrayDiskWriter()); + + const char* tmpl = + "" + "" + "" + "%s" + "http://example.org" + "" + ""; + + dw->setString(StringFormat(tmpl, "9223372036854775807").str()); + m = proc.parseFromBinaryStream(dw); + + dw->setString(StringFormat(tmpl, "-1").str()); + try { + m = proc.parseFromBinaryStream(dw); + CPPUNIT_FAIL("exception must be thrown."); + } catch(RecoverableException& e) {} +} + void MetalinkProcessorTest::testBadSize() { MetalinkProcessor proc; @@ -850,21 +893,22 @@ void MetalinkProcessorTest::testBadPieceNo() { MetalinkProcessor proc; SharedHandle dw(new ByteArrayDiskWriter()); - dw->setString("" - "" - "" - " " - " " - " abc" - " xyz" - " " - " " - " abc" - " " - " " - "" - "" - ""); + dw->setString + ("" + "" + "" + " " + " " + " 44213f9f4d59b557314fadcd233232eebcac8012" + " 44213f9f4d59b557314fadcd233232eebcac8012" + " " + " " + " 44213f9f4d59b557314fadcd233232eebcac8012" + " " + " " + "" + "" + ""); try { SharedHandle m = proc.parseFromBinaryStream(dw); @@ -883,20 +927,21 @@ void MetalinkProcessorTest::testBadPieceLength() { MetalinkProcessor proc; SharedHandle dw(new ByteArrayDiskWriter()); - dw->setString("" - "" - "" - " " - " " - " abc" - " " - " " - " abc" - " " - " " - "" - "" - ""); + dw->setString + ("" + "" + "" + " " + " " + " 44213f9f4d59b557314fadcd233232eebcac8012" + " " + " " + " 44213f9f4d59b557314fadcd233232eebcac8012" + " " + " " + "" + "" + ""); try { SharedHandle m = proc.parseFromBinaryStream(dw); @@ -915,20 +960,21 @@ void MetalinkProcessorTest::testUnsupportedType_piece() { MetalinkProcessor proc; SharedHandle dw(new ByteArrayDiskWriter()); - dw->setString("" - "" - "" - " " - " " - " abc" - " " - " " - " abc" - " " - " " - "" - "" - ""); + dw->setString + ("" + "" + "" + " " + " " + " 44213f9f4d59b557314fadcd233232eebcac8012" + " " + " " + " 44213f9f4d59b557314fadcd233232eebcac8012" + " " + " " + "" + "" + ""); try { SharedHandle m = proc.parseFromBinaryStream(dw); diff --git a/test/metalink4.xml b/test/metalink4.xml index aa4c6609..e6916458 100644 --- a/test/metalink4.xml +++ b/test/metalink4.xml @@ -7,17 +7,17 @@ 1.0 en A description of the example file for download. - 80bc95fd391772fa61c91ed68567f0980bb45fd9 - 80bc95fd391772fa61c91ed68567f0980bb45fd9 + cbd18db4cc2f85cedef654fccc4a4d8 + 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 - metalinkhash1 - metalinkhash2 - metalinkhash3 + 5bd9f7248df0f3a6a86ab6c95f48787d546efa14 + 9413ee70957a09d55704123687478e07f18c7b29 + 44213f9f4d59b557314fadcd233232eebcac8012 - metalinkhash1 - metalinkhash2 - metalinkhash3 + 0245178074fd042e19b7c3885b360fc21064b30e73f5626c7e3b005d048069c5 + 487ba2299be7f759d7c7bf6a4ac3a32cee81f1bb9332fc485947e32918864fb2 + 37290d74ac4d186e3a8e5785d259d2ec04fac91ae28092e7620ec8bc99e830aa ftp://ftp.example.com/example.ext http://example.com/example.ext