From 79a31173b0644813fa576dd8d47d0e391e80b48b Mon Sep 17 00:00:00 2001 From: Mano Segransan Date: Mon, 6 Nov 2023 17:05:54 +0100 Subject: [PATCH] Start implementation of S3 as an HttpExtHandler --- .clang-format | 259 ++++++++ .gitignore | 2 + src/CMakeLists.txt | 1 + src/XrdHttp/XrdHttpExtHandler.cc | 8 + src/XrdHttp/XrdHttpExtHandler.hh | 3 + src/XrdHttp/XrdHttpProtocol.cc | 1 + src/XrdS3.cmake | 90 +++ src/XrdS3/S3Response.cc | 335 ++++++++++ src/XrdS3/S3Response.hh | 57 ++ src/XrdS3/TODO.md | 38 ++ src/XrdS3/XrdS3.cc | 546 ++++++++++++++++ src/XrdS3/XrdS3.hh | 66 ++ src/XrdS3/XrdS3Action.hh | 110 ++++ src/XrdS3/XrdS3Api.cc | 900 ++++++++++++++++++++++++++ src/XrdS3/XrdS3Api.hh | 296 +++++++++ src/XrdS3/XrdS3Auth.cc | 358 +++++++++++ src/XrdS3/XrdS3Auth.hh | 107 ++++ src/XrdS3/XrdS3Crypt.cc | 79 +++ src/XrdS3/XrdS3Crypt.hh | 169 +++++ src/XrdS3/XrdS3ErrorResponse.hh | 208 ++++++ src/XrdS3/XrdS3ObjectStore.cc | 1017 ++++++++++++++++++++++++++++++ src/XrdS3/XrdS3ObjectStore.hh | 188 ++++++ src/XrdS3/XrdS3Req.cc | 324 ++++++++++ src/XrdS3/XrdS3Req.hh | 100 +++ src/XrdS3/XrdS3Router.cc | 90 +++ src/XrdS3/XrdS3Router.hh | 81 +++ src/XrdS3/XrdS3Utils.cc | 153 +++++ src/XrdS3/XrdS3Utils.hh | 83 +++ src/XrdS3/XrdS3Xml.cc | 27 + src/XrdS3/XrdS3Xml.hh | 34 + src/XrdS3/export-lib-symbols | 7 + src/XrdVersionPlugin.hh | 1 + 32 files changed, 5738 insertions(+) create mode 100644 .clang-format create mode 100644 src/XrdS3.cmake create mode 100644 src/XrdS3/S3Response.cc create mode 100644 src/XrdS3/S3Response.hh create mode 100644 src/XrdS3/TODO.md create mode 100644 src/XrdS3/XrdS3.cc create mode 100644 src/XrdS3/XrdS3.hh create mode 100644 src/XrdS3/XrdS3Action.hh create mode 100644 src/XrdS3/XrdS3Api.cc create mode 100644 src/XrdS3/XrdS3Api.hh create mode 100644 src/XrdS3/XrdS3Auth.cc create mode 100644 src/XrdS3/XrdS3Auth.hh create mode 100644 src/XrdS3/XrdS3Crypt.cc create mode 100644 src/XrdS3/XrdS3Crypt.hh create mode 100644 src/XrdS3/XrdS3ErrorResponse.hh create mode 100644 src/XrdS3/XrdS3ObjectStore.cc create mode 100644 src/XrdS3/XrdS3ObjectStore.hh create mode 100644 src/XrdS3/XrdS3Req.cc create mode 100644 src/XrdS3/XrdS3Req.hh create mode 100644 src/XrdS3/XrdS3Router.cc create mode 100644 src/XrdS3/XrdS3Router.hh create mode 100644 src/XrdS3/XrdS3Utils.cc create mode 100644 src/XrdS3/XrdS3Utils.hh create mode 100644 src/XrdS3/XrdS3Xml.cc create mode 100644 src/XrdS3/XrdS3Xml.hh create mode 100644 src/XrdS3/export-lib-symbols diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000000..37c8ab9c585 --- /dev/null +++ b/.clang-format @@ -0,0 +1,259 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveDeclarations: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveMacros: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: + Kind: Always + OverEmptyLines: 0 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakAfterAttributes: Never +BreakAfterJavaFieldAnnotations: false +BreakArrays: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: Always +BreakBeforeBraces: Attach +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: true +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 3 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: AfterExternBlock +IndentGotoLabels: true +IndentPPDirectives: None +IndentRequiresClause: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertBraces: false +InsertNewlineAtEOF: false +InsertTrailingCommas: None +IntegerLiteralSeparator: + Binary: 0 + BinaryMinDigits: 0 + Decimal: 0 + DecimalMinDigits: 0 + Hex: 0 + HexMinDigits: 0 +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PackConstructorInitializers: NextLine +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +PPIndentWidth: -1 +QualifierAlignment: Leave +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + - ParseTestProto + - ParsePartialTestProto + CanonicalDelimiter: pb + BasedOnStyle: google +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RemoveSemicolon: false +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseTab: Never +WhitespaceSensitiveMacros: + - BOOST_PP_STRINGIZE + - CF_SWIFT_NAME + - NS_SWIFT_NAME + - PP_STRINGIZE + - STRINGIZE +... + diff --git a/.gitignore b/.gitignore index 11165537c4d..14aab31fb20 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ docker/xrootd.tar.gz # Python build artifacts dist/ *.egg-info +cmake-build-debug +.idea/ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 39d7ae578b4..4047b4c8107 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -60,6 +60,7 @@ if( NOT XRDCL_ONLY ) if( BUILD_HTTP ) include( XrdHttp ) include( XrdTpc ) + include( XrdS3 ) endif() if( BUILD_MACAROONS ) diff --git a/src/XrdHttp/XrdHttpExtHandler.cc b/src/XrdHttp/XrdHttpExtHandler.cc index caa77fa9c86..0d59956a54c 100644 --- a/src/XrdHttp/XrdHttpExtHandler.cc +++ b/src/XrdHttp/XrdHttpExtHandler.cc @@ -70,6 +70,14 @@ int XrdHttpExtReq::BuffgetData(int blen, char **data, bool wait) { return nb; } +int XrdHttpExtReq::BuffgetLine(XrdOucString &dest) { + + if (!prot) return -1; + int nb = prot->BuffgetLine(dest); + + return nb; +} + void XrdHttpExtReq::GetClientID(std::string &clid) { char buff[512]; diff --git a/src/XrdHttp/XrdHttpExtHandler.hh b/src/XrdHttp/XrdHttpExtHandler.hh index 60ea711d317..6aecff3714a 100644 --- a/src/XrdHttp/XrdHttpExtHandler.hh +++ b/src/XrdHttp/XrdHttpExtHandler.hh @@ -37,6 +37,7 @@ #include #include "XrdNet/XrdNetPMark.hh" +#include "XrdOuc/XrdOucString.hh" class XrdLink; class XrdSecEntity; @@ -72,6 +73,8 @@ public: /// Get a pointer to data read from the client, valid for up to blen bytes from the buffer. Returns the validity int BuffgetData(int blen, char **data, bool wait); + int BuffgetLine(XrdOucString &dest); + /// Sends a basic response. If the length is < 0 then it is calculated internally int SendSimpleResp(int code, const char *desc, const char *header_to_add, const char *body, long long bodylen); diff --git a/src/XrdHttp/XrdHttpProtocol.cc b/src/XrdHttp/XrdHttpProtocol.cc index 0004cdee23d..73924ed9664 100644 --- a/src/XrdHttp/XrdHttpProtocol.cc +++ b/src/XrdHttp/XrdHttpProtocol.cc @@ -1579,6 +1579,7 @@ int XrdHttpProtocol::StartSimpleResp(int code, const char *desc, const char *hea else if (code == 403) ss << "Forbidden"; else if (code == 404) ss << "Not Found"; else if (code == 405) ss << "Method Not Allowed"; + else if (code == 411) ss << "Length Required"; else if (code == 416) ss << "Range Not Satisfiable"; else if (code == 500) ss << "Internal Server Error"; else if (code == 504) ss << "Gateway Timeout"; diff --git a/src/XrdS3.cmake b/src/XrdS3.cmake new file mode 100644 index 00000000000..7f4a304b118 --- /dev/null +++ b/src/XrdS3.cmake @@ -0,0 +1,90 @@ + +#------------------------------------------------------------------------------- +# Modules +#------------------------------------------------------------------------------- +set(LIB_XRD_S3 XrdHttpS3-${PLUGIN_VERSION}) + +#------------------------------------------------------------------------------- +# Shared library version +#------------------------------------------------------------------------------- + +if (BUILD_TPC) + #----------------------------------------------------------------------------- + # The XrdHttp library + #----------------------------------------------------------------------------- + + if (TINYXML_FOUND) + set(TINYXML_FILES "") + set(TINYXML_LIBRARIES ${TINYXML_LIBRARIES}) + else () + set(TINYXML_FILES + XrdXml/tinyxml/tinystr.cpp XrdXml/tinyxml/tinystr.h + XrdXml/tinyxml/tinyxml.cpp XrdXml/tinyxml/tinyxml.h + XrdXml/tinyxml/tinyxmlerror.cpp + XrdXml/tinyxml/tinyxmlparser.cpp) + set(TINYXML_LIBRARIES "") + endif () + + add_library( + ${LIB_XRD_S3} + MODULE + XrdS3/XrdS3.cc XrdS3/XrdS3.hh + XrdS3/XrdS3Utils.cc + XrdS3/XrdS3Utils.hh + XrdS3/XrdS3Crypt.cc + XrdS3/XrdS3Crypt.hh + XrdS3/XrdS3Auth.cc + XrdS3/XrdS3Auth.hh + XrdS3/XrdS3Router.cc + XrdS3/XrdS3Router.hh + XrdS3/XrdS3Req.cc + XrdS3/XrdS3Req.hh + ${TINYXML_FILES} + XrdS3/XrdS3Xml.cc + XrdS3/XrdS3Xml.hh + XrdS3/XrdS3ErrorResponse.hh + XrdS3/XrdS3Api.cc + XrdS3/XrdS3Api.hh + XrdS3/S3Response.cc + XrdS3/S3Response.hh + XrdS3/XrdS3ObjectStore.cc + XrdS3/XrdS3ObjectStore.hh + XrdS3/XrdS3Action.hh + ) + + target_link_libraries( + ${LIB_XRD_S3} + PRIVATE + XrdServer + XrdUtils + XrdHttpUtils + tinyxml2 + ${CMAKE_DL_LIBS} + ${CMAKE_THREAD_LIBS_INIT}) + + if (TINYXML_FOUND) + target_include_directories(${LIB_XRD_S3} PRIVATE ${TINYXML_INCLUDE_DIR}) + else () + target_include_directories(${LIB_XRD_S3} PRIVATE XrdXml/tinyxml) + endif () + + if (MacOSX) + set(S3_LINK_FLAGS, "-Wl") + else () + set(S3_LINK_FLAGS, "-Wl,--version-script=${CMAKE_SOURCE_DIR}/src/XrdS3/export-lib-symbols") + endif () + + set_target_properties( + ${LIB_XRD_S3} + PROPERTIES + LINK_FLAGS "${S3_LINK_FLAGS}" + COMPILE_DEFINITIONS "${XRD_COMPILE_DEFS}") + + #----------------------------------------------------------------------------- + # Install + #----------------------------------------------------------------------------- + install( + TARGETS ${LIB_XRD_S3} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +endif () diff --git a/src/XrdS3/S3Response.cc b/src/XrdS3/S3Response.cc new file mode 100644 index 00000000000..2bff5ad48ee --- /dev/null +++ b/src/XrdS3/S3Response.cc @@ -0,0 +1,335 @@ +// +// Created by segransm on 11/16/23. +// + +#include "S3Response.hh" + +#include + +#include "XrdS3ObjectStore.hh" +#include "XrdS3Xml.hh" + +namespace S3 { + +int ListBucketsResponse(XrdS3Req& req, const std::string& id, + const std::string& display_name, + const std::vector& buckets) { + S3Xml printer; + + printer.OpenElement("ListAllMyBucketsResult"); + printer.OpenElement("Owner"); + printer.AddElement("ID", id); + printer.AddElement("DisplayName", display_name); + printer.CloseElement(); + + printer.OpenElement("Buckets"); + + for (const auto& bucket : buckets) { + printer.OpenElement("Bucket"); + printer.AddElement("Name", bucket.name); + printer.AddElement("CreationDate", bucket.created); + printer.CloseElement(); + } + + printer.CloseElement(); + printer.CloseElement(); + + std::string hd = "Content-Type: application/xml"; + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +int ListObjectVersionsResponse(XrdS3Req& req, const std::string& bucket, + const bool encode, const char delimiter, + const int max_keys, const std::string& prefix, + const ListObjectsInfo& vinfo) { + S3Xml printer; + auto encoder = [req, encode](const std::string& str) { + return encode ? req.ctx->utils.ObjectUriEncode(str) : str; + }; + + printer.OpenElement("ListVersionsResult"); + + // todo: put common code like this in separate func + for (const auto& pfx : vinfo.common_prefixes) { + printer.OpenElement("CommonPrefixes"); + printer.AddElement("Prefix", encoder(pfx)); + printer.CloseElement(); + } + + // todo: delete marker + // printer.AddElement("DeleteMarker", ""); + + if (delimiter) { + printer.AddElement("Delimiter", encoder(std::string(1, delimiter))); + } + + if (encode) { + printer.AddElement("EncodingType", "url"); + } + + printer.AddElement("IsTruncated", vinfo.is_truncated); + + printer.AddElement("KeyMarker", encoder(vinfo.key_marker)); + + printer.AddElement("MaxKeys", max_keys); + printer.AddElement("Name", bucket); + + if (!vinfo.next_marker.empty()) { + printer.AddElement("NextKeyMarker", encoder(vinfo.next_marker)); + } + if (!vinfo.next_vid_marker.empty()) { + printer.AddElement("NextVersionIdMarker", vinfo.next_vid_marker); + } + printer.AddElement("VersionIdMarker", vinfo.vid_marker); + printer.AddElement("Prefix", encoder(prefix)); + for (const auto& version : vinfo.objects) { + printer.OpenElement("Version"); + // todo: all fields + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ObjectVersion.html + + printer.AddElement("Key", encoder(version.name)); + printer.AddElement("LastModified", + S3Utils::timestampToIso8016(version.last_modified)); + printer.AddElement("Size", version.size); + // todo: + printer.AddElement("VersionId", "1"); + printer.CloseElement(); + } + + printer.CloseElement(); + std::string hd = "Content-Type: application/xml"; + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +int DeleteObjectsResponse(XrdS3Req& req, bool quiet, + const std::vector& deleted, + const std::vector& err) { + S3Xml printer; + + printer.OpenElement("DeleteResult"); + + if (!quiet) { + for (const auto& d : deleted) { + printer.OpenElement("Deleted"); + printer.AddElement("DeleteMarker", d.delete_marker); + printer.AddElement("DeleteMarkerVersionId", d.delete_marker_version_id); + printer.AddElement("Key", d.key); + printer.AddElement("VersionId", d.version_id); + printer.CloseElement(); + } + } + + for (const auto& e : err) { + printer.OpenElement("Error"); + printer.AddElement("Code", S3ErrorMap.find(e.code)->second.code); + printer.AddElement("Key", e.key); + printer.AddElement("Message", e.message); + printer.AddElement("VersionId", e.version_id); + printer.CloseElement(); + } + + printer.CloseElement(); + + std::string hd = "Content-Type: application/xml"; + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +int ListObjectsV2Response(XrdS3Req& req, const std::string& bucket, + const std::string& prefix, + const std::string& continuation_token, + const char delimiter, const int max_keys, + const bool fetch_owner, + const std::string& start_after, const bool encode, + const ListObjectsInfo& oinfo) { + S3Xml printer; + auto encoder = [req, encode](const std::string& str) { + return encode ? req.ctx->utils.ObjectUriEncode(str) : str; + }; + + printer.OpenElement("ListBucketResult"); + printer.AddElement("Name", bucket); + printer.AddElement("MaxKeys", max_keys); + printer.AddElement("ContinuationToken", encoder(continuation_token)); + if (encode) { + printer.AddElement("EncodingType", "url"); + } + + if (delimiter) { + printer.AddElement("Delimiter", encoder(std::string(1, delimiter))); + } + + if (!start_after.empty()) { + printer.AddElement("StartAfter", encoder(start_after)); + } + + printer.AddElement("Prefix", encoder(prefix)); + + printer.AddElement("KeyCount", + oinfo.objects.size() + oinfo.common_prefixes.size()); + printer.AddElement("IsTruncated", oinfo.is_truncated); + if (oinfo.is_truncated) { + printer.AddElement("NextContinuationToken", encoder(oinfo.key_marker)); + } + + for (const auto& object : oinfo.objects) { + printer.OpenElement("Contents"); + printer.AddElement("Key", encoder(object.name)); + printer.AddElement("LastModified", + S3Utils::timestampToIso8016(object.last_modified)); + printer.AddElement("Size", object.size); + if (fetch_owner) { + printer.OpenElement("Owner"); + // todo: display name + printer.AddElement("ID", object.owner); + printer.CloseElement(); + } + printer.CloseElement(); + } + + for (const auto& pfx : oinfo.common_prefixes) { + printer.OpenElement("CommonPrefixes"); + printer.AddElement("Prefix", encoder(pfx)); + printer.CloseElement(); + } + + printer.CloseElement(); + + std::string hd = "Content-Type: application/xml"; + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +int ListObjectsResponse(XrdS3Req& req, const std::string& bucket, + const std::string& prefix, const char delimiter, + const std::string& marker, const int max_keys, + const bool encode, const ListObjectsInfo& objects) { + S3Xml printer; + auto encoder = [req, encode](const std::string& str) { + return encode ? req.ctx->utils.ObjectUriEncode(str) : str; + }; + + printer.OpenElement("ListBucketResult"); + + for (const auto& pfx : objects.common_prefixes) { + printer.OpenElement("CommonPrefixes"); + printer.AddElement("Prefix", encoder(pfx)); + printer.CloseElement(); + } + + for (const auto& object : objects.objects) { + printer.OpenElement("Contents"); + printer.AddElement("Key", encoder(object.name)); + printer.AddElement("LastModified", + S3Utils::timestampToIso8016(object.last_modified)); + printer.AddElement("Size", object.size); + printer.CloseElement(); + } + + if (delimiter) { + printer.AddElement("Delimiter", encoder(std::string(1, delimiter))); + } + + if (encode) { + printer.AddElement("EncodingType", "url"); + } + printer.AddElement("IsTruncated", objects.is_truncated); + + printer.AddElement("Marker", encoder(marker)); + printer.AddElement("MaxKeys", max_keys); + printer.AddElement("Name", bucket); + + if (objects.is_truncated && delimiter != 0) { + printer.AddElement("NextMarker", encoder(objects.key_marker)); + } + + printer.AddElement("Prefix", prefix); + + printer.CloseElement(); + + std::string hd = "Content-Type: application/xml"; + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +int CopyObjectResponse(XrdS3Req& req, const std::string& etag) { + S3Xml printer; + + printer.OpenElement("CopyObjectResult"); + + printer.AddElement("ETag", etag); + printer.CloseElement(); + + return req.ChunkResp(printer.CStr(), printer.CStrSize() - 1); +} + +int CreateMultipartUploadResponse(XrdS3Req& req, const std::string& upload_id) { + S3Xml printer; + printer.OpenElement("InitiateMultipartUploadResult"); + + printer.AddElement("Bucket", req.bucket); + printer.AddElement("Key", req.object); + printer.AddElement("UploadId", upload_id); + + printer.CloseElement(); + + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} +int ListMultipartUploadResponse( + XrdS3Req& req, + const std::vector& uploads) { + S3Xml printer; + printer.OpenElement("ListMultipartUploadsResult"); + + printer.AddElement("Bucket", req.bucket); + + for (const auto& [key, upload_id] : uploads) { + printer.OpenElement("Upload"); + + printer.AddElement("Key", key); + printer.AddElement("UploadId", upload_id); + + printer.CloseElement(); + } + + printer.CloseElement(); + + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +int ListPartsResponse(XrdS3Req& req, const std::string& upload_id, + const std::vector& parts) { + S3Xml printer; + printer.OpenElement("ListPartsResult"); + + printer.AddElement("Bucket", req.bucket); + printer.AddElement("Key", req.object); + printer.AddElement("UploadId", upload_id); + + for (const auto& [etag, last_modified, part_number, size] : parts) { + printer.OpenElement("Part"); + + printer.AddElement("ETag", etag); + printer.AddElement("LastModified", + S3Utils::timestampToIso8016(last_modified)); + printer.AddElement("PartNumber", part_number); + printer.AddElement("Size", size); + + printer.CloseElement(); + } + + printer.CloseElement(); + + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +int CompleteMultipartUploadResponse(XrdS3Req& req) { + S3Xml printer; + printer.OpenElement("CompleteMultipartUploadResult"); + + printer.AddElement("Bucket", req.bucket); + printer.AddElement("Key", req.object); + + printer.CloseElement(); + + return req.S3Response(200, {}, printer.CStr(), printer.CStrSize() - 1); +} + +} // namespace S3 diff --git a/src/XrdS3/S3Response.hh b/src/XrdS3/S3Response.hh new file mode 100644 index 00000000000..96ab4d81e96 --- /dev/null +++ b/src/XrdS3/S3Response.hh @@ -0,0 +1,57 @@ +// +// Created by segransm on 11/16/23. +// + +#ifndef XROOTD_S3RESPONSE_HH +#define XROOTD_S3RESPONSE_HH + +#include +#include + +#include "XrdS3ObjectStore.hh" +#include "XrdS3Req.hh" + +namespace S3 { + +int ListBucketsResponse(XrdS3Req& req, const std::string& id, + const std::string& display_name, + const std::vector& buckets); + +int ListObjectVersionsResponse(XrdS3Req& req, const std::string& bucket, + bool encode, char delimiter, int max_keys, + const std::string& prefix, + const ListObjectsInfo& vinfo); + +int DeleteObjectsResponse(XrdS3Req& req, bool quiet, + const std::vector& deleted, + const std::vector& err); + +int ListObjectsV2Response(XrdS3Req& req, const std::string& bucket, + const std::string& prefix, + const std::string& continuation_token, char delimiter, + int max_keys, bool fetch_owner, + const std::string& start_after, bool encode, + const ListObjectsInfo& oinfo); + +int ListObjectsResponse(XrdS3Req& req, const std::string& bucket, + const std::string& prefix, char delimiter, + const std::string& marker, int max_keys, bool encode, + const ListObjectsInfo& objects); + +int CopyObjectResponse(XrdS3Req& req, const std::string& etag); + +int CreateMultipartUploadResponse(XrdS3Req& req, const std::string& upload_id); + +int ListMultipartUploadResponse( + XrdS3Req& req, + const std::vector& uploads); + + +int ListPartsResponse(XrdS3Req& req, const std::string& upload_id, + const std::vector& parts); + +int CompleteMultipartUploadResponse(XrdS3Req& req); + +} // namespace S3 + +#endif // XROOTD_S3RESPONSE_HH diff --git a/src/XrdS3/TODO.md b/src/XrdS3/TODO.md new file mode 100644 index 00000000000..8d094d067a3 --- /dev/null +++ b/src/XrdS3/TODO.md @@ -0,0 +1,38 @@ +# Must do +- Global store/auth? +- Finish refactor +- Verify date when authorizing +- Possible to create objects with name ending with a '/' +- Possible to create object and dir with same name? +- request id header? +- metadata +- key name: utf8, 1024 bytes max +- limit header size (8KB)? +- combine headers if same name? +- combine query params if same name? +- verify signature once body has been read (if no body check against empty hash signature) +- user metadata max 2KB +- parse all metadata headers (check all headers for put request) +- checksum headers +- handle versions and version headers +- copy object: x-amz-metadata-directive +- get object: ranges/parts + +- copy operation +- acl +- versioning +- anonymous +- multipart upload + +- Maybe only support Directory Buckets at the beginning, would simplify stuff +- Common function for all list operations, including list multipart uploads + +- check `test_object_raw_put_authenticated_expired` test + +- lock fs operations +- If we move to C++17: stringview + +# Get more info +- SIGV2 +- SIGV4A +- SOAP diff --git a/src/XrdS3/XrdS3.cc b/src/XrdS3/XrdS3.cc new file mode 100644 index 00000000000..a1ce5d2d939 --- /dev/null +++ b/src/XrdS3/XrdS3.cc @@ -0,0 +1,546 @@ +#include "XrdS3.hh" + +#include + +#include +#include + +#include "XrdHttp/XrdHttpExtHandler.hh" +#include "XrdOuc/XrdOucEnv.hh" +#include "XrdOuc/XrdOucStream.hh" +#include "XrdOuc/XrdOucString.hh" +#include "XrdS3ErrorResponse.hh" +#include "XrdVersion.hh" + +namespace S3 { +XrdVERSIONINFO(XrdHttpGetExtHandler, HttpS3); + +int NotFoundHandler(XrdS3Req &req) { + // todo: send error msg + return req.S3ErrorResponse(S3Error::NoSuchAccessPoint); +} + +S3Handler::S3Handler(XrdSysError *log, const char *config, XrdOucEnv *myEnv) + : mLog(log->logger(), "S3_"), + mApi(), + mRouter(&mLog, NotFoundHandler) { + if (!ParseConfig(config, *myEnv)) { + throw std::runtime_error("Failed to configure the HTTP S3 handler."); + } + + ctx.log = &mLog; + + mApi = S3Api(mConfig.data_dir, mConfig.auth_dir); + + ConfigureRouter(); + + mLog.Say("Finished configuring S3 Handler"); +} + +void S3Handler::ConfigureRouter() { +#define AC(f) Action::f + +#define HANDLER(f) \ + Action::f, #f, [this](XrdS3Req &req) { return mApi.f##Handler(req); } + // The router needs to be initialized in the right order, with the most + // restrictive matcher first. + + /* -------------------------------------------------------------------------*/ + /* HEAD */ + /* -------------------------------------------------------------------------*/ + + // HeadObject + mRouter.AddRoute(S3Route(HANDLER(HeadObject)) + .Method(HttpMethod::Head) + .Path(PathMatch::MatchObject)); + // HeadBucket + mRouter.AddRoute(S3Route(HANDLER(HeadBucket)) + .Method(HttpMethod::Head) + .Path(PathMatch::MatchBucket)); + + /* -------------------------------------------------------------------------*/ + /* GET */ + /* -------------------------------------------------------------------------*/ + + /* + * MatchObject + * */ + + mRouter.AddRoute(S3Route(HANDLER(GetObjectAcl)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"acl", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetObjectAttributes)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"attributes", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetObjectLegalHold)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"legal-hold", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetObjectLockConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"object-lock", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetObjectRetention)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"retention", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetObjectTagging)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"tagging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetObjectTorrent)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"torrent", ""}})); + mRouter.AddRoute(S3Route(HANDLER(ListParts)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject) + .Queries({{"uploadId", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(GetObject)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchObject)); + + /* + * MatchBucket + * */ + mRouter.AddRoute(S3Route(HANDLER(ListObjectsV2)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"list-type", "2"}})); + mRouter.AddRoute(S3Route(HANDLER(ListObjectVersions)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"versions", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketAccelerateConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"accelerate", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketAcl)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"acl", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketAnalyticsConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"analytics", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(ListBucketAnalyticsConfigurations)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"analytics", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketCors)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"cors", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketEncryption)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"encryption", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketIntelligentTieringConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"intelligent-tiering", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(ListBucketIntelligentTieringConfigurations)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"inteligent-tiering", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketInventoryConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"inventory", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(ListBucketInventoryConfigurations)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"inventory", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketLifecycleConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"lifecycle", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketLocation)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"location", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketLogging)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"logging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketMetricsConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"metrics", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(ListBucketMetricsConfigurations)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"metrics", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketNotificationConfiguration)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"notification", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketOwnershipControls)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"ownershipControls", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketPolicy)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"policy", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketPolicyStatus)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"policyStatus", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketReplication)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"replication", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketRequestPayment)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"requestPayment", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketTagging)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"tagging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketVersioning)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"versioning", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetBucketWebsite)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"website", ""}})); + mRouter.AddRoute(S3Route(HANDLER(GetPublicAccessBlock)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"publicAccessBlock", ""}})); + mRouter.AddRoute(S3Route(HANDLER(ListMultipartUploads)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket) + .Queries({{"uploads", ""}})); + mRouter.AddRoute(S3Route(HANDLER(ListObjects)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchBucket)); + + /* + * MatchNoBucket + * */ + mRouter.AddRoute(S3Route(HANDLER(ListBuckets)) + .Method(HttpMethod::Get) + .Path(PathMatch::MatchNoBucket)); + + /* -------------------------------------------------------------------------*/ + /* PUT */ + /* -------------------------------------------------------------------------*/ + + /* + * MatchObject + * */ + + mRouter.AddRoute(S3Route(HANDLER(PutObjectAcl)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Queries({{"acl", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutObjectLegalHold)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Queries({{"legal-hold", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutObjectLockConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Queries({{"object-lock", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutObjectRetention)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Queries({{"retention", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutObjectTagging)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Queries({{"tagging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(UploadPartCopy)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Queries({{"partNumber", "+"}, {"uploadId", "+"}}) + .Headers({{"x-amz-copy-source", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(UploadPart)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Queries({{"partNumber", "+"}, {"uploadId", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(CopyObject)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject) + .Headers({{"x-amz-copy-source", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(PutObject)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchObject)); + + /* + * MatchBucket + * */ + mRouter.AddRoute(S3Route(HANDLER(PutBucketAccelerateConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"accelerate", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketAcl)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"acl", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketAnalyticsConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"analytics", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketCors)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"cors", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketEncryption)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"encryption", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketIntelligentTieringConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"intelligent-tiering", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketInventoryConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"inventory", ""}, {"id", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketLifecycleConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"lifecycle", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketLogging)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"logging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketMetricsConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"metrics", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketNotificationConfiguration)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"notification", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketOwnershipControls)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"ownershipControls", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketPolicy)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"policy", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketReplication)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"replication", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketRequestPayment)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"requestPayment", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketTagging)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"tagging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketVersioning)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"versioning", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutBucketWebsite)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"website", ""}})); + mRouter.AddRoute(S3Route(HANDLER(PutPublicAccessBlock)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket) + .Queries({{"publicAccessBlock", ""}})); + mRouter.AddRoute(S3Route(HANDLER(CreateBucket)) + .Method(HttpMethod::Put) + .Path(PathMatch::MatchBucket)); + + /* -------------------------------------------------------------------------*/ + /* DELETE */ + /* -------------------------------------------------------------------------*/ + + /* + * MatchObject + * */ + mRouter.AddRoute(S3Route(HANDLER(AbortMultipartUpload)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchObject) + .Queries({{"uploadId", "+"}})); + + mRouter.AddRoute(S3Route(HANDLER(DeleteObjectTagging)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchObject) + .Queries({{"tagging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteObject)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchObject)); + + /* + * MatchBucket + * */ + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketAnalyticsConfiguration)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"analytics", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketCors)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"cors", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketEncryption)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"encryption", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketIntelligentTieringConfiguration)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"intelligent-tiering", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketInventoryConfiguration)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"inventory", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketLifecycle)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"lifecycle", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketMetricsConfiguration)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"metrics", ""}, {"id", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketOwnershipControls)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"ownershipControls", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketPolicy)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"policy", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketReplication)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"replication", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketTagging)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"tagging", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucketWebsite)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"website", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeletePublicAccessBlock)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket) + .Queries({{"publicAcccessBlock", ""}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteBucket)) + .Method(HttpMethod::Delete) + .Path(PathMatch::MatchBucket)); + + /* -------------------------------------------------------------------------*/ + /* POST */ + /* -------------------------------------------------------------------------*/ + mRouter.AddRoute(S3Route(HANDLER(CreateMultipartUpload)) + .Method(HttpMethod::Post) + .Path(PathMatch::MatchObject) + .Queries({{"uploads", ""}})); + mRouter.AddRoute(S3Route(HANDLER(RestoreObject)) + .Method(HttpMethod::Post) + .Path(PathMatch::MatchObject) + .Queries({{"restore", ""}})); + mRouter.AddRoute(S3Route(HANDLER(SelectObjectContent)) + .Method(HttpMethod::Post) + .Path(PathMatch::MatchObject) + .Queries({{"select", ""}, {"select-type", "2"}})); + + mRouter.AddRoute(S3Route(HANDLER(CompleteMultipartUpload)) + .Method(HttpMethod::Post) + .Path(PathMatch::MatchObject) + .Queries({{"uploadId", "+"}})); + mRouter.AddRoute(S3Route(HANDLER(DeleteObjects)) + .Method(HttpMethod::Post) + .Path(PathMatch::MatchBucket) + .Queries({{"delete", ""}})); + + // todo: + // mRouter.AddRoute(S3Route(HANDLER(WriteGetObjectResponse)) + // .Method(HttpMethod::Post) + // .Path(PathMatch::MatchObject) + // .Queries({{"", ""}})); + +#undef HANDLER +} + +bool S3Handler::ParseConfig(const char *config, XrdOucEnv &env) { + XrdOucStream Config(&mLog, getenv("XRDINSTANCE"), &env, "=====> "); + + auto fd = open(config, O_RDONLY); + + if (fd < 0) { + return false; + } + + Config.Attach(fd); + + const char *val; + + while ((val = Config.GetMyFirstWord())) { + if (!strcmp("s3.authdir", val)) { + if (!(val = Config.GetWord())) { + Config.Close(); + return false; + } + mConfig.auth_dir = val; + } else if (!strcmp("s3.datadir", val)) { + if (!(val = Config.GetWord())) { + Config.Close(); + return false; + } + mConfig.data_dir = val; + } else if (!strcmp("s3.region", val)) { + if (!(val = Config.GetWord())) { + Config.Close(); + return false; + } + mConfig.region = val; + } else if (!strcmp("s3.service", val)) { + if (!(val = Config.GetWord())) { + Config.Close(); + return false; + } + mConfig.service = val; + } + } + Config.Close(); + + return (!mConfig.data_dir.empty() && !mConfig.auth_dir.empty() && + !mConfig.service.empty() && !mConfig.region.empty()); +} + +S3Handler::~S3Handler() = default; + +bool S3Handler::MatchesPath(const char *verb, const char *path) { + // match all paths for now + return true; +} + +int S3Handler::ProcessReq(XrdHttpExtReq &req) { + XrdS3Req s3req(&ctx, req); + + // todo: s3req.Validate() -> S3Error + // ex: return S3ErrorREsponse(err) + // todo: include s3errorresponse in s3req class + // ex: s3req.SendError() + if (!s3req.isValid()) { + return 1; + } + + return mRouter.ProcessReq(s3req); +} + +extern "C" { + +XrdHttpExtHandler *XrdHttpGetExtHandler(XrdSysError *log, const char *config, + const char *parms, XrdOucEnv *myEnv) { + return new S3Handler(log, config, myEnv); +} +} +}; // namespace S3 diff --git a/src/XrdS3/XrdS3.hh b/src/XrdS3/XrdS3.hh new file mode 100644 index 00000000000..cbe65e58e6d --- /dev/null +++ b/src/XrdS3/XrdS3.hh @@ -0,0 +1,66 @@ + +#ifndef XROOTD_XRDS3_HH +#define XROOTD_XRDS3_HH + +#include +#include + +#include "XrdHttp/XrdHttpChecksumHandler.hh" +#include "XrdHttp/XrdHttpExtHandler.hh" +#include "XrdS3Api.hh" +#include "XrdS3Auth.hh" +#include "XrdS3Crypt.hh" +#include "XrdS3Router.hh" +#include "XrdS3Utils.hh" +#include "XrdSys/XrdSysError.hh" +#include "XrdXrootd/XrdXrootdProtocol.hh" + +namespace S3 { + +class S3Handler : public XrdHttpExtHandler { + public: + S3Handler(XrdSysError *log, const char *config, XrdOucEnv *myEnv); + + ~S3Handler() override; + + bool MatchesPath(const char *verb, const char *path) override; + + int ProcessReq(XrdHttpExtReq &req) override; + + // Abstract method in the base class, but does not seem to be used + int Init(const char *cfgfile) override { return 0; } + + public: + // todo: global s3crypt etc. + Context ctx; + + private: + struct { + std::string data_dir; + std::string auth_dir; + std::string region; + std::string service; + } mConfig; + + XrdSysError mLog; + + S3Api mApi; + S3Router mRouter; + + // todo: dont calculate empty sha + const std::string EMPTY_SHA = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + // todo: size of biggest algorithm + // Algorithm + Date + Scope + CanonicalRequestHash + // const size_t STRING_TO_SIGN_LENGTH = + // 16 + 1 + 16 + 1 + + // (8 + 1 + mRegion.size() + 1 + mService.size() + 1 + 12) + 1 + 64; + + bool ParseConfig(const char *config, XrdOucEnv &env); + + void ConfigureRouter(); +}; + +} // namespace S3 + +#endif diff --git a/src/XrdS3/XrdS3Action.hh b/src/XrdS3/XrdS3Action.hh new file mode 100644 index 00000000000..3267fd88409 --- /dev/null +++ b/src/XrdS3/XrdS3Action.hh @@ -0,0 +1,110 @@ +// +// Created by segransm on 11/17/23. +// + +#ifndef XROOTD_XRDS3ACTION_HH +#define XROOTD_XRDS3ACTION_HH + +namespace S3 { + +enum class Action { + Unknown, + AbortMultipartUpload, + CompleteMultipartUpload, + CopyObject, + CreateBucket, + CreateMultipartUpload, + DeleteBucket, + DeleteBucketAnalyticsConfiguration, + DeleteBucketCors, + DeleteBucketEncryption, + DeleteBucketIntelligentTieringConfiguration, + DeleteBucketInventoryConfiguration, + DeleteBucketLifecycle, + DeleteBucketMetricsConfiguration, + DeleteBucketOwnershipControls, + DeleteBucketPolicy, + DeleteBucketReplication, + DeleteBucketTagging, + DeleteBucketWebsite, + DeleteObject, + DeleteObjects, + DeleteObjectTagging, + DeletePublicAccessBlock, + GetBucketAccelerateConfiguration, + GetBucketAcl, + GetBucketAnalyticsConfiguration, + GetBucketCors, + GetBucketEncryption, + GetBucketIntelligentTieringConfiguration, + GetBucketInventoryConfiguration, + GetBucketLifecycleConfiguration, + GetBucketLocation, + GetBucketLogging, + GetBucketMetricsConfiguration, + GetBucketNotificationConfiguration, + GetBucketOwnershipControls, + GetBucketPolicy, + GetBucketPolicyStatus, + GetBucketReplication, + GetBucketRequestPayment, + GetBucketTagging, + GetBucketVersioning, + GetBucketWebsite, + GetObject, + GetObjectAcl, + GetObjectAttributes, + GetObjectLegalHold, + GetObjectLockConfiguration, + GetObjectRetention, + GetObjectTagging, + GetObjectTorrent, + GetPublicAccessBlock, + HeadBucket, + HeadObject, + ListBucketAnalyticsConfigurations, + ListBucketIntelligentTieringConfigurations, + ListBucketInventoryConfigurations, + ListBucketMetricsConfigurations, + ListBuckets, + ListMultipartUploads, + ListObjects, + ListObjectsV2, + ListObjectVersions, + ListParts, + PutBucketAccelerateConfiguration, + PutBucketAcl, + PutBucketAnalyticsConfiguration, + PutBucketCors, + PutBucketEncryption, + PutBucketIntelligentTieringConfiguration, + PutBucketInventoryConfiguration, + PutBucketLifecycle, + PutBucketLifecycleConfiguration, + PutBucketLogging, + PutBucketMetricsConfiguration, + PutBucketNotification, + PutBucketNotificationConfiguration, + PutBucketOwnershipControls, + PutBucketPolicy, + PutBucketReplication, + PutBucketRequestPayment, + PutBucketTagging, + PutBucketVersioning, + PutBucketWebsite, + PutObject, + PutObjectAcl, + PutObjectLegalHold, + PutObjectLockConfiguration, + PutObjectRetention, + PutObjectTagging, + PutPublicAccessBlock, + RestoreObject, + SelectObjectContent, + UploadPart, + UploadPartCopy, + WriteGetObjectResponse, +}; +} + +#endif // XROOTD_XRDS3ACTION_HH diff --git a/src/XrdS3/XrdS3Api.cc b/src/XrdS3/XrdS3Api.cc new file mode 100644 index 00000000000..8ebe33e1a1d --- /dev/null +++ b/src/XrdS3/XrdS3Api.cc @@ -0,0 +1,900 @@ +// +// Created by segransm on 11/16/23. +// + +#include "XrdS3Api.hh" + +#include +#include + +#include "S3Response.hh" +#include "XrdCks/XrdCksCalcmd5.hh" +#include "XrdS3Auth.hh" +#include "XrdS3ErrorResponse.hh" + +namespace S3 { + +bool ParseCreateBucketBody(char* body, int length, std::string& location) { + tinyxml2::XMLDocument doc; + + doc.Parse(body, length); + + if (doc.Error()) { + return false; + } + + tinyxml2::XMLElement* elem = doc.RootElement(); + + if (elem == nullptr || + std::string(elem->Name()) != "CreateBucketConfiguration") { + return false; + } + + elem = elem->FirstChildElement(); + if (elem == nullptr) { + return false; + } + if (std::string(elem->Name()) != "LocationConstraint") { + return false; + } + + location = elem->GetText(); + + if (elem->NextSiblingElement() != nullptr) { + return false; + } + return true; +} + +int S3Api::CreateBucketHandler(XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::CreateBucket, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + int length = 0; + auto it = req.lowercase_headers.find("content-length"); + if (it != req.lowercase_headers.end()) { + try { + length = std::stoi(it->second); + } catch (std::exception&) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + } + if (length < 0) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + + std::string location; + if (length > 0) { + char* ptr; + if (req.ReadBody(length, &ptr, true) != length) { + return req.S3ErrorResponse(S3Error::IncompleteBody); + } + + if (!ParseCreateBucketBody(ptr, length, location) || location.empty()) { + return req.S3ErrorResponse(S3Error::MalformedXML); + } + } + + S3Error error = objectStore.CreateBucket(req.id, req.bucket, location); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + Headers headers = {{"Location", '/' + req.bucket}}; + return req.S3Response(200, headers, ""); +} + +int S3Api::ListBucketsHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::ListBuckets, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + auto buckets = objectStore.ListBuckets(req.id); + + // todo: display name + return ListBucketsResponse(req, req.id, "display name", buckets); +} + +int S3Api::HeadBucketHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::HeadBucket, + req.bucket, req.object); + // Head bucket does not return a body when an error occurs + if (err != S3Error::None) { + return req.S3Response(S3ErrorMap.find(err)->second.httpCode); + } + + return req.Ok(); +} + +int S3Api::DeleteBucketHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::DeleteBucket, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + // todo: we assume that bucket is owner by req.id; + S3Error error = objectStore.DeleteBucket(req.bucket); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + return req.S3Response(204); +} + +int S3Api::DeleteObjectHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::DeleteObject, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + // todo: req.path should be req.object + S3Error error = objectStore.DeleteObject(req.bucket, req.object); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + return req.S3Response(204); +} + +S3Error ValidatePreconditions(const std::string& etag, time_t last_modified, + const Headers& headers) { + // See https://datatracker.ietf.org/doc/html/rfc7232#section-6 for + // precondition evaluation order + auto it = headers.end(); + if ((it = headers.find("if-match")) != headers.end()) { + if (it->second != etag) { + return S3Error::PreconditionFailed; + } + } else { + if ((it = headers.find("if-unmodified-since")) != headers.end()) { + tm date{}; + auto ret = + strptime(it->second.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &date); + if (ret == nullptr || *ret != '\0') { + return S3Error::InvalidArgument; + } + if (last_modified > timegm(&date)) { + return S3Error::PreconditionFailed; + } + } + } + if ((it = headers.find("if-none-match")) != headers.end()) { + if (it->second == etag) { + return S3Error::NotModified; + } + } else { + if ((it = headers.find("if-modified-since")) != headers.end()) { + tm date{}; + auto ret = + strptime(it->second.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &date); + if (ret == nullptr || *ret != '\0') { + return S3Error::InvalidArgument; + } + fprintf(stderr, "FOUND PRECONDITION! `%s` `%s` `%s`\n", + it->second.c_str(), to_string(last_modified).c_str(), + to_string(timegm(&date)).c_str()); + if (last_modified < timegm(&date)) { + fprintf(stderr, "ERROR\n"); + return S3Error::NotModified; + } + } + } + return S3Error::None; + // todo range header +} + +#define LOG(msg) std::cerr << msg << std::endl; + +int S3Api::GetObjectHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::GetObject, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + S3ObjectStore::Object obj; + + auto error = objectStore.GetObject(req.bucket, req.object, obj); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + std::map headers = obj.GetAttributes(); + if (!S3Utils::HasHeader(headers, "etag") || + !S3Utils::HasHeader(headers, "last-modified")) { + return req.S3ErrorResponse(S3Error::InternalError); + } + + std::string etag = headers["etag"]; + time_t last_modified = std::stol(headers["last-modified"]); + + error = ValidatePreconditions(etag, last_modified, req.lowercase_headers); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + headers["last-modified"] = S3Utils::timestampToIso8016(last_modified); + + if (obj.GetSize() == 0) { + return req.S3Response(200, headers, nullptr, 0); + } else if (obj.GetSize() <= 32000000) { + char* ptr = new char[obj.GetSize()]; + auto i = obj.GetStream().readsome(ptr, obj.GetSize()); + if (i == 0) { + delete[] ptr; + return req.S3ErrorResponse(S3Error::InternalError); + } + auto ret = req.S3Response(200, headers, ptr, i); + delete[] ptr; + + return ret; + } else { + auto ret = req.StartChunkedResp(200, headers); + + char* ptr = new char[32000000]; + auto stream = &obj.GetStream(); + while (stream->good()) { + auto i = stream->readsome(ptr, 32000000); + + req.ChunkResp(ptr, i); + } + req.ChunkResp(nullptr, 0); + return ret; + } + + // todo: autodetect content type on put object + // headers.insert({"Content-Type", "text/plain"}); +} + +S3Error ParseCommonQueryParams( + const std::map& query_params, char& delimiter, + bool& encode_values, int& max_keys, std::string& prefix) { + auto it = query_params.end(); + + delimiter = 0; + if ((it = query_params.find("delimiter")) != query_params.end()) { + if (it->second.length() > 1) { + return S3Error::InvalidArgument; + } + if (it->second.empty()) { + delimiter = 0; + } else { + delimiter = it->second[0]; + } + } + + encode_values = false; + if ((it = query_params.find("encoding-type")) != query_params.end()) { + if (it->second != "url") { + return S3Error::InvalidArgument; + } + encode_values = true; + } + + // 1k by default, change depending on url params + max_keys = 1000; + if ((it = query_params.find("max-keys")) != query_params.end()) { + try { + max_keys = std::stoi(it->second); + } catch (std::exception& e) { + return S3Error::InvalidArgument; + } + } + + prefix = ""; + if ((it = query_params.find("prefix")) != query_params.end()) { + prefix = it->second; + } + + return S3Error::None; +} + +int S3Api::ListObjectVersionsHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::ListObjectVersions, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + char delimiter; + std::string prefix; + bool encode_values; + int max_keys; + + auto error = ParseCommonQueryParams(req.query, delimiter, encode_values, + max_keys, prefix); + + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + auto it = req.query.end(); + + std::string key_marker; + if ((it = req.query.find("key-marker")) != req.query.end()) { + key_marker = it->second; + } + std::string version_id_marker; + if ((it = req.query.find("version-id-marker")) != req.query.end()) { + version_id_marker = it->second; + } + + auto vinfo = objectStore.ListObjectVersions( + req.bucket, prefix, key_marker, version_id_marker, delimiter, max_keys); + + // todo: key_marker is lastt key returned, next_key_marker is the one after + // key_marker todo: next vid marker && vid marker + return ListObjectVersionsResponse(req, req.bucket, encode_values, delimiter, + max_keys, prefix, vinfo); +} + +#define VALIDATE_REQUEST(action) \ + auto err = \ + auth.ValidateRequest(req, objectStore, action, req.bucket, req.object); \ + if (err != S3Error::None) { \ + return req.S3ErrorResponse(err); \ + } + +#define PUT_LIMIT 5000000000 + +int S3Api::CopyObjectHandler(XrdS3Req& req) { + // return req.S3ErrorResponse(S3Error::NotImplemented, "", ""); + // // todo: combine code for all functions that call ValidateRequest then ret + VALIDATE_REQUEST(Action::CopyObject) + + auto source = + req.ctx->utils.UriDecode(req.lowercase_headers["x-amz-copy-source"]); + // todo: validate name + auto pos = source.find('/'); + if (pos == std::string::npos) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + auto bucket_src = source.substr(0, pos); + auto object_src = source.substr(pos + 1); + + // // todo: do this in AuthorizeRequest + err = auth.ValidateRequest(req, objectStore, Action::GetObject, bucket_src, + object_src); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + if (bucket_src == req.bucket && object_src == req.object) { + return req.S3ErrorResponse(S3Error::InvalidRequest); + } + + S3ObjectStore::Object obj; + err = objectStore.GetObject(bucket_src, object_src, obj); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + std::map headers = obj.GetAttributes(); + if (!S3Utils::HasHeader(headers, "etag") || + !S3Utils::HasHeader(headers, "last-modified")) { + return req.S3ErrorResponse(S3Error::InternalError); + } + + // std::string etag = headers["etag"]; + // time_t last_modified = std::stoul(headers["last-modified"]); + + // err = ValidatePreconditions(etag, last_modified, req.lowercase_headers); + // if (err != S3Error::None) { + // return req.S3ErrorResponse(err, "", ""); + // } + + if (obj.GetSize() > PUT_LIMIT) { + return req.S3ErrorResponse(S3Error::EntityTooLarge); + } + + Headers hd = {{"Content-Type", "application/xml"}}; + req.StartChunkedResp(200, hd); + + auto error = objectStore.CopyObject(req.bucket, req.object, obj, + req.lowercase_headers, headers); + if (error != S3Error::None) { + req.S3ErrorResponse(error, "", "", true); + } else { + CopyObjectResponse(req, headers["ETag"]); + } + + return req.ChunkResp(nullptr, 0); +} + +int S3Api::PutObjectHandler(XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::PutObject, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + auto chunked = false; + unsigned long length = 0; + auto it = req.lowercase_headers.find("content-length"); + if (it == req.lowercase_headers.end()) { + if (S3Utils::HeaderEq(req.lowercase_headers, "transfer-encoding", + "chunked")) { + chunked = true; + } else { + return req.S3ErrorResponse(S3Error::MissingContentLength); + } + } else { + try { + length = std::stoul(it->second); + } catch (std::exception&) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + + if (length > PUT_LIMIT) { + return req.S3ErrorResponse(S3Error::EntityTooLarge); + } + } + + S3ObjectStore::Object obj; + auto error = objectStore.GetObject(req.bucket, req.object, obj); + if (error == S3Error::None) { + std::map headers = obj.GetAttributes(); + if (!S3Utils::HasHeader(headers, "etag") || + !S3Utils::HasHeader(headers, "last-modified")) { + return req.S3ErrorResponse(S3Error::InternalError); + } + + std::string etag = headers["etag"]; + time_t last_modified = std::stol(headers["last-modified"]); + + error = ValidatePreconditions(etag, last_modified, req.lowercase_headers); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + } + + std::map headers; + error = objectStore.PutObject(req, length, chunked, headers); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + return req.S3Response(200, headers, ""); +} + +int S3Api::HeadObjectHandler(XrdS3Req& req) { + auto error = auth.ValidateRequest(req, objectStore, Action::HeadObject, + req.bucket, req.object); + if (error != S3Error::None) { + return req.S3Response(S3ErrorMap.find(error)->second.httpCode); + } + + S3ObjectStore::Object obj; + + error = objectStore.GetObject(req.bucket, req.object, obj); + if (error != S3Error::None) { + return req.S3Response(S3ErrorMap.find(error)->second.httpCode); + } + + std::map headers = obj.GetAttributes(); + + // todo: autodetect content type on put object + // headers.insert({"Content-Type", "text/plain"}); + + auto content_length = obj.GetSize(); + + return req.S3Response(200, headers, nullptr, (long long)content_length); +} + +struct DeleteObjectsQuery { + bool quiet; + std::vector objects; +}; + +bool ParseDeleteObjectsBody(char* body, int length, DeleteObjectsQuery& query) { + tinyxml2::XMLDocument doc; + + doc.Parse(body, length); + + if (doc.Error()) { + fprintf(stderr, "DOC ERROR: %d\n", doc.ErrorID()); + return false; + } + + tinyxml2::XMLElement* elem = doc.RootElement(); + + if (elem == nullptr || std::string(elem->Name()) != "Delete") { + fprintf(stderr, "no delete elem\n"); + return false; + } + + elem = elem->FirstChildElement(); + if (elem == nullptr) { + return false; + } + + tinyxml2::XMLElement* child = nullptr; + const char* str = nullptr; + while (elem) { + const std::string name(elem->Name()); + if (name == "Object") { + if ((child = elem->FirstChildElement("Key")) == nullptr) { + fprintf(stderr, "no key elem\n"); + return false; + } + + fprintf(stderr, "firstchild: %p\n", child->FirstChild()); + fprintf(stderr, "getext: %s\n", child->GetText()); + if ((str = child->GetText()) == nullptr) { + // fprintf(stderr, "cant get text of key %p\n", + // child->FirstChild()); + return false; + } + std::string version_id; + if ((child = elem->FirstChildElement("VersionId")) != nullptr) { + version_id = child->GetText(); + } + // todo: parse objects that end with / + query.objects.push_back({str, version_id}); + } else if (name == "Quiet") { + if (elem->QueryBoolText(&query.quiet)) { + fprintf(stderr, "quiet is not bool\n"); + return false; + } + } else { + fprintf(stderr, "unknown element name: %s\n", name.c_str()); + return false; + } + elem = elem->NextSiblingElement(); + } + return true; +} + +int S3Api::DeleteObjectsHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::DeleteObjects, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + auto it = req.lowercase_headers.find("content-length"); + if (it == req.lowercase_headers.end()) { + return req.S3ErrorResponse(S3Error::MissingContentLength); + } + int length; + try { + length = std::stoi(it->second); + } catch (std::exception&) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + if (length < 0) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + + char* ptr; + if (req.ReadBody(length, &ptr, true) != length) { + return req.S3ErrorResponse(S3Error::IncompleteBody); + } + + DeleteObjectsQuery query; + if (!ParseDeleteObjectsBody(ptr, length, query) || query.objects.empty()) { + return req.S3ErrorResponse(S3Error::MalformedXML); + } + + if (query.objects.size() > 1000) { + return req.S3ErrorResponse(S3Error::InvalidRequest); + } + + std::vector deleted; + std::vector error; + + std::tie(deleted, error) = + objectStore.DeleteObjects(req.bucket, query.objects); + + return DeleteObjectsResponse(req, query.quiet, deleted, error); +} + +int S3Api::ListObjectsV2Handler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::ListObjectsV2, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + char delimiter; + std::string prefix; + bool encode_values; + int max_keys; + auto error = ParseCommonQueryParams(req.query, delimiter, encode_values, + max_keys, prefix); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + auto it = req.query.end(); + std::string continuation_token; + if ((it = req.query.find("continuation-token")) != req.query.end()) { + continuation_token = it->second; + } + std::string start_after; + if ((it = req.query.find("start-after")) != req.query.end()) { + start_after = it->second; + } + + bool fetch_owner = false; + if ((it = req.query.find("fetch-owner")) != req.query.end()) { + if (it->second == "true") { + fetch_owner = true; + } else if (it->second == "false") { + fetch_owner = false; + } else { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + } + + auto objectinfo = + objectStore.ListObjectsV2(req.bucket, prefix, continuation_token, + delimiter, max_keys, fetch_owner, start_after); + + return ListObjectsV2Response(req, req.bucket, prefix, continuation_token, + delimiter, max_keys, fetch_owner, start_after, + encode_values, objectinfo); +} + +int S3Api::ListObjectsHandler(S3::XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::ListObjects, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + char delimiter; + std::string prefix; + bool encode_values; + int max_keys; + auto error = ParseCommonQueryParams(req.query, delimiter, encode_values, + max_keys, prefix); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + auto it = req.query.end(); + std::string marker; + if ((it = req.query.find("marker")) != req.query.end()) { + marker = it->second; + } + auto objectinfo = + objectStore.ListObjects(req.bucket, prefix, marker, delimiter, max_keys); + return ListObjectsResponse(req, req.bucket, prefix, delimiter, marker, + max_keys, encode_values, objectinfo); +} +int S3Api::CreateMultipartUploadHandler(XrdS3Req& req) { + auto err = auth.ValidateRequest( + req, objectStore, Action::CreateMultipartUpload, req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + std::string upload_id = + objectStore.CreateMultipartUpload(req, req.bucket, req.object); + + if (upload_id.empty()) { + return req.S3ErrorResponse(S3Error::InternalError); + } + return CreateMultipartUploadResponse(req, upload_id); +} + +int S3Api::ListMultipartUploadsHandler(XrdS3Req& req) { + auto err = auth.ValidateRequest( + req, objectStore, Action::ListMultipartUploads, req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + auto multipart_uploads = objectStore.ListMultipartUploads(req.bucket); + + return ListMultipartUploadResponse(req, multipart_uploads); +} + +int S3Api::AbortMultipartUploadHandler(XrdS3Req& req) { + auto err = auth.ValidateRequest( + req, objectStore, Action::AbortMultipartUpload, req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + // This function will never be called if the query params do not contain + // `uploadId`. + auto upload_id = req.query["uploadId"]; + + err = objectStore.AbortMultipartUpload(req.bucket, req.object, upload_id); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + return req.S3Response(204); +} + +int S3Api::ListPartsHandler(XrdS3Req& req) { + auto err = auth.ValidateRequest( + req, objectStore, Action::AbortMultipartUpload, req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + // This function will never be called if the query params do not contain + // `uploadId`. + auto upload_id = req.query["uploadId"]; + + auto [error, parts] = + objectStore.ListParts(req.bucket, req.object, upload_id); + if (error != S3Error::None) { + return req.S3ErrorResponse(error); + } + + return ListPartsResponse(req, upload_id, parts); +} +int S3Api::UploadPartHandler(XrdS3Req& req) { + auto err = auth.ValidateRequest(req, objectStore, Action::UploadPart, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + // This function will never be called if the query params do not contain + // `uploadId` and `partNumber`. + auto upload_id = req.query["uploadId"]; + size_t part_number; + try { + part_number = std::stoul(req.query["partNumber"]); + } catch (std::exception& e) { + return req.S3ErrorResponse(S3Error::InvalidRequest); + } + + if (part_number < 1 || part_number > 10000) { + return req.S3ErrorResponse(S3Error::InvalidRequest); + } + + bool chunked = false; + size_t length = 0; + auto it = req.lowercase_headers.find("content-length"); + if (it == req.lowercase_headers.end()) { + if (S3Utils::HeaderEq(req.lowercase_headers, "transfer-encoding", + "chunked")) { + chunked = true; + } else { + return req.S3ErrorResponse(S3Error::MissingContentLength); + } + } else { + try { + length = std::stoul(it->second); + } catch (std::exception&) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + // todo: check minimum size if not last part + if (length > PUT_LIMIT) { + return req.S3ErrorResponse(S3Error::EntityTooLarge); + } + } + + std::map headers; + err = objectStore.UploadPart(req, upload_id, part_number, length, chunked, + headers); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + return req.S3Response(200, headers, ""); +} + +// todo: handle checksum + +bool ParseCompleteMultipartUploadBody( + char* body, int length, std::vector& query) { + tinyxml2::XMLDocument doc; + + doc.Parse(body, length); + + if (doc.Error()) { + fprintf(stderr, "DOC ERROR: %d\n", doc.ErrorID()); + return false; + } + + tinyxml2::XMLElement* elem = doc.RootElement(); + + if (elem == nullptr || + std::string(elem->Name()) != "CompleteMultipartUpload") { + fprintf(stderr, "no delete elem\n"); + return false; + } + + elem = elem->FirstChildElement(); + if (elem == nullptr) { + return false; + } + + tinyxml2::XMLElement* child = nullptr; + while (elem) { + const std::string name(elem->Name()); + if (name == "Part") { + if ((child = elem->FirstChildElement("ETag")) == nullptr) { + fprintf(stderr, "no etag\n"); + return false; + } + + if (child->GetText() == nullptr) { + fprintf(stderr, "cant get text of etag %p\n", child->FirstChild()); + return false; + } + + std::string etag(child->GetText()); + + if ((child = elem->FirstChildElement("PartNumber")) == nullptr) { + fprintf(stderr, "no part number\n"); + return false; + } + if (child->GetText() == nullptr) { + fprintf(stderr, "cant get text of part_number %p\n", + child->FirstChild()); + return false; + } + + query.push_back({etag, {}, std::stoul(child->GetText()), {}}); + } else { + fprintf(stderr, "unknown element name: %s\n", name.c_str()); + return false; + } + elem = elem->NextSiblingElement(); + } + return true; +} + +int S3Api::CompleteMultipartUploadHandler(XrdS3Req& req) { + auto err = + auth.ValidateRequest(req, objectStore, Action::CompleteMultipartUpload, + req.bucket, req.object); + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + + auto it = req.lowercase_headers.find("content-length"); + if (it == req.lowercase_headers.end()) { + return req.S3ErrorResponse(S3Error::MissingContentLength); + } + int length; + try { + length = std::stoi(it->second); + } catch (std::exception&) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + if (length < 0) { + return req.S3ErrorResponse(S3Error::InvalidArgument); + } + + char* ptr; + if (req.ReadBody(length, &ptr, true) != length) { + return req.S3ErrorResponse(S3Error::IncompleteBody); + } + + std::vector query; + if (!ParseCompleteMultipartUploadBody(ptr, length, query) || query.empty()) { + return req.S3ErrorResponse(S3Error::MalformedXML); + } + + if (query.size() > 10000) { + return req.S3ErrorResponse(S3Error::InvalidRequest); + } + + auto upload_id = req.query["uploadId"]; + + err = objectStore.CompleteMultipartUpload(req, req.bucket, req.object, + upload_id, std::move(query)); + + if (err != S3Error::None) { + return req.S3ErrorResponse(err); + } + return CompleteMultipartUploadResponse(req); +} + +} // namespace S3 diff --git a/src/XrdS3/XrdS3Api.hh b/src/XrdS3/XrdS3Api.hh new file mode 100644 index 00000000000..b57870879fc --- /dev/null +++ b/src/XrdS3/XrdS3Api.hh @@ -0,0 +1,296 @@ +// +// Created by segransm on 11/16/23. +// + +#ifndef XROOTD_XRDS3API_HH +#define XROOTD_XRDS3API_HH + +#include + +#include "XrdS3Auth.hh" +#include "XrdS3ObjectStore.hh" +#include "XrdS3Req.hh" + +namespace S3 { + +class S3Api { + public: + S3Api() = default; + S3Api(const std::string& data_path, const std::string &auth_path) + : objectStore(data_path), auth(auth_path) {} + + ~S3Api() = default; + + // Bucket Operations + int CreateBucketHandler(XrdS3Req &req); + int DeleteBucketHandler(XrdS3Req &req); + int HeadBucketHandler(XrdS3Req &req); + int ListBucketsHandler(XrdS3Req &req); + + // Object Operations + int ListObjectsHandler(XrdS3Req &req); + int ListObjectsV2Handler(XrdS3Req &req); + int GetObjectHandler(XrdS3Req &req); + int HeadObjectHandler(XrdS3Req &req); + int PutObjectHandler(XrdS3Req &req); + int DeleteObjectHandler(XrdS3Req &req); + int DeleteObjectsHandler(XrdS3Req &req); + int ListObjectVersionsHandler(XrdS3Req &req); + int CopyObjectHandler(XrdS3Req &req); + int CreateMultipartUploadHandler(XrdS3Req &req); + int ListMultipartUploadsHandler(XrdS3Req &req); + int AbortMultipartUploadHandler(XrdS3Req &req); + int ListPartsHandler(XrdS3Req &req); + int UploadPartHandler(XrdS3Req &req); + int CompleteMultipartUploadHandler(XrdS3Req &req); + + // Not implemented + + int UploadPartCopyHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + + + + int DeleteBucketAnalyticsConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketCorsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketEncryptionHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketIntelligentTieringConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketInventoryConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketLifecycleHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketMetricsConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketOwnershipControlsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketPolicyHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketReplicationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketTaggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteBucketWebsiteHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeleteObjectTaggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int DeletePublicAccessBlockHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketAccelerateConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketAclHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketAnalyticsConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketCorsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketEncryptionHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketIntelligentTieringConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketInventoryConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketLifecycleHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketLifecycleConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketLocationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketLoggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketMetricsConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketNotificationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketNotificationConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketOwnershipControlsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketPolicyHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketPolicyStatusHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketReplicationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketRequestPaymentHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketTaggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketVersioningHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetBucketWebsiteHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetObjectAclHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetObjectAttributesHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetObjectLegalHoldHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetObjectLockConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetObjectRetentionHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetObjectTaggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetObjectTorrentHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int GetPublicAccessBlockHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int ListBucketAnalyticsConfigurationsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int ListBucketIntelligentTieringConfigurationsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int ListBucketInventoryConfigurationsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int ListBucketMetricsConfigurationsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + + int PutBucketAccelerateConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketAclHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketAnalyticsConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketCorsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketEncryptionHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketIntelligentTieringConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketInventoryConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketLifecycleHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketLifecycleConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketLoggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketMetricsConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketNotificationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketNotificationConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketOwnershipControlsHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketPolicyHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketReplicationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketRequestPaymentHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketTaggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketVersioningHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutBucketWebsiteHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutObjectAclHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutObjectLegalHoldHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutObjectLockConfigurationHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutObjectRetentionHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutObjectTaggingHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int PutPublicAccessBlockHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int RestoreObjectHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + int SelectObjectContentHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + + int WriteGetObjectResponseHandler(XrdS3Req &req) { + return req.S3ErrorResponse(S3Error::NotImplemented); + } + + private: + S3ObjectStore objectStore; + S3Auth auth; +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3API_HH diff --git a/src/XrdS3/XrdS3Auth.cc b/src/XrdS3/XrdS3Auth.cc new file mode 100644 index 00000000000..7df33afcf31 --- /dev/null +++ b/src/XrdS3/XrdS3Auth.cc @@ -0,0 +1,358 @@ +// +// Created by segransm on 11/9/23. +// + +#include "XrdS3Auth.hh" + +#include + +#include + +#include "XrdOuc/XrdOucStream.hh" +#include "XrdOuc/XrdOucTUtils.hh" +#include "XrdOuc/XrdOucUtils.hh" +#include "XrdS3ObjectStore.hh" + +namespace S3 { + +AuthType S3Auth::GetRequestAuthType(const XrdS3Req &req) { + if (req.method == Put) { + if (S3Utils::HeaderEq(req.lowercase_headers, X_AMZ_CONTENT_SHA256, + STREAMING_SHA256_PAYLOAD)) { + return AuthType::StreamingSigned; + } + if (S3Utils::HeaderEq(req.lowercase_headers, X_AMZ_CONTENT_SHA256, + STREAMING_SHA256_TRAILER)) { + return AuthType::StreamingSignedTrailer; + } + if (S3Utils::HeaderEq(req.lowercase_headers, X_AMZ_CONTENT_SHA256, + STREAMING_UNSIGNED_TRAILER)) { + return AuthType::StreamingUnsignedTrailer; + } + } + + if (S3Utils::HeaderStartsWith(req.lowercase_headers, "authorization", + AWS4_ALGORITHM)) { + return AuthType::Signed; + } + if (S3Utils::HeaderEq(req.query, "X-Amz-Algorithm", AWS4_ALGORITHM)) { + return AuthType::Presigned; + } + // todo: parse other auth types + return AuthType::Unknown; +} +S3Error S3Auth::AuthenticateRequest(XrdS3Req &req) { + switch (GetRequestAuthType(req)) { + case AuthType::Signed: { + // todo: not hardcode + return VerifySigV4(req, "us-east-1", "s3"); + } + // todo: invalidate all other requests for now + default: { + return S3Error::NotImplemented; + } + } +} + +// todo: handle errors +S3Auth::SigV4 S3Auth::ParseSigV4(const XrdS3Req &req, const std::string ®ion, + const std::string &service) { + SigV4 sig; + + auto it = req.lowercase_headers.find("authorization"); + if (it == req.lowercase_headers.end()) { + return {}; + } + auto authorization = it->second; + + size_t loc = authorization.find(' '); + if (loc == std::string::npos) { + return {}; + } + + if (authorization.substr(0, loc) != AWS4_ALGORITHM) { + return {}; + } + + authorization.erase(0, loc); + + std::vector components; + XrdOucTUtils::splitString(components, authorization, ","); + + if (components.size() != 3) { + return {}; + } + + for (const auto &component : components) { + loc = component.find('='); + if (loc == std::string::npos) { + return {}; + } + std::string key = component.substr(0, loc); + XrdOucUtils::trim(key); + std::string value = component.substr(loc + 1); + + if (key == "Credential") { + std::vector credentials; + credentials.reserve(5); + XrdOucTUtils::splitString(credentials, value, "/"); + + if (credentials.size() < 5) { + return {}; + } + if (credentials[credentials.size() - 1] != "aws4_request") { + return {}; + } + if (credentials[credentials.size() - 2] != service) { + return {}; + } + if (credentials[credentials.size() - 3] != region) { + return {}; + } + // todo: validate date + + sig.credentials.request = "aws4_request"; + sig.credentials.service = service; + sig.credentials.region = region; + sig.credentials.date = credentials[credentials.size() - 4]; + // access key can contain '/', reconstruct it back after split + for (size_t i = 0; i < credentials.size() - 4; ++i) { + sig.credentials.access_key += credentials[i]; + } + } else if (key == "SignedHeaders") { + std::vector headers; + headers.reserve(3); + XrdOucTUtils::splitString(headers, value, ";"); + sig.signed_headers.insert(headers.begin(), headers.end()); + } else if (key == "Signature") { + sig.signature = value; + } else { + return {}; + } + } + + return sig; +} +std::string S3Auth::GetCanonicalRequestHash( + Context *ctx, const std::string &method, const std::string &canonical_uri, + const std::string &canonical_query_string, + const std::string &canonical_headers, const std::string &signed_headers, + const std::string &hashed_payload) { + const std::string canonical_request = + S3Utils::stringJoin('\n', method, canonical_uri, canonical_query_string, + canonical_headers, signed_headers, hashed_payload); + + fprintf(stderr, "Canonical request:\n%s\n", canonical_request.c_str()); + const auto hashed_request = ctx->crypt.mSha256.calculate(canonical_request); + + return S3Utils::HexEncode(hashed_request); +} + +std::string S3Auth::GetStringToSign(const std::string &algorithm, + const struct tm &date, + const std::string &canonical_request_hash, + const SigV4::Scope &scope) { + const std::string scope_str = + S3Utils::stringJoin('/', scope.date, scope.region, scope.service, + std::string("aws4_request")); + + std::string date_iso8601 = S3Utils::timestampToIso8016(&date); + + std::string string_to_sign = + S3Utils::stringJoin('\n', algorithm, std::string(date_iso8601), scope_str, + canonical_request_hash); + + return string_to_sign; +} + +sha256_digest S3Auth::GetSigningKey(Context *ctx, const std::string &secret_key, + const SigV4::Scope &scope) { + std::string key = "AWS4" + secret_key; + auto dateKey = ctx->crypt.mHmac.calculate(scope.date, key); + + auto dateRegionKey = ctx->crypt.mHmac.calculate(scope.region, dateKey); + auto dateRegionServiceKey = + ctx->crypt.mHmac.calculate(scope.service, dateRegionKey); + return ctx->crypt.mHmac.calculate(std::string("aws4_request"), + dateRegionServiceKey); +} + +std::string S3Auth::GetSignature(Context *ctx, const std::string &secret_key, + const SigV4::Scope &scope, + const std::string &string_to_sign) { + const auto signing_key = GetSigningKey(ctx, secret_key, scope); + + const auto digest = ctx->crypt.mHmac.calculate(string_to_sign, signing_key); + + return S3Utils::HexEncode(digest); +} +S3Error S3Auth::VerifySigV4(XrdS3Req &req, const std::string ®ion, + const std::string &service) { + fprintf(stderr, "Verifying sigv4...\n"); + auto sig = ParseSigV4(req, region, service); + + req.id = sig.credentials.access_key; + if (req.id.empty()) { + fprintf(stderr, "REQ ID IS EMPTY!\n"); + return S3Error::InvalidAccessKeyId; + } + Context *ctx = req.ctx; + + // todo: global store? + if (keyMap.find(sig.credentials.access_key) == keyMap.end()) { + fprintf(stderr, "KEY NOT IN STORE!\n"); + return S3Error::InvalidAccessKeyId; + } + auto key = keyMap.find(sig.credentials.access_key)->second; + + // todo: not segfault + auto hashed_payload = + req.lowercase_headers.find(X_AMZ_CONTENT_SHA256)->second; + + auto canonical_uri = ctx->utils.ObjectUriEncode(req.uri_path); + auto canonical_query_string = GetCanonicalQueryString(ctx, req.query); + + std::string canonical_headers, signed_headers; + std::tie(canonical_headers, signed_headers) = + GetCanonicalHeaders(req.lowercase_headers, sig.signed_headers); + + // todo: compare hash on stream close if type == xs and body is not empty + auto canonical_request_hash = GetCanonicalRequestHash( + ctx, HttpMethodMap.find(req.method)->second, canonical_uri, + canonical_query_string, canonical_headers, signed_headers, + hashed_payload); + // todo: limit to 7 days + + // todo: sanitize if we log + + // todo: not hardcode algorithm + + auto string_to_sign = GetStringToSign( + AWS4_ALGORITHM, req.date, canonical_request_hash, sig.credentials); + fprintf(stderr, "String to sign:\n%s\n", string_to_sign.c_str()); + + const auto signature = + GetSignature(ctx, key, sig.credentials, string_to_sign); + ctx->log->Emsg("VerifySignature", "calculated signature:", signature.c_str()); + ctx->log->Emsg("VerifySignature", + " received signature:", sig.signature.c_str()); + + // todo: secure compare? + if (signature == sig.signature) { + return S3Error::None; + } + return S3Error::SignatureDoesNotMatch; +} + +std::string S3Auth::GetCanonicalQueryString( + Context *ctx, const std::map &query_params) { + std::vector> query_params_map; + std::string canonical_query_string; + query_params_map.reserve(query_params.size()); + + for (const auto &p : query_params) { + query_params_map.emplace_back(ctx->utils.UriEncode(p.first), + ctx->utils.UriEncode(p.second)); + } + std::sort(query_params_map.begin(), query_params_map.end()); + + for (const auto ¶m : query_params_map) { + canonical_query_string.append(param.first + "=" + param.second + '&'); + } + if (!canonical_query_string.empty()) { + canonical_query_string.pop_back(); + } + + return canonical_query_string; +} + +std::tuple S3Auth::GetCanonicalHeaders( + const Headers &headers, const std::set &signed_headers) { + std::string canonical_headers, canonical_signed_headers; + std::vector> canonical_headers_map; + + for (const auto &hd : headers) { + if (signed_headers.count(hd.first)) { + std::string value(hd.second); + + // todo: separate the values for a multi-value header using commas. + S3Utils::TrimAll(value); + canonical_headers_map.emplace_back(hd.first, value); + } else if (hd.first.substr(0, 6) == "x-amz-" || + hd.first == "content-type" || hd.first == "host") { + // signed headers must include all x-amz-* headers, host and content-type + // (if present) headers. + return {}; + } + } + std::sort(canonical_headers_map.begin(), canonical_headers_map.end()); + + for (const auto &hd : canonical_headers_map) { + canonical_headers.append(hd.first + ':' + hd.second + '\n'); + canonical_signed_headers.append(hd.first + ';'); + } + if (!signed_headers.empty()) { + canonical_signed_headers.pop_back(); + } + + return std::make_tuple(canonical_headers, canonical_signed_headers); +} + +// todo: do we need object here? +S3Error S3Auth::ValidateRequest(XrdS3Req &req, const S3ObjectStore &objectStore, + const Action &action, const std::string &bucket, + const std::string &object) { + auto err = AuthenticateRequest(req); + if (err != S3Error::None) { + return err; + } + + return AuthorizeRequest(req, objectStore, action, bucket, object); +} + +S3Error S3Auth::AuthorizeRequest(const XrdS3Req &req, + const S3ObjectStore &objectStore, + const Action &action, + const std::string &bucket, + const std::string &object) { + // todo: + if (action == Action::ListBuckets || action == Action::CreateBucket) { + return S3Error::None; + } + // todo: head bucket might not need to be owner + + auto owner = objectStore.GetBucketOwner(bucket); + if (owner.empty()) { + return S3Error::NoSuchBucket; + } + if (owner == req.id) { + return S3Error::None; + } else { + return S3Error::AccessDenied; + } + // todo: iam instead of manually +} +S3Auth::S3Auth(const std::string &path) { + XrdOucStream stream; + + auto fd = + open((path + "/users").c_str(), O_RDONLY | O_CREAT, S_IREAD | S_IWRITE); + + if (fd < 0) { + throw std::runtime_error("Unable to open auth file"); + } + + stream.Attach(fd); + const char *line; + while ((line = stream.GetLine())) { + const std::string l(line); + + auto pos = l.find(':'); + const std::string id = l.substr(0, pos); + const std::string key = l.substr(pos + 1); + + keyMap.insert({id, key}); + } +} + +} // namespace S3 diff --git a/src/XrdS3/XrdS3Auth.hh b/src/XrdS3/XrdS3Auth.hh new file mode 100644 index 00000000000..a32849c7f98 --- /dev/null +++ b/src/XrdS3/XrdS3Auth.hh @@ -0,0 +1,107 @@ +// +// Created by segransm on 11/9/23. +// + +#ifndef XROOTD_XRDS3AUTH_HH +#define XROOTD_XRDS3AUTH_HH + +#include + +#include "XrdS3Action.hh" +#include "XrdS3Crypt.hh" +#include "XrdS3ObjectStore.hh" +#include "XrdS3Req.hh" + +namespace S3 { + +enum class AuthType { + Unknown, + Anonymous, + Presigned, + PostPolicy, + Signed, + StreamingSigned, + StreamingSignedTrailer, + StreamingUnsignedTrailer, +}; + +const std::string EMPTY_SHA256 = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + +const std::string STREAMING_SHA256_PAYLOAD = + "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; +const std::string STREAMING_SHA256_TRAILER = + "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER"; +const std::string SHA256_PAYLOAD = "AWS4-HMAC-SHA256-PAYLOAD"; +const std::string SHA256_TRAILER = "AWS4-HMAC-SHA256-TRAILER"; +const std::string UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; +const std::string STREAMING_UNSIGNED_TRAILER = + "STREAMING-UNSIGNED-PAYLOAD-TRAILER"; + +const std::string AWS4_ALGORITHM = "AWS4-HMAC-SHA256"; + +const std::string X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256"; + +class S3Auth { + public: + S3Auth() = default; + + explicit S3Auth(const std::string &path); + + ~S3Auth() = default; + + static AuthType GetRequestAuthType(const XrdS3Req &req); + + S3Error AuthenticateRequest(XrdS3Req &req); + + S3Error AuthorizeRequest(const XrdS3Req &req, const S3ObjectStore &objectStore, + const Action &action, const std::string &bucket, + const std::string &object); + + S3Error ValidateRequest(XrdS3Req &req, const S3ObjectStore &objectStore, + const Action &action, const std::string &bucket, + const std::string &object); + S3Error VerifySigV4(XrdS3Req &req, const std::string ®ion, + const std::string &service); + static std::string GetCanonicalQueryString( + Context *ctx, const std::map &query_params); + static std::tuple GetCanonicalHeaders( + const Headers &headers, const std::set &signed_headers); + + struct SigV4 { + std::string signature; + std::set signed_headers; + struct Scope { + std::string access_key; + std::string date; + std::string region; + std::string service; + std::string request; + } credentials; + }; + static SigV4 ParseSigV4(const XrdS3Req &req, const std::string ®ion, + const std::string &service); + static std::string GetCanonicalRequestHash( + Context *ctx, const std::string &method, const std::string &canonical_uri, + const std::string &canonical_query_string, + const std::string &canonical_headers, const std::string &signed_headers, + const std::string &hashed_payload); + static std::string GetStringToSign(const std::string &algorithm, + const struct tm &date, + const std::string &canonical_request_hash, + const SigV4::Scope &scope); + static std::string GetSignature(Context *ctx, const std::string &secret_key, + const SigV4::Scope &scope, + const std::string &string_to_sign); + static sha256_digest GetSigningKey(Context *ctx, + const std::string &secret_key, + const SigV4::Scope &scope); + + private: + // Map of user id to key + std::map keyMap; +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3AUTH_HH diff --git a/src/XrdS3/XrdS3Crypt.cc b/src/XrdS3/XrdS3Crypt.cc new file mode 100644 index 00000000000..6f25ba42dd9 --- /dev/null +++ b/src/XrdS3/XrdS3Crypt.cc @@ -0,0 +1,79 @@ +// +// Created by segransm on 11/3/23. +// + +#include "XrdS3Crypt.hh" + +#include +#include +#include + +#include +#include + +namespace S3 { + +S3Crypt::HMAC_SHA256::HMAC_SHA256() { + fprintf(stderr, "Constructing HMAC\n"); + static const std::string digestName = "SHA256"; + OSSL_PARAM params[2]; + + mac = EVP_MAC_fetch(nullptr, "HMAC", nullptr); + if (mac == nullptr) { + throw std::bad_alloc(); + } + ctx = EVP_MAC_CTX_new(mac); + if (ctx == nullptr) { + EVP_MAC_free(mac); + throw std::bad_alloc(); + } + + params[0] = OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, (char *)digestName.c_str(), digestName.size()); + params[1] = OSSL_PARAM_construct_end(); + + if (!EVP_MAC_CTX_set_params(ctx, params)) { + EVP_MAC_CTX_free(ctx); + EVP_MAC_free(mac); + throw std::runtime_error("Unable to set ctx params"); + } +} +S3Crypt::HMAC_SHA256::~HMAC_SHA256() { + fprintf(stderr, "Destroying HMAC\n"); + EVP_MAC_CTX_free(ctx); + EVP_MAC_free(mac); +} + +S3Crypt::SHA256::SHA256() { + md = EVP_MD_fetch(nullptr, "SHA256", nullptr); + if (md == nullptr) { + throw std::bad_alloc(); + } + + ctx = EVP_MD_CTX_new(); + if (ctx == nullptr) { + EVP_MD_free(md); + throw std::bad_alloc(); + } + + if (!EVP_DigestInit(ctx, md)) { + EVP_MD_CTX_free(ctx); + EVP_MD_free(md); + throw std::runtime_error("Unable to init digest"); + } +} + +S3Crypt::SHA256::~SHA256() { + EVP_MD_CTX_free(ctx); + EVP_MD_free(md); +} + +S3Crypt::Base64::Base64() { + ctx = EVP_ENCODE_CTX_new(); + if (ctx == nullptr) { + throw std::bad_alloc(); + } +} + +S3Crypt::Base64::~Base64() { EVP_ENCODE_CTX_free(ctx); } +} // namespace S3 diff --git a/src/XrdS3/XrdS3Crypt.hh b/src/XrdS3/XrdS3Crypt.hh new file mode 100644 index 00000000000..ff7dad28480 --- /dev/null +++ b/src/XrdS3/XrdS3Crypt.hh @@ -0,0 +1,169 @@ +// +// Created by segransm on 11/3/23. +// + +#ifndef XROOTD_XRDS3CRYPT_HH +#define XROOTD_XRDS3CRYPT_HH + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace S3 { + +using sha256_digest = std::array; + +class S3Crypt { + public: + S3Crypt() = default; + ~S3Crypt() = default; + + class HMAC_SHA256 { + public: + HMAC_SHA256(); + ~HMAC_SHA256(); + + template + sha256_digest calculate(const T &src, const U &key) { + if (!EVP_MAC_init(ctx, (const unsigned char *)key.data(), key.size(), + nullptr)) { + return {}; + } + + if (!EVP_MAC_update(ctx, (const unsigned char *)src.data(), src.size())) { + return {}; + } + + size_t outl; + if (!EVP_MAC_final(ctx, digest.data(), &outl, digest.size())) { + return {}; + } + + return digest; + } + + private: + EVP_MAC *mac; + EVP_MAC_CTX *ctx; + sha256_digest digest{}; + }; + + class SHA256 { + public: + SHA256(); + ~SHA256(); + + template + sha256_digest calculate(const T &src) { + if (!EVP_DigestInit_ex2(ctx, nullptr, nullptr)) { + return {}; + } + + if (!EVP_DigestUpdate(ctx, src.data(), src.size())) { + return {}; + } + + unsigned int outl; + if (!EVP_DigestFinal_ex(ctx, digest.data(), &outl)) { + return {}; + } + + return digest; + } + + void Init() { EVP_DigestInit_ex2(ctx, nullptr, nullptr); } + + template + void Update(const T &src) { + EVP_DigestUpdate(ctx, src.data(), src.size()); + } + + void Update(const char *src, size_t size) { + EVP_DigestUpdate(ctx, src, size); + } + + sha256_digest Finish() { + unsigned int outl; + if (!EVP_DigestFinal_ex(ctx, digest.data(), &outl)) { + return {}; + } + + return digest; + } + + private: + EVP_MD *md; + EVP_MD_CTX *ctx; + sha256_digest digest{}; + }; + + class Base64 { + public: + Base64(); + ~Base64(); + + template + std::string encode(const T &src) { + EVP_EncodeInit(ctx); + std::vector res; + res.resize(4 * (src.size() + 2) / 3 + 1); + + int outl; + if (!EVP_EncodeUpdate(ctx, res.data(), &outl, + reinterpret_cast(src.data()), + src.size())) { + return {}; + } + + int outlf; + EVP_EncodeFinal(ctx, res.data() + outl, &outlf); + + assert(static_cast(outl + outlf) <= res.size()); + return {reinterpret_cast(res.data()), + static_cast(outl + outlf - 1)}; + } + + template + std::vector decode(const T &src) { + EVP_DecodeInit(ctx); + + std::vector res; + res.resize(3 * src.size() / 4 + 1); + + int outl; + if (EVP_DecodeUpdate(ctx, res.data(), &outl, + reinterpret_cast(src.data()), + src.size()) < 0) { + return {}; + } + + int outlf; + if (EVP_DecodeFinal(ctx, res.data() + outl, &outlf) == -1) { + return {}; + } + + assert(static_cast(outl + outlf) <= res.size()); + + res.resize(outl + outlf); + return res; + } + + private: + EVP_ENCODE_CTX *ctx; + }; + + HMAC_SHA256 mHmac; + SHA256 mSha256; + Base64 mBase64; +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3CRYPT_HH diff --git a/src/XrdS3/XrdS3ErrorResponse.hh b/src/XrdS3/XrdS3ErrorResponse.hh new file mode 100644 index 00000000000..c3885aebe1e --- /dev/null +++ b/src/XrdS3/XrdS3ErrorResponse.hh @@ -0,0 +1,208 @@ +// +// Created by segransm on 11/14/23. +// + +#ifndef XROOTD_XRDS3ERRORRESPONSE_HH +#define XROOTD_XRDS3ERRORRESPONSE_HH + +#include +#include +#include + +namespace S3 { + +struct S3ErrorCode { + std::string code; + std::string description; + int httpCode; +}; + +enum class S3Error { + None, + AccessControlListNotSupported, + AccessDenied, + AccessPointAlreadyOwnedByYou, + AccountProblem, + AllAccessDisabled, + AmbiguousGrantByEmailAddress, + AuthorizationHeaderMalformed, + BadDigest, + BucketAlreadyExists, + BucketAlreadyOwnedByYou, + BucketNotEmpty, + ClientTokenConflict, + CredentialsNotSupported, + CrossLocationLoggingProhibited, + EntityTooSmall, + EntityTooLarge, + ExpiredToken, + IllegalLocationConstraintException, + IllegalVersioningConfigurationException, + IncompleteBody, + IncorrectNumberOfFilesInPostRequest, + InlineDataTooLarge, + InternalError, + InvalidAccessKeyId, + InvalidAccessPoint, + InvalidAccessPointAliasError, + InvalidAddressingHeader, + InvalidArgument, + InvalidBucketAclWithObjectOwnership, + InvalidBucketName, + InvalidBucketState, + InvalidDigest, + InvalidEncryptionAlgorithmError, + InvalidLocationConstraint, + InvalidObjectState, + InvalidPart, + InvalidPartOrder, + InvalidPayer, + InvalidPolicyDocument, + InvalidRange, + InvalidRequest, + InvalidSecurity, + InvalidSOAPRequest, + InvalidStorageClass, + InvalidTargetBucketForLogging, + InvalidToken, + InvalidURI, + KeyTooLongError, + MalformedACLError, + MalformedPOSTRequest, + MalformedXML, + MaxMessageLengthExceeded, + MaxPostPreDataLengthExceededError, + MetadataTooLarge, + MethodNotAllowed, + MissingAttachment, + MissingContentLength, + MissingRequestBodyError, + MissingSecurityElement, + MissingSecurityHeader, + NoLoggingStatusForKey, + NoSuchBucket, + NoSuchBucketPolicy, + NoSuchCORSConfiguration, + NoSuchKey, + NoSuchLifecycleConfiguration, + NoSuchMultiRegionAccessPoint, + NoSuchWebsiteConfiguration, + NoSuchTagSet, + NoSuchUpload, + NoSuchVersion, + NotImplemented, + NotModified, + NotSignedUp, + OwnershipControlsNotFoundError, + OperationAborted, + PermanentRedirect, + PreconditionFailed, + Redirect, + RequestHeaderSectionTooLarge, + RequestIsNotMultiPartContent, + RequestTimeout, + RequestTimeTooSkewed, + RequestTorrentOfBucketError, + RestoreAlreadyInProgress, + ServerSideEncryptionConfigurationNotFoundError, + ServiceUnavailable, + SignatureDoesNotMatch, + SlowDown, + TemporaryRedirect, + TokenRefreshRequired, + TooManyAccessPoints, + TooManyBuckets, + TooManyMultiRegionAccessPointregionsError, + TooManyMultiRegionAccessPoints, + UnexpectedContent, + UnresolvableGrantByEmailAddress, + UserKeyMustBeSpecified, + NoSuchAccessPoint, + InvalidTag, + MalformedPolicy, + // S3 Error + + XAmzContentSHA256Mismatch, + // XrdErrors + InvalidObjectName, + ObjectExistAsDir, + ObjectExistInObjectPath, +}; + +// todo: description +const std::map S3ErrorMap = { + {S3Error::NotImplemented, + {.code = "NotImplemented", .description = "", .httpCode = 501}}, + {S3Error::MissingContentLength, + {.code = "MissingContentLength", .description = "", .httpCode = 411}}, + {S3Error::IncompleteBody, + {.code = "IncompleteBody", .description = "", .httpCode = 400}}, + {S3Error::InternalError, + {.code = "InternalError", .description = "", .httpCode = 500}}, + {S3Error::BucketNotEmpty, + {.code = "BucketNotEmpty", .description = "", .httpCode = 409}}, + {S3Error::BadDigest, + {.code = "BadDigest", .description = "", .httpCode = 400}}, + {S3Error::AccessDenied, + {.code = "AccessDenied", .description = "", .httpCode = 403}}, + {S3Error::InvalidDigest, + {.code = "InvalidDigest", .description = "", .httpCode = 400}}, + {S3Error::InvalidRequest, + {.code = "InvalidRequest", .description = "", .httpCode = 400}}, + {S3Error::BucketAlreadyOwnedByYou, + {.code = "BucketAlreadyOwnedByYou", .description = "", .httpCode = 409}}, + {S3Error::InvalidURI, + {.code = "InvalidURI", .description = "", .httpCode = 400}}, + {S3Error::InvalidObjectName, + {.code = "InvalidObjectName", + .description = "Object name is not valid", + .httpCode = 400}}, + {S3Error::ObjectExistAsDir, + {.code = "ObjectExistAsDir", + .description = "A directory already exist with this path", + .httpCode = 400}}, + {S3Error::ObjectExistInObjectPath, + {.code = "ObjectExistInObjectPath", + .description = "An object already exist in the object path", + .httpCode = 400}}, + {S3Error::NoSuchKey, + {.code = "NoSuchKey", + .description = "Object does not exist", + .httpCode = 404}}, + {S3Error::InvalidBucketName, + {.code = "InvalidBucketName", + .description = "Bucket name is not valid", + .httpCode = 400}}, + {S3Error::InvalidArgument, + {.code = "InvalidArgument", .description = "", .httpCode = 400}}, + {S3Error::NoSuchBucket, + {.code = "NoSuchBucket", .description = "", .httpCode = 404}}, + {S3Error::OperationAborted, + {.code = "OperationAborted", .description = "", .httpCode = 404}}, + {S3Error::BucketAlreadyExists, + {.code = "BucketAlreadyExists", .description = "", .httpCode = 409}}, + {S3Error::MalformedXML, + {.code = "MalformedXML", .description = "", .httpCode = 400}}, + {S3Error::PreconditionFailed, + {.code = "PreconditionFailed", .description = "", .httpCode = 412}}, + {S3Error::NotModified, + {.code = "NotModified", .description = "", .httpCode = 304}}, + {S3Error::SignatureDoesNotMatch, + {.code = "SignatureDoesNotMatch", .description = "", .httpCode = 403}}, + {S3Error::InvalidAccessKeyId, + {.code = "InvalidAccessKeyId", .description = "", .httpCode = 403}}, + {S3Error::NoSuchAccessPoint, + {.code = "NoSuchAccessPoint", .description = "", .httpCode = 404}}, + {S3Error::XAmzContentSHA256Mismatch, + {.code = "XAmzContentSHA256Mismatch", .description = "", .httpCode = 400}}, + {S3Error::NoSuchUpload, + {.code = "NoSuchUpload", .description = "", .httpCode = 404}}, + {S3Error::InvalidPart, + {.code = "InvalidPart", .description = "", .httpCode = 400}}, + {S3Error::InvalidPartOrder, + {.code = "InvalidPartOrder", .description = "", .httpCode = 400}}, +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3ERRORRESPONSE_HH diff --git a/src/XrdS3/XrdS3ObjectStore.cc b/src/XrdS3/XrdS3ObjectStore.cc new file mode 100644 index 00000000000..27fe025431b --- /dev/null +++ b/src/XrdS3/XrdS3ObjectStore.cc @@ -0,0 +1,1017 @@ +// +// Created by segransm on 11/17/23. +// + +#include "XrdS3ObjectStore.hh" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "XrdCks/XrdCksCalcmd5.hh" +#include "XrdS3Req.hh" + +#define XRD_MULTIPART_UPLOAD_DIR "__XRD__MULTIPART__" + +namespace S3 { + +std::string GetDirName(const std::filesystem::path &p) { + std::string name; + for (const auto &v : p) { + if (!v.empty()) { + name = v; + } + } + return name; +} + +std::string GetXattr(const std::filesystem::path &path, + const std::string &key) { + std::string res; + + res.resize(getxattr(path.c_str(), key.c_str(), nullptr, 0)); + getxattr(path.c_str(), key.c_str(), res.data(), res.size()); + + return res; +} + +S3ObjectStore::S3ObjectStore(std::string p) : path(p) { + std::filesystem::create_directory(std::filesystem::path(path) / + XRD_MULTIPART_UPLOAD_DIR); + + for (const auto &bucket : std::filesystem::directory_iterator(path)) { + ssize_t i; + auto name = GetDirName(bucket.path()); + if (name == XRD_MULTIPART_UPLOAD_DIR) { + continue; + } + if ((i = getxattr(bucket.path().c_str(), "user.owner", BUFFER, BUFFSIZE)) >= + 0) { + BUFFER[i] = 0; + bucketOwners.insert({name, BUFFER}); + } + if ((i = getxattr(bucket.path().c_str(), "user.createdAt", BUFFER, + BUFFSIZE)) >= 0) { + BUFFER[i] = 0; + bucketInfo.insert({name, {name, BUFFER}}); + } + + MultipartUploads uploads; + for (const auto &entry : std::filesystem::directory_iterator( + std::filesystem::path(path) / XRD_MULTIPART_UPLOAD_DIR / name)) { + if (entry.is_directory()) { + MultipartUpload upload{GetXattr(entry.path(), "user.key"), {}}; + + for (const auto &part : std::filesystem::directory_iterator(entry)) { + auto etag = GetXattr(part.path(), "user.etag"); + auto last_modified = part.last_write_time(); + auto size = part.file_size(); + upload.parts.insert({std::stoul(part.path().filename()), + {etag, last_modified, size}}); + } + uploads.insert({GetDirName(entry.path()), upload}); + } + } + multipartUploads.insert({name, uploads}); + } +} + +bool S3ObjectStore::ValidateBucketName(const std::string &name) { + if (name.size() < 3 || name.size() > 63) { + return false; + } + + if (!isalnum(name[0]) || !isalnum(name[name.size() - 1])) { + return false; + } + + return std::all_of(name.begin(), name.end(), [](const auto &c) { + return islower(c) || isdigit(c) || c == '.' || c == '-'; + }); +} + +std::string S3ObjectStore::GetBucketOwner(const std::string &bucket) const { + auto it = bucketOwners.find(bucket); + if (it != bucketOwners.end()) { + return it->second; + } + return {}; +} + +S3Error S3ObjectStore::SetMetadata( + const std::string &object, + const std::map &metadata) { + for (const auto &meta : metadata) { + if (setxattr(object.c_str(), ("user." + meta.first).c_str(), + meta.second.c_str(), meta.second.size(), 0)) { + // fprintf(stderr, "UNABLE TO SET XATTR: %s", metadata) + return S3Error::InternalError; + } + } + return S3Error::None; +} + +S3Error S3ObjectStore::CreateBucket(const std::string &id, + const std::string &bucket, + const std::string &_location) { + if (!ValidateBucketName(bucket)) { + return S3Error::InvalidBucketName; + } + + auto owner = GetBucketOwner(bucket); + if (!owner.empty()) { + if (owner == id) { + return S3Error::BucketAlreadyOwnedByYou; + } + return S3Error::BucketAlreadyExists; + } + + auto bucketPath = path / bucket; + if (!std::filesystem::create_directory(std::filesystem::path(path) / + XRD_MULTIPART_UPLOAD_DIR / bucket) || + !std::filesystem::create_directory(std::filesystem::path(path) / + bucket)) { + return S3Error::InternalError; + } + + if (setxattr(bucketPath.c_str(), "user.owner", id.c_str(), id.size(), + XATTR_CREATE)) { + fprintf(stderr, "Unable to set xattr...\n"); + std::filesystem::remove_all(bucketPath.c_str()); + return S3Error::InternalError; + } + + auto now = std::to_string(std::time(nullptr)); + if (setxattr(bucketPath.c_str(), "user.createdAt", now.c_str(), now.size(), + XATTR_CREATE)) { + fprintf(stderr, "Unable to set xattr...\n"); + std::filesystem::remove_all(bucketPath.c_str()); + return S3Error::InternalError; + } + + fprintf(stderr, "Created bucket %s with owner %s\n", bucketPath.c_str(), + id.c_str()); + + bucketOwners.insert({bucket, id}); + bucketInfo.insert({bucket, {bucket, now}}); + multipartUploads.insert({bucket, {}}); + + return S3Error::None; +} + +std::pair BaseDir(std::string p) { + std::string basedir; + auto pos = p.rfind('/'); + if (pos != std::string::npos) { + basedir = p.substr(0, pos); + p.erase(0, pos + 1); + } + + return {basedir, p}; +} + +S3Error S3ObjectStore::DeleteBucket(const std::string &bucket) { + if (!ValidateBucketName(bucket)) { + return S3Error::InvalidBucketName; + } + + if (GetBucketOwner(bucket).empty()) { + return S3Error::NoSuchBucket; + } + + auto bucketPath = path / bucket; + + if (!std::filesystem::is_empty(bucketPath) || + !std::filesystem::is_empty(path / XRD_MULTIPART_UPLOAD_DIR / bucket)) { + return S3Error::BucketNotEmpty; + } + + std::filesystem::remove(bucketPath); + std::filesystem::remove(path / XRD_MULTIPART_UPLOAD_DIR / bucket); + + bucketOwners.erase(bucket); + bucketInfo.erase(bucket); + + return S3Error::None; +} + +S3Error S3ObjectStore::Object::Init(const std::filesystem::path &p) { + if (!exists(p) || is_directory(p)) { + return S3Error::NoSuchKey; + } + + std::vector attrnames; + std::vector attrlist; + auto attrlen = listxattr(p.c_str(), nullptr, 0); + attrlist.resize(attrlen); + listxattr(p.c_str(), attrlist.data(), attrlen); + auto i = attrlist.begin(); + while (i != attrlist.end()) { + auto tmp = std::find(i, attrlist.end(), 0); + attrnames.emplace_back(i, tmp); + i = tmp + 1; + } + std::vector value; + for (const auto &attr : attrnames) { + if (attr.substr(0, 5) != "user.") continue; + attrlen = getxattr(p.c_str(), attr.c_str(), nullptr, 0); + value.resize(attrlen); + if (attrlen > 0) { + value[attrlen - 1] = 0; + } + getxattr(p.c_str(), attr.c_str(), value.data(), attrlen); + attributes.insert({attr.substr(5), {value.begin(), value.end()}}); + } + + name = p; + this->size = file_size(p); + this->ifs = std::ifstream(p, std::ios_base::binary); + + return S3Error::None; +} + +S3Error S3ObjectStore::GetObject(const std::string &bucket, + const std::string &object, Object &obj) { + auto objectPath = path / bucket / object; + + return obj.Init(objectPath); +} + +S3Error S3ObjectStore::DeleteObject(const std::string &bucket, + const std::string &key) { + std::string base, obj; + + auto full_path = path / bucket / key; + + if (!std::filesystem::remove(full_path)) { + return S3Error::NoSuchKey; + } + + std::error_code ec; + do { + full_path = full_path.parent_path(); + } while (full_path != path / bucket && + std::filesystem::remove(full_path, ec)); + + return S3Error::None; +} + +std::vector S3ObjectStore::ListBuckets( + const std::string &id) const { + std::vector buckets; + for (const auto &b : bucketOwners) { + if (b.second == id) { + // todo: + buckets.push_back(bucketInfo.find(b.first)->second); + } + } + return buckets; +} + +// todo: this should only be a simple wrapper around a LostObjects that is used +// with ListObjectsV2 ListObjects ListObjectVersion, etc. +ListObjectsInfo S3ObjectStore::ListObjectVersions( + const std::string &bucket, const std::string &prefix, + const std::string &key_marker, const std::string &version_id_marker, + const char delimiter, int max_keys) { + auto f = [](const std::filesystem::path &root, const std::string &object) { + struct stat buf; + + if (!stat((root / object).c_str(), &buf)) { + return ObjectInfo{object, buf.st_mtim.tv_sec, std::to_string(buf.st_size), + ""}; + } + return ObjectInfo{}; + }; + + // todo: vid_marker + return ListObjectsCommon(bucket, prefix, key_marker, delimiter, max_keys, + true, f); +} + +int mkpath(std::string s, mode_t mode, size_t pos = 0) { + std::string dir; + int mdret; + + while ((pos = s.find_first_of('/', pos)) != std::string::npos) { + dir = s.substr(0, pos++); + if (dir.size() == 0) continue; // if leading / first time is 0 length + fprintf(stderr, "MKPATH: MKDIR: %s\n", dir.c_str()); + if ((mdret = mkdir(dir.c_str(), mode)) && errno != EEXIST) { + fprintf(stderr, "--> MKDIR: ERRNO: %s %s\n", dir.c_str(), + strerror(errno)); + return mdret; + } + fprintf(stderr, "MKDIR: ERRNO: %s %s\n", dir.c_str(), strerror(errno)); + } + return mdret; +} +// todo: +#define PUT_LIMIT 5000000000 + +S3Error S3ObjectStore::CopyObject(const std::string &bucket, + const std::string &key, Object &source_obj, + const Headers &reqheaders, Headers &headers) { + auto final_path = path / bucket / key; + auto tmp_path = path / XRD_MULTIPART_UPLOAD_DIR / bucket / + S3Utils::HexEncode(key + std::to_string(std::rand())); + std::error_code ec; + + if (is_directory(final_path)) { + return S3Error::ObjectExistAsDir; + } + + if (!std::filesystem::create_directories(final_path.parent_path(), ec)) { + if (ec.value() == ENOTDIR) { + return S3Error::ObjectExistInObjectPath; + } else if (ec.value() != 0) { + throw std::runtime_error("INTERNAL ERROR"); + return S3Error::InternalError; + } + } + + std::ofstream ofs(tmp_path, std::ios_base::binary | std::ios_base::trunc); + + if (!ofs.is_open()) { + throw std::runtime_error("INTERNAL ERROR"); + return S3Error::InternalError; + } + + XrdCksCalcmd5 xs; + xs.Init(); + size_t final_size = 0; + + auto stream = &source_obj.GetStream(); + streamsize i = 0; + while ((i = stream->readsome(BUFFER, BUFFSIZE)) > 0) { + xs.Update(BUFFER, i); + final_size += i; + ofs.write(BUFFER, i); + } + ofs.close(); + + char *fxs = xs.Final(); + std::vector md5(fxs, fxs + 16); + auto error = S3Error::None; + + std::map metadata; + if (S3Utils::HeaderEq(headers, "x-amz-metadata-directive", "REPLACE")) { + auto add_header = [&metadata, &headers](const std::string &name) { + if (S3Utils::HasHeader(headers, name)) { + metadata.insert({name, headers.find(name)->second}); + } + }; + + add_header("cache-control"); + add_header("content-disposition"); + add_header("content-type"); + + metadata.insert({"last-modified", std::to_string(std::time(nullptr))}); + } else { + metadata = source_obj.GetAttributes(); + // Metadata + // todo: validate headers + + // todo: validate calculated md5 + // todo: etag not always md5 + // (https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html) + auto md5hex = '"' + S3Utils::HexEncode(md5) + '"'; + metadata.insert({"etag", md5hex}); + headers.clear(); + headers.insert({"ETag", md5hex}); + } + error = SetMetadata(tmp_path, metadata); + if (error != S3Error::None) { + // todo: remove path too + std::filesystem::remove(tmp_path); + } + + std::filesystem::rename(tmp_path, final_path); + + return error; +} + +// todo: check path of multipart upload on creation +// todo: deny uploads with path of multipart upload if in progress, maybe +// create a temp object + +S3Error S3ObjectStore::UploadPart(XrdS3Req &req, const std::string &upload_id, + size_t part_number, unsigned long size, + bool chunked, Headers &headers) { + auto buploads = multipartUploads.find(req.bucket); + if (buploads == multipartUploads.end()) { + return S3Error::InternalError; + } + + auto upload = buploads->second.find(upload_id); + if (upload == buploads->second.end()) { + return S3Error::NoSuchUpload; + } + if (upload->second.key != req.object) { + return S3Error::InvalidRequest; + } + + auto part_path = std::filesystem::path(path) / XRD_MULTIPART_UPLOAD_DIR / + req.bucket / upload_id / std::to_string(part_number); + + fprintf(stderr, "Opening part %s\n", part_path.c_str()); + auto *f = fopen(part_path.c_str(), "w"); + if (f == nullptr) { + fprintf(stderr, "ERRNO: %s\n", strerror(errno)); + return S3Error::InternalError; + } + + auto error = S3Error::None; + // todo: dont check if not neededd, handle different cheksum types + XrdCksCalcmd5 xs; + xs.Init(); + S3Crypt::SHA256 sha; + sha.Init(); + + auto readBuffer = [&req, &xs, &sha, f](unsigned long length) { + int buflen = 0; + unsigned long readlen = 0; + char *ptr; + while (length > 0 && + (buflen = req.ReadBody( + length > INT_MAX ? INT_MAX : static_cast(length), &ptr, + false)) > 0) { + readlen = buflen; + if (length < readlen) { + return S3Error::IncompleteBody; + } + length -= readlen; + xs.Update(ptr, buflen); + sha.Update(ptr, buflen); + if (fwrite(ptr, 1, readlen, f) != readlen) { + return S3Error::InternalError; + } + } + if (buflen < 0 || length != 0) { + return S3Error::IncompleteBody; + } + return S3Error::None; + }; + + unsigned long final_size = size; + if (chunked) { + int length; + final_size = 0; + XrdOucString chunk_size; + + do { + req.BuffgetLine(chunk_size); + chunk_size.erasefromend(2); + try { + length = std::stoi(chunk_size.c_str(), nullptr, 16); + } catch (std::exception &) { + error = S3Error::InvalidRequest; + break; + } + final_size += length; + if (final_size > PUT_LIMIT) { + error = S3Error::EntityTooLarge; + break; + } + error = readBuffer(length); + req.BuffgetLine(chunk_size); + } while (error == S3Error::None && length != 0); + } else { + error = readBuffer(size); + } + + fclose(f); + + char *fxs = xs.Final(); + sha256_digest sha256 = sha.Finish(); + std::vector md5(fxs, fxs + 16); + if (error == S3Error::None) { + if (!req.md5.empty() && req.md5 != md5) { + error = S3Error::BadDigest; + } else if (!S3Utils::HeaderEq(req.lowercase_headers, "x-amz-content-sha256", + S3Utils::HexEncode(sha256))) { + error = S3Error::XAmzContentSHA256Mismatch; + } + } + + if (error != S3Error::None) { + // todo: remove path too + std::remove(part_path.c_str()); + return error; + } + + std::map metadata; + // Metadata + // todo: validate headers + // todo: etag not always md5 + // (https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html) + auto md5hex = '"' + S3Utils::HexEncode(md5) + '"'; + metadata.insert({"etag", md5hex}); + headers.insert({"ETag", md5hex}); + + error = SetMetadata(part_path, metadata); + if (error != S3Error::None) { + // todo: remove path too + fprintf(stderr, "meta error %d\n", (int)error); + std::remove(part_path.c_str()); + } else { + upload->second.parts[part_number] = {md5hex, last_write_time(part_path), + final_size}; + } + + return error; +} + +S3Error S3ObjectStore::PutObject(XrdS3Req &req, unsigned long size, + bool chunked, Headers &headers) { + auto final_path = path / req.bucket / req.object; + auto tmp_path = path / XRD_MULTIPART_UPLOAD_DIR / req.bucket / + S3Utils::HexEncode(req.object + std::to_string(std::rand())); + + std::error_code ec; + + if (is_directory(final_path)) { + return S3Error::ObjectExistAsDir; + } + + if (!std::filesystem::create_directories(final_path.parent_path(), ec)) { + if (ec.value() == ENOTDIR) { + return S3Error::ObjectExistInObjectPath; + } else if (ec.value() != 0) { + fprintf(stderr, "Unable to create directories... %d\n", ec.value()); + return S3Error::InternalError; + } + } + + std::ofstream ofs(tmp_path, std::ios_base::binary | std::ios_base::trunc); + + if (!ofs.is_open()) { + return S3Error::InternalError; + } + + auto error = S3Error::None; + // todo: dont check if not needed, handle different cheksum types + XrdCksCalcmd5 xs; + xs.Init(); + S3Crypt::SHA256 sha; + sha.Init(); + + auto readBuffer = [&req, &xs, &sha, &ofs](unsigned long length) { + int buflen = 0; + unsigned long readlen = 0; + char *ptr; + while (length > 0 && + (buflen = req.ReadBody( + length > INT_MAX ? INT_MAX : static_cast(length), &ptr, + false)) > 0) { + readlen = buflen; + if (length < readlen) { + return S3Error::IncompleteBody; + } + length -= readlen; + xs.Update(ptr, buflen); + sha.Update(ptr, buflen); + ofs.write(ptr, readlen); + } + if (buflen < 0 || length != 0) { + return S3Error::IncompleteBody; + } + return S3Error::None; + }; + + unsigned long final_size = size; + if (chunked) { + int length; + final_size = 0; + XrdOucString chunk_size; + + do { + req.BuffgetLine(chunk_size); + chunk_size.erasefromend(2); + try { + length = std::stoi(chunk_size.c_str(), nullptr, 16); + } catch (std::exception &) { + error = S3Error::InvalidRequest; + break; + } + final_size += length; + if (final_size > PUT_LIMIT) { + error = S3Error::EntityTooLarge; + break; + } + error = readBuffer(length); + req.BuffgetLine(chunk_size); + } while (error == S3Error::None && length != 0); + } else { + error = readBuffer(size); + } + + ofs.close(); + + char *fxs = xs.Final(); + sha256_digest sha256 = sha.Finish(); + std::vector md5(fxs, fxs + 16); + if (error == S3Error::None) { + if (!req.md5.empty() && req.md5 != md5) { + error = S3Error::BadDigest; + } else if (!S3Utils::HeaderEq(req.lowercase_headers, "x-amz-content-sha256", + S3Utils::HexEncode(sha256))) { + error = S3Error::XAmzContentSHA256Mismatch; + } + } + + if (error != S3Error::None) { + // todo: remove path too + std::filesystem::remove(tmp_path); + return error; + } + + std::map metadata; + // Metadata + // todo: validate headers + auto add_header = [&metadata, &req](const std::string &name) { + if (S3Utils::HasHeader(req.lowercase_headers, name)) { + metadata.insert({name, req.lowercase_headers.find(name)->second}); + } + }; + + add_header("cache-control"); + add_header("content-disposition"); + add_header("content-type"); + + metadata.insert({"last-modified", std::to_string(std::time(nullptr))}); + // todo: etag not always md5 + // (https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html) + auto md5hex = '"' + S3Utils::HexEncode(md5) + '"'; + metadata.insert({"etag", md5hex}); + headers.insert({"ETag", md5hex}); + // todo: all other system-headers + + // todo: handle non asccii chars: + // (https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html) + for (const auto &hd : req.lowercase_headers) { + if (hd.first.substr(0, 11) == "x-amz-meta-") { + metadata.insert({hd.first, hd.second}); + } + } + error = SetMetadata(tmp_path, metadata); + if (error != S3Error::None) { + // todo: remove path too + fprintf(stderr, "meta error %d\n", (int)error); + std::filesystem::remove(tmp_path); + } + + std::filesystem::rename(tmp_path, final_path); + + return error; +} + +std::tuple, std::vector> +S3ObjectStore::DeleteObjects(const std::string &bucket, + const std::vector &objects) { + auto bucketPath = path / bucket; + + std::vector deleted; + std::vector error; + + for (const auto &o : objects) { + fprintf(stderr, "Deleting object %s -> %s\n", bucket.c_str(), + o.key.c_str()); + auto err = DeleteObject(bucket, o.key); + if (err == S3Error::None || err == S3Error::NoSuchKey) { + deleted.push_back({o.key, o.version_id, false, ""}); + } else { + error.push_back({S3Error::InternalError, o.key, "", o.version_id}); + } + } + return {deleted, error}; +} + +ListObjectsInfo S3ObjectStore::ListObjectsV2( + const std::string &bucket, const std::string &prefix, + const std::string &continuation_token, const char delimiter, int max_keys, + bool fetch_owner, const std::string &start_after) { + auto f = [fetch_owner](const std::filesystem::path &root, + const std::string &object) { + struct stat buf; + + auto owner = ""; + if (fetch_owner) { + // todo + owner = "abc"; + } + + if (!stat((root / object).c_str(), &buf)) { + return ObjectInfo{object, buf.st_mtim.tv_sec, std::to_string(buf.st_size), + owner}; + } + return ObjectInfo{}; + }; + + return ListObjectsCommon( + bucket, prefix, + continuation_token.empty() ? start_after : continuation_token, delimiter, + max_keys, false, f); +} + +ListObjectsInfo S3ObjectStore::ListObjects(const std::string &bucket, + const std::string &prefix, + const std::string &marker, + const char delimiter, int max_keys) { + auto f = [](const std::filesystem::path &root, const std::string &object) { + struct stat buf; + + if (!stat((root / object).c_str(), &buf)) { + return ObjectInfo{object, buf.st_mtim.tv_sec, std::to_string(buf.st_size), + ""}; + } + return ObjectInfo{}; + }; + + return ListObjectsCommon(bucket, prefix, marker, delimiter, max_keys, false, + f); +} + +ListObjectsInfo S3ObjectStore::ListObjectsCommon( + const std::string &bucket, std::string prefix, const std::string &marker, + char delimiter, int max_keys, bool get_versions, + const std::function &f) { + std::string basedir; + + if (prefix == "/" || max_keys == 0) { + return {}; + } + + std::tie(basedir, prefix) = BaseDir(prefix); + + if (!basedir.empty()) { + basedir += '/'; + } + auto fullpath = path / bucket; + + struct BasicPath { + std::string base; + std::string name; + unsigned char d_type; + }; + + fprintf(stderr, "fp: %s bd: %s, pf: %s\n", fullpath.c_str(), basedir.c_str(), + prefix.c_str()); + std::deque entries; + + struct dirent **ent = nullptr; + int n; + if ((n = scandir((fullpath / basedir).c_str(), &ent, nullptr, alphasort)) < + 0) { + return {}; + } + for (auto i = 0; i < n; i++) { + if (prefix.compare(0, prefix.size(), std::string(ent[i]->d_name), 0, + prefix.size()) == 0) { + entries.push_back({basedir, ent[i]->d_name, ent[i]->d_type}); + } + free(ent[i]); + } + free(ent); + ent = nullptr; + + ListObjectsInfo list{}; + + while (!entries.empty()) { + auto entry = entries.front(); + entries.pop_front(); + + if (entry.name == "." || entry.name == "..") { + continue; + } + + auto entry_path = entry.base + entry.name; + + // Skip to marker + if (entry_path.compare(0, marker.size(), marker) < 0) { + continue; + } + // When listing versions, the marker indicated the key to start with, and + // not the last key to skip + if (!get_versions) { + if (!marker.empty() && entry_path == marker) { + continue; + } + } + + if (list.objects.size() + list.common_prefixes.size() >= (size_t)max_keys) { + list.is_truncated = true; + list.next_marker = entry_path; + list.next_vid_marker = "1"; + return list; + } + + fprintf(stderr, "Checking entry: %s\n", entry_path.c_str()); + size_t m; + if ((m = entry_path.find(delimiter, prefix.length() + basedir.length() + + 1)) != std::string::npos) { + fprintf(stderr, "common prefix: %s %zu\n", entry_path.c_str(), m); + list.common_prefixes.insert(entry_path.substr(0, m + 1)); + list.key_marker = entry_path.substr(0, m + 1); + list.vid_marker = "1"; + continue; + } + + if (entry.d_type == DT_UNKNOWN) { + // todo + throw std::runtime_error("Unknown entry type"); + } + + if (entry.d_type == DT_DIR) { + if (delimiter == '/') { + fprintf(stderr, "common prefix dir: %s\n", entry_path.c_str()); + list.common_prefixes.insert(entry_path + '/'); + list.key_marker = entry_path + '/'; + list.vid_marker = "1"; + continue; + } + + if ((n = scandir((fullpath / entry_path).c_str(), &ent, nullptr, + alphasort)) < 0) { + return {}; + } + for (size_t i = n; i > 0; i--) { + entries.push_front( + {entry_path + '/', ent[i - 1]->d_name, ent[i - 1]->d_type}); + free(ent[i - 1]); + } + free(ent); + continue; + } + + list.objects.push_back(f(fullpath, entry_path)); + list.key_marker = entry_path; + list.vid_marker = "1"; + } + list.next_marker = ""; + list.next_vid_marker = ""; + return list; +} + +std::string S3ObjectStore::CreateMultipartUpload(XrdS3Req &req, + const std::string &bucket, + const std::string &key) { + // todo: handle metadata + auto upload_id = S3Utils::HexEncode(req.ctx->crypt.mSha256.calculate( + bucket + key + std::to_string(std::rand()))); + + auto p = std::filesystem::path(path) / XRD_MULTIPART_UPLOAD_DIR / bucket / + upload_id; + if (!std::filesystem::create_directory(p)) { + return ""; + } + + if (setxattr(p.c_str(), "user.key", key.c_str(), key.size(), XATTR_CREATE)) { + std::filesystem::remove(p); + return ""; + } + + auto it = multipartUploads.find(bucket); + if (it != multipartUploads.end()) { + it->second.insert({upload_id, {key, {}}}); + } else { + multipartUploads.insert({bucket, {{upload_id, {key, {}}}}}); + } + + return upload_id; +} + +std::vector +S3ObjectStore::ListMultipartUploads(const std::string &bucket) { + auto it = multipartUploads.find(bucket); + if (it == multipartUploads.end()) { + return {}; + } + + std::vector res; + res.reserve(it->second.size()); + for (const auto &[id, info] : it->second) { + res.push_back({info.key, id}); + } + + return res; +} + +S3Error S3ObjectStore::AbortMultipartUpload(const std::string &bucket, + const std::string &key, + const std::string &upload_id) { + auto buploads = multipartUploads.find(bucket); + if (buploads == multipartUploads.end()) { + return S3Error::InternalError; + } + + auto upload = buploads->second.find(upload_id); + if (upload == buploads->second.end()) { + return S3Error::NoSuchUpload; + } + if (upload->second.key != key) { + return S3Error::InvalidRequest; + } + + buploads->second.erase(upload_id); + std::filesystem::remove_all(std::filesystem::path(path) / + XRD_MULTIPART_UPLOAD_DIR / bucket / upload_id); + + return S3Error::None; +} + +std::pair> +S3ObjectStore::ListParts(const std::string &bucket, const std::string &key, + const std::string &upload_id) { + auto buploads = multipartUploads.find(bucket); + if (buploads == multipartUploads.end()) { + return {S3Error::InternalError, {}}; + } + + auto upload = buploads->second.find(upload_id); + if (upload == buploads->second.end()) { + return {S3Error::NoSuchUpload, {}}; + } + if (upload->second.key != key) { + return {S3Error::InvalidRequest, {}}; + } + + std::vector parts; + + for (const auto &[n, info] : upload->second.parts) { + parts.push_back({info.etag, info.last_modified, n, info.size}); + } + + return {S3Error::None, parts}; +} + +S3Error S3ObjectStore::CompleteMultipartUpload( + XrdS3Req &req, const std::string &bucket, const std::string &key, + const std::string &upload_id, const std::vector &parts) { + auto buploads = multipartUploads.find(bucket); + if (buploads == multipartUploads.end()) { + return S3Error::InternalError; + } + + auto upload = buploads->second.find(upload_id); + if (upload == buploads->second.end()) { + return S3Error::NoSuchUpload; + } + if (upload->second.key != key) { + return S3Error::InvalidRequest; + } + + size_t max = 0; + auto it = upload->second.parts.end(); + for (const auto &[etag, _, n, __] : parts) { + if ((it = upload->second.parts.find(n)) == upload->second.parts.end()) { + return S3Error::InvalidPart; + } + if (it->second.etag != etag) { + return S3Error::InvalidPart; + } + if (n <= max) { + return S3Error::InvalidPartOrder; + } + max = n; + } + + auto p = std::filesystem::path(path) / bucket / key; + create_directories(p.parent_path()); + + std::ofstream ofs(p, std::ios_base::binary | std::ios_base::trunc); + + // todo: send spaces to avoid timing out connection + for (const auto &part : parts) { + std::ifstream ifs(std::filesystem::path(path) / XRD_MULTIPART_UPLOAD_DIR / + bucket / upload_id / std::to_string(part.part_number), + std::ios_base::binary); + + ofs << ifs.rdbuf(); + ifs.close(); + } + + // todo: set metadata + ofs.close(); + + std::filesystem::remove_all(std::filesystem::path(path) / + XRD_MULTIPART_UPLOAD_DIR / bucket / upload_id); + buploads->second.erase(upload); + + return S3Error::None; +} + +} // namespace S3 diff --git a/src/XrdS3/XrdS3ObjectStore.hh b/src/XrdS3/XrdS3ObjectStore.hh new file mode 100644 index 00000000000..19285db958e --- /dev/null +++ b/src/XrdS3/XrdS3ObjectStore.hh @@ -0,0 +1,188 @@ +// +// Created by segransm on 11/17/23. +// + +#ifndef XROOTD_XRDS3OBJECTSTORE_HH +#define XROOTD_XRDS3OBJECTSTORE_HH + +#include +#include +#include +#include +#include +#include + +#include "XrdS3ErrorResponse.hh" +#include "XrdS3Req.hh" + +namespace S3 { + +struct ObjectInfo { + std::string name; + time_t last_modified; + std::string size; + std::string owner; +}; + +struct ListObjectsInfo { + bool is_truncated; + std::string key_marker; + std::string next_marker; + std::string vid_marker; + std::string next_vid_marker; + std::vector objects; + std::set common_prefixes; +}; + +struct DeletedObject { + std::string key; + std::string version_id; + bool delete_marker; + std::string delete_marker_version_id; +}; + +struct ErrorObject { + S3Error code; + std::string key; + std::string message; + std::string version_id; +}; + +struct SimpleObject { + std::string key; + std::string version_id; +}; + +class S3ObjectStore { + public: + S3ObjectStore() = default; + + explicit S3ObjectStore(std::string path); + + ~S3ObjectStore() = default; + + struct BucketInfo { + std::string name; + std::string created; + }; + class Object { + public: + Object() = default; + ~Object() = default; + + S3Error Init(const std::filesystem::path &path); + + std::ifstream &GetStream() { return ifs; }; + + size_t GetSize() const { return size; }; + const std::map &GetAttributes() const { + return attributes; + }; + + private: + // 32 MB + const int bufsize = 32000000; + size_t read{}; + std::string name{}; + size_t size{}; + std::ifstream ifs{}; + std::map attributes{}; + }; + + // todo: handle location constraint + S3Error CreateBucket(const std::string &id, const std::string &bucket, + const std::string &_location); + S3Error DeleteBucket(const std::string &bucket); + S3Error GetObject(const std::string &bucket, const std::string &object, + Object &obj); + S3Error DeleteObject(const std::string &bucket, const std::string &object); + std::string GetBucketOwner(const std::string &bucket) const; + static S3Error SetMetadata( + const std::string &object, + const std::map &metadata); + std::vector ListBuckets(const std::string &id) const; + ListObjectsInfo ListObjectVersions(const std::string &bucket, + const std::string &prefix, + const std::string &key_marker, + const std::string &version_id_marker, + char delimiter, int max_keys); + ListObjectsInfo ListObjectsV2(const std::string &bucket, + const std::string &prefix, + const std::string &continuation_token, + char delimiter, int max_keys, bool fetch_owner, + const std::string &start_after); + ListObjectsInfo ListObjects(const std::string &bucket, + const std::string &prefix, + const std::string &marker, char delimiter, + int max_keys); + S3Error PutObject(XrdS3Req &req, unsigned long size, bool chunked, + Headers &headers); + + std::tuple, std::vector> + DeleteObjects(const std::string &bucket, + const std::vector &objects); + + S3Error CopyObject(const std::string &bucket, const std::string &object, + Object &source_obj, const Headers &reqheaders, + Headers &headers); + + std::string CreateMultipartUpload(XrdS3Req &req, const std::string &bucket, + const std::string &object); + + struct Part { + std::string etag; + std::filesystem::file_time_type last_modified; + size_t size; + }; + struct MultipartUpload { + std::string key; + std::map parts; + }; + struct MultipartUploadInfo { + std::string key; + std::string upload_id; + }; + struct PartInfo { + std::string etag; + std::filesystem::file_time_type last_modified; + size_t part_number; + size_t size; + }; + + std::vector ListMultipartUploads( + const std::string &bucket); + + S3Error AbortMultipartUpload(const string &bucket, const string &key, + const string &upload_id); + + typedef std::map MultipartUploads; + std::pair> ListParts( + const string &bucket, const string &key, const string &upload_id); + + S3Error UploadPart(XrdS3Req &req, const string &upload_id, size_t part_number, + unsigned long size, bool chunked, Headers &headers); + S3Error CompleteMultipartUpload(XrdS3Req &req, const string &bucket, + const string &key, const string &upload_id, + const std::vector &parts); + + private: + static const int BUFFSIZE = 8000; + char BUFFER[BUFFSIZE]{}; + static bool ValidateBucketName(const std::string &name); + + std::filesystem::path path; + + std::map bucketOwners; + std::map bucketInfo; + std::map multipartUploads; + + ListObjectsInfo ListObjectsCommon( + const std::string &bucket, std::string prefix, const std::string &marker, + char delimiter, int max_keys, bool get_versions, + const std::function &f); +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3OBJECTSTORE_HH diff --git a/src/XrdS3/XrdS3Req.cc b/src/XrdS3/XrdS3Req.cc new file mode 100644 index 00000000000..2b438574938 --- /dev/null +++ b/src/XrdS3/XrdS3Req.cc @@ -0,0 +1,324 @@ +// +// Created by segransm on 11/9/23. +// + +#include "XrdS3Req.hh" + +#include "XrdOuc/XrdOucString.hh" +#include "XrdOuc/XrdOucTUtils.hh" +#include "XrdS3.hh" +#include "XrdS3Auth.hh" +#include "XrdS3Xml.hh" + +namespace S3 { + +XrdS3Req::XrdS3Req(Context *ctx, XrdHttpExtReq &req) + : XrdHttpExtReq(req), ctx(ctx) { + valid = false; + ParseReq(); + + if (!ValidateAuth()) { + return; + } + + fprintf(stderr, "obj: %s\n", object.c_str()); + auto error = ValidatePath(object); + fprintf(stderr, "validated: %s %d\n", object.c_str(), (int)error); + // todo: check query params? + if (error != S3Error::None) { + S3ErrorResponse(error, "", "", false); + return; + } + + valid = true; +}; + +void XrdS3Req::ParseReq() { + // Convert headers to lowercase + for (const auto &hd : headers) { + XrdOucString key(hd.first.c_str()); + key.lower(0, 0); + lowercase_headers.insert(std::make_pair(key.c_str(), hd.second)); + } + + // todo: check it exists + std::string uri = headers["xrd-http-fullresource"]; + + size_t pos = uri.find('?'); + if (pos == std::string::npos) { + // XrdReq already uri decode + uri_path = uri; + } else { + uri_path = uri.substr(0, pos); + std::vector tokens; + XrdOucTUtils::splitString(tokens, uri.substr(pos + 1), "&"); + + for (const auto ¶m : tokens) { + pos = param.find('='); + if (pos == std::string::npos) { + query.insert({ctx->utils.UriDecode(param), ""}); + } else { + query.insert({ctx->utils.UriDecode(param.substr(0, pos)), + ctx->utils.UriDecode(param.substr(pos + 1))}); + } + } + } + + // todo: support virtual hosted request + bucket = uri_path.substr(1); + pos = bucket.find('/'); + if (pos == std::string::npos) { + object = ""; + } else { + object = bucket.substr(pos + 1); + bucket.erase(pos); + } + + if (verb == "GET") { + method = Get; + } else if (verb == "HEAD") { + method = Head; + } else if (verb == "POST") { + method = Post; + } else if (verb == "PUT") { + method = Put; + } else if (verb == "PATCH") { + method = Patch; + } else if (verb == "DELETE") { + method = Delete; + } else if (verb == "CONNECT") { + method = Connect; + } else if (verb == "OPTIONS") { + method = Options; + } else if (verb == "TRACE") { + method = Trace; + } else { + // todo: ret + throw std::runtime_error("INVALID METHOD"); + } +} + +bool XrdS3Req::ValidateAuth() { + fprintf(stderr, "VALIDATING AUTH %d\n", + (int)S3Auth::GetRequestAuthType(*this)); + switch (S3Auth::GetRequestAuthType(*this)) { + case AuthType::Signed: + case AuthType::StreamingSigned: + case AuthType::StreamingSignedTrailer: { + // todo: validate date + fprintf(stderr, "Parsing date...\n"); + if (!ParseDateHeader(lowercase_headers)) { + S3ErrorResponse(S3Error::AccessDenied); + return false; + } + fprintf(stderr, "Parsing md5...\n"); + if (!ParseMd5Header()) { + S3ErrorResponse(S3Error::InvalidDigest); + return false; + } + if (!ParseContentLengthHeader()) { + S3ErrorResponse(S3Error::InvalidRequest); + return false; + } + return true; + } + case AuthType::Unknown: + S3ErrorResponse(S3Error::AccessDenied); + return false; + default: + S3ErrorResponse(S3Error::AccessDenied); + return false; + } +} + +// todo: maybe overkill and not needed +bool XrdS3Req::ParseDateHeader(const Headers &headers) { + fprintf(stderr, "Parsing date...\n"); + auto it = headers.find("x-amz-date"); + if (it != headers.end()) { + auto ret = strptime(it->second.c_str(), "%Y%m%dT%H%M%SZ", &date); + if (ret != nullptr && *ret == '\0') { + fprintf(stderr, "Found valid x-amz-date\n"); + return true; + } + } + + it = headers.find("date"); + if (it == headers.end()) { + fprintf(stderr, "No x-amz-date or date header\n"); + return false; + } + // Try ISO8601 + auto ret = strptime(it->second.c_str(), "%Y%m%dT%H%M%SZ", &date); + if (ret != nullptr && *ret == '\0') { + fprintf(stderr, "Found valid date (ISO8601)\n"); + return true; + } + // Try http date format + ret = strptime(it->second.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &date); + if (ret != nullptr && *ret == '\0') { + fprintf(stderr, "Found valid date\n"); + return true; + } + + fprintf(stderr, "Date is invalid: %s\n", it->second.c_str()); + return false; +} + +bool XrdS3Req::ParseMd5Header() { + auto it = lowercase_headers.find("content-md5"); + if (it == lowercase_headers.end()) { + return true; + } + if (it->second.empty()) { + return false; + } + md5 = ctx->crypt.mBase64.decode(it->second); + + fprintf(stderr, "%zu\n", md5.size()); + if (md5.size() != 16) { + return false; + } + return true; +} +bool XrdS3Req::ParseContentLengthHeader() { + fprintf(stderr, "VALIDATING MD5\n"); + auto it = lowercase_headers.find("content-length"); + if (it == lowercase_headers.end()) { + return true; + } + if (it->second.empty()) { + return false; + } + + try { + int i = std::stoi(it->second); + if (i < 0) { + return false; + } + } catch (std::exception &e) { + return false; + } + + return true; +} + +S3Error XrdS3Req::ValidatePath(const std::string &path) { + std::vector components; + + fprintf(stderr, "PATH: `%s`\n", path.c_str()); + if (path.empty()) { + return S3Error::None; + } + + if (path[path.size() - 1] == '/') { + return S3Error::InvalidObjectName; + } + + XrdOucTUtils::splitString(components, path, "/"); + if (!std::all_of(components.begin(), components.end(), [](const auto &c) { + return (!c.empty() && c != "." && c != ".."); + })) { + return S3Error::InvalidObjectName; + } + + // Key names containing only whitespaces get skipped when parsing xml with + // tinyxml2 + auto name = components.end() - 1; + if (std::all_of(name->begin(), name->end(), + [](const auto &c) { return isspace(c); })) { + return S3Error::InvalidObjectName; + } + + return S3Error::None; +} + +int XrdS3Req::S3ErrorResponse(S3Error err) { + return S3ErrorResponse(err, "", "", false); +} + +int XrdS3Req::S3ErrorResponse(S3Error err, const string &ressource, + const string &request_id, bool chunked) { + S3Xml printer; + + auto e = S3ErrorMap.find(err); + if (e == S3ErrorMap.end()) { + throw std::runtime_error("Error " + std::to_string(static_cast(err)) + + " not implemented"); + } + if (err == S3Error::InternalError) { + throw std::runtime_error("INTERNAL ERROR"); + } + auto f = e->second; + + fprintf(stderr, "Sending error: %s %s %d\n", f.code.c_str(), + f.description.c_str(), f.httpCode); + printer.OpenElement("Error"); + printer.AddElement("Code", f.code); + printer.AddElement("Message", f.description); + printer.AddElement("Ressource", ressource); + printer.AddElement("RequestId", request_id); + printer.CloseElement(); + + if (chunked) { + return ChunkResp(printer.CStr(), printer.CStrSize() - 1); + } else { + return SendSimpleResp(f.httpCode, nullptr, nullptr, printer.CStr(), + printer.CStrSize() - 1); + } +} + +std::string MergeHeaders(const std::map &headers) { + std::stringstream ss; + + for (const auto &hd : headers) { + ss << hd.first << ':' << hd.second << '\n'; + } + std::string res = ss.str(); + + return res.substr(0, res.empty() ? 0 : res.length() - 1); +} + +int XrdS3Req::S3Response(int code, const map &headers, + const string &body) { + std::string headers_str = MergeHeaders(headers); + + return SendSimpleResp(code, nullptr, headers_str.c_str(), body.c_str(), + body.size()); +} + +int XrdS3Req::S3Response(int code, const map &headers, + const char *body, long long size) { + std::string headers_str = MergeHeaders(headers); + + fprintf(stderr, "headers: %s\n", headers_str.c_str()); + return SendSimpleResp(code, nullptr, headers_str.c_str(), body, size); +} + +int XrdS3Req::Ok() { return S3Response(200); } + +int XrdS3Req::StartChunkedOk() { + return XrdHttpExtReq::StartChunkedResp(200, nullptr, nullptr); +} + +int XrdS3Req::StartChunkedResp(int code, const Headers &headers) { + return XrdHttpExtReq::StartChunkedResp(200, nullptr, + MergeHeaders(headers).c_str()); +} + +int XrdS3Req::S3Response(int code) { + return SendSimpleResp(code, nullptr, nullptr, nullptr, 0); +} + +int XrdS3Req::ReadBody(int blen, char **data, bool wait) { + if (!has_read) { + has_read = true; + if (S3Utils::HeaderEq(lowercase_headers, "expect", "100-continue")) { + S3Response(100); + } + } + + return XrdHttpExtReq::BuffgetData(blen, data, wait); +} + +} // namespace S3 diff --git a/src/XrdS3/XrdS3Req.hh b/src/XrdS3/XrdS3Req.hh new file mode 100644 index 00000000000..0d429ca339f --- /dev/null +++ b/src/XrdS3/XrdS3Req.hh @@ -0,0 +1,100 @@ +// +// Created by segransm on 11/9/23. +// + +#ifndef XROOTD_XRDS3REQ_HH +#define XROOTD_XRDS3REQ_HH + +#include + +#include "XrdHttp/XrdHttpExtHandler.hh" +#include "XrdS3Crypt.hh" +#include "XrdS3ErrorResponse.hh" +#include "XrdS3Utils.hh" + +namespace S3 { + +using Headers = std::map; + +enum HttpMethod { + Get, + Head, + Post, + Put, + Patch, + Delete, + Connect, + Options, + Trace +}; + +const std::map HttpMethodMap{ + {Get, "GET"}, {Head, "HEAD"}, {Post, "POST"}, + {Put, "PUT"}, {Patch, "PATCH"}, {Delete, "DELETE"}, + {Connect, "CONNECT"}, {Options, "OPTIONS"}, {Trace, "TRACE"}, +}; + +struct Context { + S3Utils utils; + S3Crypt crypt; + XrdSysError *log; +}; + +class XrdS3Req : protected XrdHttpExtReq { + public: + XrdS3Req(Context *ctx, XrdHttpExtReq &req); + + ~XrdS3Req() = default; + + bool isValid() const { return valid; } + + void ParseReq(); + + bool valid; + std::string bucket; + std::string object; + Context *ctx; + + using Headers = std::map; + + HttpMethod method; + std::string uri_path; + struct tm date {}; + std::string id; + std::vector md5; + + std::map query; + std::map lowercase_headers; + bool ParseDateHeader(const Headers &headers); + bool ValidateAuth(); + static S3Error ValidatePath(const std::string &path); + bool ParseMd5Header(); + bool ParseContentLengthHeader(); + + int Ok(); + int S3Response(int code); + int S3Response(int code, const std::map &headers, + const std::string &body); + int S3Response(int code, const std::map &headers, + const char *body, long long size); + + + int S3ErrorResponse(S3Error err); + int S3ErrorResponse(S3Error err, const std::string &ressource, + const std::string &request_id, bool chunked); + + int ReadBody(int blen, char **data, bool wait); + + int StartChunkedOk(); + int StartChunkedResp(int code, const Headers &headers); + using XrdHttpExtReq::BuffgetLine; + using XrdHttpExtReq::ChunkResp; + private: + bool has_read{}; +}; + +using HandlerFunc = std::function; + +} // namespace S3 + +#endif // XROOTD_XRDS3REQ_HH diff --git a/src/XrdS3/XrdS3Router.cc b/src/XrdS3/XrdS3Router.cc new file mode 100644 index 00000000000..68711eeb996 --- /dev/null +++ b/src/XrdS3/XrdS3Router.cc @@ -0,0 +1,90 @@ +// +// Created by segransm on 11/9/23. +// + +#include "XrdS3Router.hh" + +namespace S3 { + +S3Route &S3Route::Method(const HttpMethod &m) { + matchers.emplace_back( + [m](const XrdS3Req &req) { return MatchMethod(m, req); }); + return *this; +} + +S3Route &S3Route::Path(const PathMatch &p) { + matchers.emplace_back([p](const XrdS3Req &req) { return MatchPath(p, req); }); + return *this; +} + +S3Route &S3Route::Queries( + const std::vector> &q) { + matchers.emplace_back( + [q](const XrdS3Req &req) { return MatchMap(q, req.query); }); + return *this; +} + +S3Route &S3Route::Headers( + const std::vector> &h) { + matchers.emplace_back( + [h](const XrdS3Req &req) { return MatchMap(h, req.lowercase_headers); }); + return *this; +} + +bool S3Route::Match(const XrdS3Req &req) const { + return std::all_of(matchers.begin(), matchers.end(), + [req](const auto &matcher) { return matcher(req); }); +} + +const HandlerFunc &S3Route::Handler() const { return handler; }; + +const std::string &S3Route::GetName() const { return name; }; + +bool S3Route::MatchMethod(const HttpMethod &method, const XrdS3Req &req) { + return method == req.method; +} + +bool S3Route::MatchPath(const PathMatch &path, const XrdS3Req &req) { + // Path will always start with '/' + switch (path) { + case PathMatch::MatchObject: + return !req.object.empty(); + case PathMatch::MatchBucket: + return !req.bucket.empty() && req.object.empty(); + case PathMatch::MatchNoBucket: + return req.bucket.empty() && req.object.empty(); + } + return false; +} + +bool S3Route::MatchMap( + const std::vector> &required, + const std::map &map) { + return std::all_of( + required.begin(), required.end(), [map](const auto ¶m) { + auto it = map.find(param.first); + + return (it != map.end() && + (param.second == "*" || + (param.second == "+" ? !it->second.empty() + : it->second == param.second))); + }); +} + +void S3Router::AddRoute(S3Route &route) { + mLog.Say("Registered route:", route.GetName().c_str()); + routes.push_back(route); +} + +int S3Router::ProcessReq(XrdS3Req &req) { + for (const auto &route : routes) { + if (route.Match(req)) { + mLog.Say("Found matching route for req:", route.GetName().c_str()); + return route.Handler()(req); + } + } + mLog.Say("Unable to find matching route for request..."); + return not_found_handler(req); +} + +} // namespace S3 diff --git a/src/XrdS3/XrdS3Router.hh b/src/XrdS3/XrdS3Router.hh new file mode 100644 index 00000000000..77e2f1701bb --- /dev/null +++ b/src/XrdS3/XrdS3Router.hh @@ -0,0 +1,81 @@ +// +// Created by segransm on 11/9/23. +// + +#ifndef XROOTD_XRDS3ROUTER_HH +#define XROOTD_XRDS3ROUTER_HH + +#include +#include +#include +#include +#include +#include + +#include "XrdS3Action.hh" +#include "XrdS3Req.hh" +#include "XrdSys/XrdSysError.hh" + +namespace S3 { + +enum class PathMatch { MatchObject, MatchBucket, MatchNoBucket }; + +class S3Route { + public: + explicit S3Route(HandlerFunc fn) : handler(std::move(fn)){}; + S3Route(Action _, std::string name, HandlerFunc fn) + : handler(std::move(fn)), name(std::move(name)){}; + + ~S3Route() = default; + + S3Route &Method(const HttpMethod &m); + + S3Route &Path(const PathMatch &p); + + S3Route &Queries(const std::vector> &q); + + S3Route &Headers(const std::vector> &h); + + bool Match(const XrdS3Req &req) const; + + const HandlerFunc &Handler() const; + + const std::string &GetName() const; + + private: + using Matcher = std::function; + + static bool MatchMethod(const HttpMethod &method, const XrdS3Req &req); + + static bool MatchPath(const PathMatch &path, const XrdS3Req &req); + + static bool MatchMap( + const std::vector> &required, + const std::map &map); + + std::vector matchers; + const HandlerFunc handler; + const std::string name; +}; + +class S3Router { + public: + explicit S3Router(XrdSysError *log, HandlerFunc fn) + : mLog(log->logger(), "S3Router_"), not_found_handler(std::move(fn)){}; + + ~S3Router() = default; + + void AddRoute(S3Route &route); + + int ProcessReq(XrdS3Req &req); + + private: + XrdSysError mLog; + std::vector routes; + + HandlerFunc not_found_handler; +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3ROUTER_HH diff --git a/src/XrdS3/XrdS3Utils.cc b/src/XrdS3/XrdS3Utils.cc new file mode 100644 index 00000000000..e90e7fc6ab0 --- /dev/null +++ b/src/XrdS3/XrdS3Utils.cc @@ -0,0 +1,153 @@ +// +// Created by segransm on 11/3/23. +// + +#include "XrdS3Utils.hh" + +#include +#include +#include + +namespace S3 { + +std::string S3Utils::UriEncode(const std::bitset<256> &encoder, + const std::string &str) { + std::ostringstream res; + + for (const auto &c : str) { + if (encoder[(unsigned char)c]) { + res << c; + } else { + res << "%" << std::uppercase << std::hex << std::setw(2) + << std::setfill('0') << (int)c; + } + } + + return res.str(); +} + +std::string S3Utils::UriDecode(const std::string &str) { + size_t i = 0; + std::string res; + res.reserve(str.size()); + unsigned char v1, v2; + + while (i < str.size()) { + if (str[i] == '%' && i + 2 < str.size()) { + v1 = mDecoder[(unsigned char)str[i + 1]]; + v2 = mDecoder[(unsigned char)str[i + 2]]; + if ((v1 | v2) == 0xFF) { + res += str[i++]; + } else { + res += (char)((v1 << 4) | v2); + i += 3; + } + } else + res += str[i++]; + } + + return res; +} + +S3Utils::S3Utils() { + for (int i = 0; i < 256; ++i) { + mEncoder[i] = isalnum(i) || i == '-' || i == '.' || i == '_' || i == '~'; + mObjectEncoder[i] = mEncoder[i]; + + if (isxdigit(i)) { + mDecoder[i] = (i < 'A') ? (i - '0') : ((i & ~0x20) - 'A' + 10); + } else { + mDecoder[i] = 0xFF; + } + } + mObjectEncoder['/'] = true; +} + +std::string S3Utils::UriEncode(const std::string &str) { + return UriEncode(mEncoder, str); +} + +std::string S3Utils::ObjectUriEncode(const std::string &str) { + return UriEncode(mObjectEncoder, str); +} + +void S3Utils::TrimAll(std::string &str) { + // Trim leading non-letters + size_t n = 0; + while (n < str.size() && isspace(str[n])) { + n++; + } + str.erase(0, n); + + if (str.empty()) { + return; + } + + // Trim trailing non-letters + n = str.size() - 1; + while (n > 0 && isspace(str[n])) { + n--; + } + str.erase(n + 1); + + // Squash sequential spaces + n = 0; + while (n < str.size()) { + size_t x = 0; + while (n + x < str.size() && isspace(str[n + x])) { + x++; + } + if (x > 1) { + str.erase(n, x - 1); + } + n++; + } +} + +bool S3Utils::HasHeader(const std::map &header, + const std::string &key) { + return (header.find(key) != header.end()); +} + +bool S3Utils::HeaderEq(const std::map &header, + const std::string &key, const std::string &val) { + auto it = header.find(key); + + return (it != header.end() && it->second == val); +} + +bool S3Utils::HeaderStartsWith(const std::map &header, + const std::string &key, const std::string &val) { + auto it = header.find(key); + + return (it != header.end() && it->second.substr(0, val.length()) == val); +} + +std::string S3Utils::timestampToIso8016(const std::string &t) { + try { + return timestampToIso8016(std::stol(t)); + } catch (std::exception &e) { + return ""; + } +} + +std::string S3Utils::timestampToIso8016( + const std::filesystem::file_time_type &t) { + fprintf(stderr, "time since epoch: %ld\n", std::chrono::duration_cast(t.time_since_epoch()).count()); + return timestampToIso8016(std::chrono::duration_cast(t.time_since_epoch()) + .count()); +} +std::string S3Utils::timestampToIso8016(const time_t &t) { + struct tm *date = gmtime(&t); + return timestampToIso8016(date); +} + +std::string S3Utils::timestampToIso8016(const struct tm *t) { + char date_iso8601[17]{}; + if (!t || strftime(date_iso8601, 17, "%Y%m%dT%H%M%SZ", t) != 16) { + return ""; + } + return date_iso8601; +} + +} // namespace S3 diff --git a/src/XrdS3/XrdS3Utils.hh b/src/XrdS3/XrdS3Utils.hh new file mode 100644 index 00000000000..a0d42f7da03 --- /dev/null +++ b/src/XrdS3/XrdS3Utils.hh @@ -0,0 +1,83 @@ +// +// Created by segransm on 11/3/23. +// + +#ifndef XROOTD_XRDS3UTILS_HH +#define XROOTD_XRDS3UTILS_HH + +#include +#include +#include +#include +#include +#include +#include + +namespace S3 { + +class S3Utils { + public: + S3Utils(); + + ~S3Utils() = default; + + std::string UriEncode(const std::string &str); + std::string ObjectUriEncode(const std::string &str); + + std::string UriDecode(const std::string &str); + + template + static std::string HexEncode(const T &s) { + std::stringstream ss; + + for (const auto &c : s) { + ss << std::hex << std::setw(2) << std::setfill('0') << (unsigned int)c; + } + + return ss.str(); + } + + static void TrimAll(std::string &str); + + template + static std::string stringJoin(char delim, const T &...args) { + std::string res; + + size_t size = 0; + for (const auto &x : {args...}) { + size += x.size(); + } + + res.reserve(size + sizeof...(args)); + + for (const auto &x : {args...}) { + res += x; + res += delim; + } + res.pop_back(); + + return res; + } + + static std::string UriEncode(const std::bitset<256> &encoder, + const std::string &str); + static bool HasHeader(const std::map &header, + const std::string &key); + static bool HeaderEq(const std::map &header, + const std::string &key, const std::string &val); + static bool HeaderStartsWith(const std::map &header, + const std::string &key, const std::string &val); + + static std::string timestampToIso8016(const std::string &t); + static std::string timestampToIso8016(const time_t &t); + static std::string timestampToIso8016(const tm *t); + static std::string timestampToIso8016(const std::filesystem::file_time_type &t); + private: + std::bitset<256> mEncoder; + std::bitset<256> mObjectEncoder; + unsigned char mDecoder[256]{}; +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3UTILS_HH diff --git a/src/XrdS3/XrdS3Xml.cc b/src/XrdS3/XrdS3Xml.cc new file mode 100644 index 00000000000..fa9e3e88e7c --- /dev/null +++ b/src/XrdS3/XrdS3Xml.cc @@ -0,0 +1,27 @@ +// +// Created by segransm on 11/13/23. +// + +#include "XrdS3Xml.hh" + +namespace S3 { + +S3Xml::S3Xml() : tinyxml2::XMLPrinter(nullptr, true) { + PushDeclaration(R"(xml version="1.0" encoding="UTF-8")"); +} + +void S3Xml::OpenElement(const char *elem) { + tinyxml2::XMLPrinter::OpenElement(elem, true); +} + +void S3Xml::CloseElement() { tinyxml2::XMLPrinter::CloseElement(true); } + +void S3Xml::AddElement(const char *key, const std::string &value) { + OpenElement(key); + if (!value.empty()) { + PushText(value.c_str()); + } + CloseElement(); +} + +} // namespace S3 diff --git a/src/XrdS3/XrdS3Xml.hh b/src/XrdS3/XrdS3Xml.hh new file mode 100644 index 00000000000..0364d431547 --- /dev/null +++ b/src/XrdS3/XrdS3Xml.hh @@ -0,0 +1,34 @@ +// +// Created by segransm on 11/13/23. +// + +#ifndef XROOTD_XRDS3XML_HH +#define XROOTD_XRDS3XML_HH + +#include + +#include + +namespace S3 { + +class S3Xml : public tinyxml2::XMLPrinter { + public: + S3Xml(); + + void OpenElement(const char *elem); + + void CloseElement(); + + void AddElement(const char *key, const std::string &value); + + template + void AddElement(const char *key, const T &value) { + OpenElement(key); + PushText(value); + CloseElement(); + } +}; + +} // namespace S3 + +#endif // XROOTD_XRDS3XML_HH diff --git a/src/XrdS3/export-lib-symbols b/src/XrdS3/export-lib-symbols new file mode 100644 index 00000000000..4e6f0d221d8 --- /dev/null +++ b/src/XrdS3/export-lib-symbols @@ -0,0 +1,7 @@ +{ +global: + XrdHttpGetExtHandler*; + +local: + *; +}; diff --git a/src/XrdVersionPlugin.hh b/src/XrdVersionPlugin.hh index 121949e6826..13f8a3fbaf6 100644 --- a/src/XrdVersionPlugin.hh +++ b/src/XrdVersionPlugin.hh @@ -183,6 +183,7 @@ "libXrdCryptossl.so", \ "libXrdHttp.so", \ "libXrdHttpTPC.so", \ + "libXrdHttpS3.so", \ "libXrdMacaroons.so", \ "libXrdN2No2p.so", \ "libXrdOssSIgpfsT.so", \