Skip to content

Commit

Permalink
Merge pull request #2491 from helloimalastair/mikkel/R2-2317
Browse files Browse the repository at this point in the history
R2-2317 add support for SSE-C via `workerd` bindings
  • Loading branch information
danlapid authored Nov 27, 2024
2 parents d9695ca + a177c0a commit 735aeb2
Show file tree
Hide file tree
Showing 8 changed files with 347 additions and 82 deletions.
6 changes: 6 additions & 0 deletions src/workerd/api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ wd_test(
data = ["queue-test.js"],
)

wd_test(
src = "r2-test.wd-test",
args = ["--experimental"],
data = ["r2-test.js"],
)

wd_test(
src = "rtti-test.wd-test",
args = ["--experimental"],
Expand Down
18 changes: 18 additions & 0 deletions src/workerd/api/r2-api.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ struct R2Conditional {
# timestamp.
}

struct R2SSECOptions {
key @0 :Text;
}

struct R2Checksums {
# The JSON name of these fields must comform to the representation of the ChecksumAlgorithm in
# the R2 gateway worker.
Expand Down Expand Up @@ -93,6 +97,7 @@ struct R2GetRequest {
range @1 :R2Range;
rangeHeader @3 :Text;
onlyIf @2 :R2Conditional;
ssec @4 :R2SSECOptions;
}

struct R2PutRequest {
Expand All @@ -106,19 +111,22 @@ struct R2PutRequest {
sha384 @7 :Data $Json.hex;
sha512 @8 :Data $Json.hex;
storageClass @9 :Text;
ssec @10 :R2SSECOptions;
}

struct R2CreateMultipartUploadRequest {
object @0 :Text;
customFields @1 :List(Record);
httpFields @2 :R2HttpFields;
storageClass @3 :Text;
ssec @4 :R2SSECOptions;
}

struct R2UploadPartRequest {
object @0 :Text;
uploadId @1 :Text;
partNumber @2 :UInt32;
ssec @3 :R2SSECOptions;
}

struct R2CompleteMultipartUploadRequest {
Expand Down Expand Up @@ -187,6 +195,11 @@ struct R2ErrorResponse {
message @2 :Text;
}

struct R2SSECResponse {
algorithm @0 :Text;
keyMd5 @1 :Text;
}

struct R2HeadResponse {
name @0 :Text;
# The name of the object.
Expand Down Expand Up @@ -220,6 +233,9 @@ struct R2HeadResponse {
storageClass @9 :Text;
# The storage class of the object. Standard or Infrequent Access.
# Provided on object creation to specify which storage tier R2 should use for this object.

ssec @10 :R2SSECResponse;
# The algorithm/key hash used for encryption(if the user used SSE-C)
}

using R2GetResponse = R2HeadResponse;
Expand All @@ -230,6 +246,8 @@ struct R2CreateMultipartUploadResponse {
uploadId @0 :Text;
# The unique identifier of this object, required for subsequent operations on
# this multipart upload.
ssec @1 :R2SSECResponse;
# The algorithm/key hash used for encryption(if the user used SSE-C)
}

struct R2UploadPartResponse {
Expand Down
44 changes: 43 additions & 1 deletion src/workerd/api/r2-bucket.c++
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

#include <array>
#include <cmath>
#include <regex>

namespace workerd::api::public_beta {
static bool isWholeNumber(double x) {
Expand Down Expand Up @@ -154,10 +155,17 @@ static jsg::Ref<T> parseObjectMetadata(R2HeadResponse::Reader responseReader,
}
}

jsg::Optional<kj::String> ssecKeyMd5;

if (responseReader.hasSsec()) {
auto ssecBuilder = responseReader.getSsec();
ssecKeyMd5 = kj::str(ssecBuilder.getKeyMd5());
}

return jsg::alloc<T>(kj::str(responseReader.getName()), kj::str(responseReader.getVersion()),
responseReader.getSize(), kj::str(responseReader.getEtag()), kj::mv(checksums), uploaded,
kj::mv(httpMetadata), kj::mv(customMetadata), range,
kj::str(responseReader.getStorageClass()), kj::fwd<Args>(args)...);
kj::str(responseReader.getStorageClass()), kj::mv(ssecKeyMd5), kj::fwd<Args>(args)...);
}

template <HeadResultT T, typename... Args>
Expand Down Expand Up @@ -253,6 +261,25 @@ void initOnlyIf(jsg::Lock& js, Builder& builder, Options& o) {
}
}

kj::Maybe<kj::String> buildSsecKey(
kj::Maybe<kj::OneOf<kj::Array<byte>, kj::String>> maybeRawSsecKey) {
KJ_IF_SOME(rawSsecKey, maybeRawSsecKey) {
KJ_SWITCH_ONEOF(rawSsecKey) {
KJ_CASE_ONEOF(keyString, kj::String) {
JSG_REQUIRE(std::regex_match(keyString.begin(), keyString.end(), std::regex("^[0-9a-f]+$")),
Error, "SSE-C Key has invalid format");
JSG_REQUIRE(keyString.size() == 64, Error, "SSE-C Key must be 32 bytes in length");
return kj::str(keyString);
}
KJ_CASE_ONEOF(keyBuff, kj::Array<byte>) {
JSG_REQUIRE(keyBuff.size() == 32, Error, "SSE-C Key must be 32 bytes in length");
return kj::encodeHex(keyBuff);
}
}
}
return kj::none;
}

template <typename Builder, typename Options>
void initGetOptions(jsg::Lock& js, Builder& builder, Options& o) {
initOnlyIf(js, builder, o);
Expand Down Expand Up @@ -296,6 +323,11 @@ void initGetOptions(jsg::Lock& js, Builder& builder, Options& o) {
}
}
}
kj::Maybe<kj::String> maybeSsecKey = buildSsecKey(kj::mv(o.ssecKey));
KJ_IF_SOME(ssecKey, maybeSsecKey) {
auto ssecBuilder = builder.initSsec();
ssecBuilder.setKey(ssecKey);
}
}

static bool isQuotedEtag(kj::StringPtr etag) {
Expand Down Expand Up @@ -559,6 +591,11 @@ jsg::Promise<kj::Maybe<jsg::Ref<R2Bucket::HeadResult>>> R2Bucket::put(jsg::Lock&
KJ_IF_SOME(s, o.storageClass) {
putBuilder.setStorageClass(s);
}
kj::Maybe<kj::String> maybeSsecKey = buildSsecKey(kj::mv(o.ssecKey));
KJ_IF_SOME(ssecKey, maybeSsecKey) {
auto ssecBuilder = putBuilder.initSsec();
ssecBuilder.setKey(ssecKey);
}
}

auto requestJson = json.encode(requestBuilder);
Expand Down Expand Up @@ -651,6 +688,11 @@ jsg::Promise<jsg::Ref<R2MultipartUpload>> R2Bucket::createMultipartUpload(jsg::L
KJ_IF_SOME(s, o.storageClass) {
createMultipartUploadBuilder.setStorageClass(s);
}
kj::Maybe<kj::String> maybeSsecKey = buildSsecKey(kj::mv(o.ssecKey));
KJ_IF_SOME(ssecKey, maybeSsecKey) {
auto ssecBuilder = createMultipartUploadBuilder.initSsec();
ssecBuilder.setKey(ssecKey);
}
}

auto requestJson = json.encode(requestBuilder);
Expand Down
37 changes: 29 additions & 8 deletions src/workerd/api/r2-bucket.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ class R2Bucket: public jsg::Object {
struct GetOptions {
jsg::Optional<kj::OneOf<Conditional, jsg::Ref<Headers>>> onlyIf;
jsg::Optional<kj::OneOf<Range, jsg::Ref<Headers>>> range;
jsg::Optional<kj::OneOf<kj::Array<byte>, kj::String>> ssecKey;

JSG_STRUCT(onlyIf, range);
JSG_STRUCT(onlyIf, range, ssecKey);
JSG_STRUCT_TS_OVERRIDE(R2GetOptions);
};

Expand Down Expand Up @@ -189,18 +190,28 @@ class R2Bucket: public jsg::Object {
jsg::Optional<kj::OneOf<kj::Array<kj::byte>, jsg::NonCoercible<kj::String>>> sha384;
jsg::Optional<kj::OneOf<kj::Array<kj::byte>, jsg::NonCoercible<kj::String>>> sha512;
jsg::Optional<kj::String> storageClass;

JSG_STRUCT(
onlyIf, httpMetadata, customMetadata, md5, sha1, sha256, sha384, sha512, storageClass);
jsg::Optional<kj::OneOf<kj::Array<byte>, kj::String>> ssecKey;

JSG_STRUCT(onlyIf,
httpMetadata,
customMetadata,
md5,
sha1,
sha256,
sha384,
sha512,
storageClass,
ssecKey);
JSG_STRUCT_TS_OVERRIDE(R2PutOptions);
};

struct MultipartOptions {
jsg::Optional<kj::OneOf<HttpMetadata, jsg::Ref<Headers>>> httpMetadata;
jsg::Optional<jsg::Dict<kj::String>> customMetadata;
jsg::Optional<kj::String> storageClass;
jsg::Optional<kj::OneOf<kj::Array<byte>, kj::String>> ssecKey;

JSG_STRUCT(httpMetadata, customMetadata, storageClass);
JSG_STRUCT(httpMetadata, customMetadata, storageClass, ssecKey);
JSG_STRUCT_TS_OVERRIDE(R2MultipartOptions);
};

Expand All @@ -215,7 +226,8 @@ class R2Bucket: public jsg::Object {
jsg::Optional<HttpMetadata> httpMetadata,
jsg::Optional<jsg::Dict<kj::String>> customMetadata,
jsg::Optional<Range> range,
kj::String storageClass)
kj::String storageClass,
jsg::Optional<kj::String> ssecKeyMd5)
: name(kj::mv(name)),
version(kj::mv(version)),
size(size),
Expand All @@ -225,7 +237,8 @@ class R2Bucket: public jsg::Object {
httpMetadata(kj::mv(httpMetadata)),
customMetadata(kj::mv(customMetadata)),
range(kj::mv(range)),
storageClass(kj::mv(storageClass)) {}
storageClass(kj::mv(storageClass)),
ssecKeyMd5(kj::mv(ssecKeyMd5)) {}

kj::String getName() const {
return kj::str(name);
Expand All @@ -251,6 +264,9 @@ class R2Bucket: public jsg::Object {
kj::StringPtr getStorageClass() const {
return storageClass;
}
jsg::Optional<kj::StringPtr> getSSECKeyMd5() const {
return ssecKeyMd5;
}

jsg::Optional<HttpMetadata> getHttpMetadata() const {
return httpMetadata.map([](const HttpMetadata& m) { return m.clone(); });
Expand Down Expand Up @@ -285,6 +301,7 @@ class R2Bucket: public jsg::Object {
JSG_LAZY_READONLY_INSTANCE_PROPERTY(customMetadata, getCustomMetadata);
JSG_LAZY_READONLY_INSTANCE_PROPERTY(range, getRange);
JSG_LAZY_READONLY_INSTANCE_PROPERTY(storageClass, getStorageClass);
JSG_LAZY_READONLY_INSTANCE_PROPERTY(ssecKeyMd5, getSSECKeyMd5);
JSG_METHOD(writeHttpMetadata);
JSG_TS_OVERRIDE(R2Object);
}
Expand All @@ -296,6 +313,7 @@ class R2Bucket: public jsg::Object {
tracker.trackField("checksums", checksums);
tracker.trackField("httpMetadata", httpMetadata);
tracker.trackField("customMetadata", customMetadata);
tracker.trackField("ssecKeyMd5", ssecKeyMd5);
}

protected:
Expand All @@ -310,6 +328,7 @@ class R2Bucket: public jsg::Object {

jsg::Optional<Range> range;
kj::String storageClass;
jsg::Optional<kj::String> ssecKeyMd5;
friend class R2Bucket;
};

Expand All @@ -325,6 +344,7 @@ class R2Bucket: public jsg::Object {
jsg::Optional<jsg::Dict<kj::String>> customMetadata,
jsg::Optional<Range> range,
kj::String storageClass,
jsg::Optional<kj::String> ssecKeyMd5,
jsg::Ref<ReadableStream> body)
: HeadResult(kj::mv(name),
kj::mv(version),
Expand All @@ -335,7 +355,8 @@ class R2Bucket: public jsg::Object {
kj::mv(KJ_ASSERT_NONNULL(httpMetadata)),
kj::mv(KJ_ASSERT_NONNULL(customMetadata)),
range,
kj::mv(storageClass)),
kj::mv(storageClass),
kj::mv(ssecKeyMd5)),
body(kj::mv(body)) {}

jsg::Ref<ReadableStream> getBody() {
Expand Down
24 changes: 22 additions & 2 deletions src/workerd/api/r2-multipart.c++
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@

#include "r2-bucket.h"
#include "r2-rpc.h"
#include "workerd/jsg/jsg.h"

#include <workerd/api/r2-api.capnp.h>
#include <workerd/util/http-util.h>

#include <capnp/compat/json.h>
#include <capnp/message.h>
#include <kj/compat/http.h>
#include <kj/encoding.h>

#include <array>
#include <cmath>
#include <regex>

namespace workerd::api::public_beta {

jsg::Promise<R2MultipartUpload::UploadedPart> R2MultipartUpload::uploadPart(jsg::Lock& js,
int partNumber,
R2PutValue value,
jsg::Optional<UploadPartOptions> options,
const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType) {
return js.evalNow([&] {
JSG_REQUIRE(partNumber >= 1 && partNumber <= 10000, TypeError,
Expand All @@ -44,6 +46,24 @@ jsg::Promise<R2MultipartUpload::UploadedPart> R2MultipartUpload::uploadPart(jsg:
uploadPartBuilder.setUploadId(uploadId);
uploadPartBuilder.setPartNumber(partNumber);
uploadPartBuilder.setObject(key);
KJ_IF_SOME(options, options) {
KJ_IF_SOME(ssecKey, options.ssecKey) {
auto ssecBuilder = uploadPartBuilder.initSsec();
KJ_SWITCH_ONEOF(ssecKey) {
KJ_CASE_ONEOF(keyString, kj::String) {
JSG_REQUIRE(
std::regex_match(keyString.begin(), keyString.end(), std::regex("^[0-9a-f]+$")),
Error, "SSE-C Key has invalid format");
JSG_REQUIRE(keyString.size() == 64, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::str(keyString));
}
KJ_CASE_ONEOF(keyBuff, kj::Array<byte>) {
JSG_REQUIRE(keyBuff.size() == 32, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::encodeHex(keyBuff));
}
}
}
}

auto requestJson = json.encode(requestBuilder);
auto bucket = this->bucket->adminBucket.map([](auto&& s) { return kj::str(s); });
Expand Down
7 changes: 7 additions & 0 deletions src/workerd/api/r2-multipart.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ class R2MultipartUpload: public jsg::Object {
JSG_STRUCT(partNumber, etag);
JSG_STRUCT_TS_OVERRIDE(R2UploadedPart);
};
struct UploadPartOptions {
jsg::Optional<kj::OneOf<kj::Array<byte>, kj::String>> ssecKey;

JSG_STRUCT(ssecKey);
JSG_STRUCT_TS_OVERRIDE(R2UploadPartOptions);
};

R2MultipartUpload(kj::String key, kj::String uploadId, jsg::Ref<R2Bucket> bucket)
: key(kj::mv(key)),
Expand All @@ -35,6 +41,7 @@ class R2MultipartUpload: public jsg::Object {
jsg::Promise<UploadedPart> uploadPart(jsg::Lock& js,
int partNumber,
R2PutValue value,
jsg::Optional<UploadPartOptions> options,
const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType);
jsg::Promise<void> abort(jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType);
jsg::Promise<jsg::Ref<R2Bucket::HeadResult>> complete(jsg::Lock& js,
Expand Down
Loading

0 comments on commit 735aeb2

Please sign in to comment.