Mercurial > hg > orthanc
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 } |