comparison OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp @ 4640:66109d24d26e

"ETag" headers for metadata and attachments now allow strong comparison (MD5 is included)
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 26 Apr 2021 15:22:44 +0200
parents f7d5372b59b3
children e8967149d87a
comparison
equal deleted inserted replaced
4639:c638dd444de0 4640:66109d24d26e
1448 1448
1449 call.GetOutput().AnswerJson(result); 1449 call.GetOutput().AnswerJson(result);
1450 } 1450 }
1451 1451
1452 1452
1453 static void SetStringContentETag(RestApiOutput& output,
1454 int64_t revision,
1455 const std::string& value)
1456 {
1457 std::string md5;
1458 Toolbox::ComputeMD5(md5, value);
1459 const std::string etag = "\"" + boost::lexical_cast<std::string>(revision) + "-" + md5 + "\"";
1460 output.GetLowLevelOutput().AddHeader("ETag", etag);
1461 }
1462
1463
1464 static void SetBufferContentETag(RestApiOutput& output,
1465 int64_t revision,
1466 const void* data,
1467 size_t size)
1468 {
1469 std::string md5;
1470 Toolbox::ComputeMD5(md5, data, size);
1471 const std::string etag = "\"" + boost::lexical_cast<std::string>(revision) + "-" + md5 + "\"";
1472 output.GetLowLevelOutput().AddHeader("ETag", etag);
1473 }
1474
1475
1476 static void SetAttachmentETag(RestApiOutput& output,
1477 int64_t revision,
1478 const FileInfo& info)
1479 {
1480 const std::string etag = ("\"" + boost::lexical_cast<std::string>(revision) + "-" +
1481 info.GetUncompressedMD5() + "\"");
1482 output.GetLowLevelOutput().AddHeader("ETag", etag);
1483 }
1484
1485
1486 static std::string GetMD5(const std::string& value)
1487 {
1488 std::string md5;
1489 Toolbox::ComputeMD5(md5, value);
1490 return md5;
1491 }
1492
1493
1453 static bool GetRevisionHeader(int64_t& revision /* out */, 1494 static bool GetRevisionHeader(int64_t& revision /* out */,
1495 std::string& md5 /* out */,
1454 const RestApiCall& call, 1496 const RestApiCall& call,
1455 const std::string& header) 1497 const std::string& header)
1456 { 1498 {
1457 std::string lower; 1499 std::string lower;
1458 Toolbox::ToLowerCase(lower, header); 1500 Toolbox::ToLowerCase(lower, header);
1467 std::string value = Toolbox::StripSpaces(found->second); 1509 std::string value = Toolbox::StripSpaces(found->second);
1468 Toolbox::RemoveSurroundingQuotes(value); 1510 Toolbox::RemoveSurroundingQuotes(value);
1469 1511
1470 try 1512 try
1471 { 1513 {
1472 revision = boost::lexical_cast<int64_t>(value); 1514 size_t comma = value.find('-');
1473 return true; 1515 if (comma != std::string::npos)
1516 {
1517 revision = boost::lexical_cast<int64_t>(value.substr(0, comma));
1518 md5 = value.substr(comma + 1);
1519 return true;
1520 }
1474 } 1521 }
1475 catch (boost::bad_lexical_cast&) 1522 catch (boost::bad_lexical_cast&)
1476 { 1523 {
1477 throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header + 1524 }
1478 "\" HTTP header should contain the revision as an integer, but found: " + value); 1525
1479 } 1526 throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header +
1527 "\" HTTP header should contain the ETag (revision followed by MD5 hash), but found: " + value);
1480 } 1528 }
1481 } 1529 }
1482 1530
1483 1531
1484 static void GetMetadata(RestApiGetCall& call) 1532 static void GetMetadata(RestApiGetCall& call)
1508 1556
1509 std::string value; 1557 std::string value;
1510 int64_t revision; 1558 int64_t revision;
1511 if (OrthancRestApi::GetIndex(call).LookupMetadata(value, revision, publicId, level, metadata)) 1559 if (OrthancRestApi::GetIndex(call).LookupMetadata(value, revision, publicId, level, metadata))
1512 { 1560 {
1513 call.GetOutput().GetLowLevelOutput(). 1561 SetStringContentETag(call.GetOutput(), revision, value); // New in Orthanc 1.9.2
1514 AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\""); // New in Orthanc 1.9.2
1515 1562
1516 int64_t userRevision; 1563 int64_t userRevision;
1517 if (GetRevisionHeader(userRevision, call, "If-None-Match") && 1564 std::string userMD5;
1518 revision == userRevision) 1565 if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
1566 userRevision == revision &&
1567 userMD5 == GetMD5(value))
1519 { 1568 {
1520 call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); 1569 call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
1521 } 1570 }
1522 else 1571 else
1523 { 1572 {
1553 1602
1554 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata 1603 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata
1555 { 1604 {
1556 bool found; 1605 bool found;
1557 int64_t revision; 1606 int64_t revision;
1558 if (GetRevisionHeader(revision, call, "if-match")) 1607 std::string md5;
1559 { 1608 if (GetRevisionHeader(revision, md5, call, "if-match"))
1560 found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision); 1609 {
1610 found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision, md5);
1561 } 1611 }
1562 else 1612 else
1563 { 1613 {
1564 OrthancConfiguration::ReaderLock lock; 1614 OrthancConfiguration::ReaderLock lock;
1565 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) 1615 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
1567 throw OrthancException(ErrorCode_Revision, 1617 throw OrthancException(ErrorCode_Revision,
1568 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\""); 1618 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\"");
1569 } 1619 }
1570 else 1620 else
1571 { 1621 {
1572 found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */); 1622 found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */, "");
1573 } 1623 }
1574 } 1624 }
1575 1625
1576 if (found) 1626 if (found)
1577 { 1627 {
1617 call.BodyToString(value); 1667 call.BodyToString(value);
1618 1668
1619 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata 1669 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata
1620 { 1670 {
1621 int64_t oldRevision; 1671 int64_t oldRevision;
1622 bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match"); 1672 std::string oldMD5;
1673 bool hasOldRevision = GetRevisionHeader(oldRevision, oldMD5, call, "if-match");
1623 1674
1624 if (!hasOldRevision) 1675 if (!hasOldRevision)
1625 { 1676 {
1626 OrthancConfiguration::ReaderLock lock; 1677 OrthancConfiguration::ReaderLock lock;
1627 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) 1678 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
1629 // "StatelessDatabaseOperations::SetMetadata()" will ignore 1680 // "StatelessDatabaseOperations::SetMetadata()" will ignore
1630 // the actual value of "oldRevision" if the metadata is 1681 // the actual value of "oldRevision" if the metadata is
1631 // inexistent as expected 1682 // inexistent as expected
1632 hasOldRevision = true; 1683 hasOldRevision = true;
1633 oldRevision = -1; // dummy value 1684 oldRevision = -1; // dummy value
1685 oldMD5.clear(); // dummy value
1634 } 1686 }
1635 } 1687 }
1636 1688
1637 int64_t newRevision; 1689 int64_t newRevision;
1638 OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value, hasOldRevision, oldRevision); 1690 OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value,
1639 1691 hasOldRevision, oldRevision, oldMD5);
1640 call.GetOutput().GetLowLevelOutput(). 1692
1641 AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\""); // New in Orthanc 1.9.2 1693 SetStringContentETag(call.GetOutput(), newRevision, value); // New in Orthanc 1.9.2
1642
1643 call.GetOutput().AnswerBuffer("", MimeType_PlainText); 1694 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1644 } 1695 }
1645 else 1696 else
1646 { 1697 {
1647 call.GetOutput().SignalError(HttpStatus_403_Forbidden); 1698 call.GetOutput().SignalError(HttpStatus_403_Forbidden);
1707 FileContentType contentType = StringToContentType(name); 1758 FileContentType contentType = StringToContentType(name);
1708 1759
1709 int64_t revision; 1760 int64_t revision;
1710 if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType)) 1761 if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType))
1711 { 1762 {
1712 call.GetOutput().GetLowLevelOutput(). 1763 SetAttachmentETag(call.GetOutput(), revision, info); // New in Orthanc 1.9.2
1713 AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\""); // New in Orthanc 1.9.2
1714 1764
1715 int64_t userRevision; 1765 int64_t userRevision;
1716 if (GetRevisionHeader(userRevision, call, "If-None-Match") && 1766 std::string userMD5;
1717 revision == userRevision) 1767 if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
1768 revision == userRevision &&
1769 info.GetUncompressedMD5() == userMD5)
1718 { 1770 {
1719 call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); 1771 call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
1720 return false; 1772 return false;
1721 } 1773 }
1722 else 1774 else
1808 CheckValidResourceType(call); 1860 CheckValidResourceType(call);
1809 1861
1810 std::string publicId = call.GetUriComponent("id", ""); 1862 std::string publicId = call.GetUriComponent("id", "");
1811 FileContentType type = StringToContentType(call.GetUriComponent("name", "")); 1863 FileContentType type = StringToContentType(call.GetUriComponent("name", ""));
1812 1864
1813 if (uncompress) 1865 FileInfo info;
1814 { 1866 if (GetAttachmentInfo(info, call))
1815 FileInfo info; 1867 {
1816 if (GetAttachmentInfo(info, call)) 1868 // NB: "SetAttachmentETag()" is already invoked by "GetAttachmentInfo()"
1869
1870 if (uncompress)
1817 { 1871 {
1818 context.AnswerAttachment(call.GetOutput(), publicId, type); 1872 context.AnswerAttachment(call.GetOutput(), publicId, type);
1819 } 1873 }
1820 }
1821 else
1822 {
1823 // Return the raw data (possibly compressed), as stored on the filesystem
1824 std::string content;
1825 int64_t revision;
1826 context.ReadAttachment(content, revision, publicId, type, false);
1827
1828 call.GetOutput().GetLowLevelOutput().
1829 AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\""); // New in Orthanc 1.9.2
1830
1831 int64_t userRevision;
1832 if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
1833 revision == userRevision)
1834 {
1835 call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
1836 }
1837 else 1874 else
1838 { 1875 {
1839 call.GetOutput().AnswerBuffer(content, MimeType_Binary); 1876 // Return the raw data (possibly compressed), as stored on the filesystem
1877 std::string content;
1878 int64_t revision;
1879 context.ReadAttachment(content, revision, publicId, type, false);
1880
1881 int64_t userRevision;
1882 std::string userMD5;
1883 if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
1884 revision == userRevision &&
1885 info.GetUncompressedMD5() == userMD5)
1886 {
1887 call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
1888 }
1889 else
1890 {
1891 call.GetOutput().AnswerBuffer(content, MimeType_Binary);
1892 }
1840 } 1893 }
1841 } 1894 }
1842 } 1895 }
1843 1896
1844 1897
2035 2088
2036 FileContentType contentType = StringToContentType(name); 2089 FileContentType contentType = StringToContentType(name);
2037 if (IsUserContentType(contentType)) // It is forbidden to modify internal attachments 2090 if (IsUserContentType(contentType)) // It is forbidden to modify internal attachments
2038 { 2091 {
2039 int64_t oldRevision; 2092 int64_t oldRevision;
2040 bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match"); 2093 std::string oldMD5;
2094 bool hasOldRevision = GetRevisionHeader(oldRevision, oldMD5, call, "if-match");
2041 2095
2042 if (!hasOldRevision) 2096 if (!hasOldRevision)
2043 { 2097 {
2044 OrthancConfiguration::ReaderLock lock; 2098 OrthancConfiguration::ReaderLock lock;
2045 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) 2099 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
2047 // "StatelessDatabaseOperations::AddAttachment()" will ignore 2101 // "StatelessDatabaseOperations::AddAttachment()" will ignore
2048 // the actual value of "oldRevision" if the metadata is 2102 // the actual value of "oldRevision" if the metadata is
2049 // inexistent as expected 2103 // inexistent as expected
2050 hasOldRevision = true; 2104 hasOldRevision = true;
2051 oldRevision = -1; // dummy value 2105 oldRevision = -1; // dummy value
2106 oldMD5.clear(); // dummy value
2052 } 2107 }
2053 } 2108 }
2054 2109
2055 int64_t newRevision; 2110 int64_t newRevision;
2056 context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(), 2111 context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(),
2057 call.GetBodySize(), hasOldRevision, oldRevision); 2112 call.GetBodySize(), hasOldRevision, oldRevision, oldMD5);
2058 2113
2059 call.GetOutput().GetLowLevelOutput(). 2114 SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize()); // New in Orthanc 1.9.2
2060 AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\""); // New in Orthanc 1.9.2
2061
2062 call.GetOutput().AnswerBuffer("{}", MimeType_Json); 2115 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
2063 } 2116 }
2064 else 2117 else
2065 { 2118 {
2066 call.GetOutput().SignalError(HttpStatus_403_Forbidden); 2119 call.GetOutput().SignalError(HttpStatus_403_Forbidden);
2117 2170
2118 if (allowed) 2171 if (allowed)
2119 { 2172 {
2120 bool found; 2173 bool found;
2121 int64_t revision; 2174 int64_t revision;
2122 if (GetRevisionHeader(revision, call, "if-match")) 2175 std::string md5;
2123 { 2176 if (GetRevisionHeader(revision, md5, call, "if-match"))
2124 found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision); 2177 {
2178 found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision, md5);
2125 } 2179 }
2126 else 2180 else
2127 { 2181 {
2128 OrthancConfiguration::ReaderLock lock; 2182 OrthancConfiguration::ReaderLock lock;
2129 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) 2183 if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
2131 throw OrthancException(ErrorCode_Revision, 2185 throw OrthancException(ErrorCode_Revision,
2132 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\""); 2186 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\"");
2133 } 2187 }
2134 else 2188 else
2135 { 2189 {
2136 found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, false, -1 /* dummy value */); 2190 found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType,
2191 false, -1 /* dummy value */, "" /* dummy value */);
2137 } 2192 }
2138 } 2193 }
2139 2194
2140 if (found) 2195 if (found)
2141 { 2196 {
2984 index.GetChildInstances(instances, *study); 3039 index.GetChildInstances(instances, *study);
2985 3040
2986 for (std::list<std::string>::const_iterator 3041 for (std::list<std::string>::const_iterator
2987 instance = instances.begin(); instance != instances.end(); ++instance) 3042 instance = instances.begin(); instance != instances.end(); ++instance)
2988 { 3043 {
2989 index.DeleteAttachment(*instance, FileContentType_DicomAsJson, false /* no revision checks */, -1 /* dummy */); 3044 index.DeleteAttachment(*instance, FileContentType_DicomAsJson,
3045 false /* no revision checks */, -1 /* dummy */, "" /* dummy */);
2990 } 3046 }
2991 } 3047 }
2992 3048
2993 call.GetOutput().AnswerBuffer("", MimeType_PlainText); 3049 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
2994 } 3050 }