Skip to content

Commit

Permalink
Add AWS-v4 docs
Browse files Browse the repository at this point in the history
  • Loading branch information
max-lt committed Aug 29, 2023
1 parent e9820d7 commit e86fb7f
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 14 deletions.
143 changes: 143 additions & 0 deletions docs/examples/s3-proxy-aws-v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Create a proxy to S3 using AWS v4 signature type

The following example shows how to read from S3 using AWS v4 signature type without using any module.

See [S3 documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html) for more information.

## Prerequisites

Ensure that you have the following environment variables set:

- `S3_BUCKET_NAME`: The name of the S3 bucket.
- `S3_ACCESS_KEY`: The access key of the S3-compatible storage.
- `S3_SECRET_KEY`: The secret key of the S3-compatible storage.
- `S3_REGION`: The region of the S3-compatible storage.

## Reading from S3

To read from S3, we need to sign the request using HMAC-SHA1, which is supported by the Web Crypto API.
We define the `HMAC` function to sign the request, and the `getObject` function to prepare the request and send it to S3.

```typescript
const s3 = {
endpoint: env.S3_ENDPOINT ?? 'https://example.s3.fr-par.scw.cloud',
region: env.S3_REGION ?? 'fr-par',
accessKey: env.S3_ACCESS_KEY ?? null,
secretKey: env.S3_SECRET_KEY ?? null
};

// Ensure that keys are set
if (!s3.accessKey || !s3.secretKey) {
throw new Error('Missing S3 configuration');
}

async function sha256(data: string): Promise<string> {
const dataUint8 = toUint8Array(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataUint8);
return toHexString(new Uint8Array(hashBuffer));
}

// Convert a string to Uint8Array
function toUint8Array(str: string) {
return Uint8Array.from(str, (c) => c.charCodeAt(0));
}

// Convert an Uint8Array to a hex string
function toHexString(arr: Uint8Array) {
return Array.from(arr)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

async function HMAC(key: Uint8Array, message: Uint8Array) {
const algorithm = { name: 'HMAC', hash: 'SHA-256' };

const cryptoKey = await crypto.subtle.importKey('raw', key, algorithm, true, ['sign']);

const signature = await crypto.subtle.sign(algorithm, cryptoKey, message);

return new Uint8Array(signature);
}

async function getS3Object(key: string) {
const url = new URL(`${s3.endpoint}/${key}`);
const date = new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); // format: YYYYMMDDTHHMMSSZ
const shortDate = date.slice(0, 8); // format: YYYYMMDD
const method = 'GET';
const canonicalURI = url.pathname;
const canonicalQueryString = '';
const hashedPayload = await sha256(''); // For GET requests, the payload is empty
const canonicalHeaders = `host:${url.host}\nx-amz-content-sha256:${hashedPayload}\nx-amz-date:${date}\n`;
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';

const canonicalRequest = [
method,
canonicalURI,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedPayload
].join('\n');

const canonicalRequestHash = await sha256(canonicalRequest);

const stringToSign = [
'AWS4-HMAC-SHA256',
date,
`${shortDate}/${s3.region}/s3/aws4_request`,
canonicalRequestHash
].join('\n');

const dateKey = await HMAC(toUint8Array('AWS4' + s3.secretKey), toUint8Array(shortDate));
const regionKey = await HMAC(dateKey, toUint8Array(s3.region));
const serviceKey = await HMAC(regionKey, toUint8Array('s3'));
const signingKey = await HMAC(serviceKey, toUint8Array('aws4_request'));
const signature = toHexString(await HMAC(signingKey, toUint8Array(stringToSign)));

const headers = new Headers();
headers.set('x-amz-date', date);
headers.set('x-amz-content-sha256', hashedPayload);
headers.set(
'Authorization',
`AWS4-HMAC-SHA256 ` + //
`Credential=${s3.accessKey}/${shortDate}/${s3.region}/s3/aws4_request, ` +
`SignedHeaders=${signedHeaders}, ` +
`Signature=${signature}`
);

// Send the request
return fetch(url.toString(), { headers, method }).then((res) => {
switch (res.status) {
case 200:
return res;
case 404:
return new Response('Not found', { status: 404 });
default:
throw new Error(`Unexpected status code: ${res.status}`);
}
});
}

addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
handleRequest(event.request)
// If the request handler throws an error, return a 500 response.
.catch(() => new Response('Internal Server Error', { status: 500 }))
);
});

async function handleRequest(request: Request): Promise<Response> {
const { pathname } = new URL(request.url);

// If the request is for the root, return the index.html file, otherwise
// return the file matching the path (note that we strip the leading slash)
const key = pathname === '/' ? 'index.html' : pathname.slice(1);
if (request.method === 'GET') {
return getS3Object(key);
}

return new Response('Not found', { status: 404 });
}
```

Your worker is now ready to be deployed and serve files from S3.
4 changes: 4 additions & 0 deletions docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
{
"name": "S3 Proxy",
"path": "s3-proxy"
},
{
"name": "S3 Proxy with AWSv4",
"path": "s3-proxy-aws-v4"
}
]
},
Expand Down
24 changes: 12 additions & 12 deletions src/app/pages/main/main.page.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<div class="min-h-screen" style="font-family: Roboto, sans-serif">
<div class="min-h-screen">
<!-- CODE -->
<div class="container flex-col my-24 max-w-7xl">
<div class="flex justify-between gap-8 flex-col lg:flex-row px-8 w-full">
<div class="container flex-col mt-24 max-w-7xl">
<div class="flex justify-between lg:gap-8 flex-col lg:flex-row px-8 w-full">
<div class="flex-1 max-w-xl mx-auto">
<div>
<h1 class="text-4xl text-slate-800 mb-4 font-bold">
Deploy&nbsp;<span class="text-blue-500">instantly</span>
<h1 class="title text-5xl text-slate-800 mb-4 font-bold">
Deploy&nbsp;<span class="text-gradient">instantly</span>
</h1>

<div class="my-12 flex">
<ul class="list-checkmark">
<ul class="list-checkmark mx-auto lg:mx-0">
<li>Schedule jobs and build powerful flows</li>
<li>Built-in monitoring</li>
<li>Scale with ease</li>
Expand All @@ -34,8 +34,8 @@ <h1 class="text-4xl text-slate-800 mb-4 font-bold">
</div>
</div>

<div class="mx-auto">
<a [href]="loginUrl" target="_blank" class="btn btn-blue text-xl px-6 py-4 rounded mt-16"> Get Started </a>
<div class="mx-auto lg:mb-8">
<a [href]="loginUrl" target="_blank" class="btn btn-blue text-xl px-6 py-4 rounded my-24"> Get Started </a>
</div>
</div>

Expand Down Expand Up @@ -91,7 +91,7 @@ <h2 class="text-2xl text-slate-800 mb-4 font-bold">Rusty 🦀</h2>
</div>

<!-- NEWSLETTER -->
<div class="container max-w-7xl my-24">
<div class="container max-w-7xl mb-24">
<div class="flex flex-col md:flex-row justify-between w-full py-12 md:py-24 border-y px-4">
<h4 class="flex items-center py-2">Sign up for our newsletter</h4>

Expand All @@ -101,14 +101,14 @@ <h4 class="flex items-center py-2">Sign up for our newsletter</h4>

<ng-template #newsLetterForm>
<form
class="flex gap-4"
class="flex gap-4 justify-between"
action="https://newsletter.workers.rocks"
method="POST"
target="hiddenFrame"
[formGroup]="newsletterForm"
(ngSubmit)="subscribe()"
>
<div class="relative border rounded">
<div class="relative border rounded flex-1 max-w-[24rem] lg:max-w-[32rem]">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
Expand All @@ -124,7 +124,7 @@ <h4 class="flex items-center py-2">Sign up for our newsletter</h4>
</svg>

<input
class="pr-4 pl-10 h-full lg:min-w-[24rem]"
class="pr-4 pl-10 h-full w-full lg:min-w-[24rem]"
type="email"
autocomplete="email"
placeholder="Email"
Expand Down
18 changes: 16 additions & 2 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@tailwind components;
@tailwind utilities;

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@800&family=Roboto:wght@100;300;400&display=swap');

:root {
--color-primary: #1f2937;
Expand Down Expand Up @@ -165,7 +165,8 @@
}

.btn-blue {
@apply bg-blue-500 text-white;
@apply text-white;
background-color: #057feb;
}

ul.list-checkmark li {
Expand All @@ -180,4 +181,17 @@
background-position: center;
background-size: 50%;
}

.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.title {
font-family: Inter, sans-serif;
}

.text-gradient {
@apply bg-gradient-to-r bg-clip-text text-transparent from-cyan-500 to-[#057feb];
}
}

0 comments on commit e86fb7f

Please sign in to comment.