Skip to content

Commit

Permalink
code drop
Browse files Browse the repository at this point in the history
  • Loading branch information
cnrdh committed Nov 3, 2022
1 parent 62998f3 commit 241b4ac
Show file tree
Hide file tree
Showing 12 changed files with 443 additions and 0 deletions.
62 changes: 62 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { get, options } from "./storage_handlers.ts";
import { documentation } from "./documentation.ts";
import { CreateProxyOptions, StorageHandler } from "./types.ts";

export const safeHandlers = new Map<
string,
StorageHandler
>([
["GET", get],
["HEAD", get],
["OPTIONS", options],
]);

export const safeMethods = new Set(["GET", "HEAD", "OPTIONS"]);
export const xmlheaders = { "content-type": "application/xml; charset=utf-8" };

export const corsheaders = new Headers([
["access-control-allow-origin", "*"],
[
"access-control-allow-methods",
"DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT",
],
]);
export const defaultSuffix = "core.windows.net";

export const buildConnectionString = (
{ account = "", key = "", suffix = defaultSuffix } = {},
): string =>
`AccountName=${account};AccountKey=${key};EndpointSuffix=${suffix};DefaultEndpointsProtocol=https;`;

export const containerPattern = new URLPattern({
pathname: "/:container([a-z][\\w-]+)/:path*",
});

const defaults = (env: Deno.Env): CreateProxyOptions => {
const account = env.get("azure_account") ?? "";
const key = env.get("azure_key") ?? "";

const _cont = env.get("azure_containers");
const containers = _cont && _cont.length > 4
? new Set<string>(JSON.parse(_cont))
: new Set<string>();

const suffix = env.get("azure_suffix") ?? defaultSuffix;

const handlers = safeHandlers;

const fallback = documentation({ account, containers, suffix, handlers });

return {
account,
key,
containers,
suffix,
handlers,
fallback,
};
};
export function config(options = {}): CreateProxyOptions {
const defaultOptions = defaults(Deno.env);
return { ...defaultOptions, ...options };
}
64 changes: 64 additions & 0 deletions create_proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { buildConnectionString, config, containerPattern } from "./config.ts";
import {
notAllowedFactory,
notConfigured,
unauthorizedContainer,
} from "./errors.ts";

import { AzureStorage } from "azure_storage_client/storage.ts";
import { CreateProxyOptions } from "./types.ts";

export const createProxy =
(options?: CreateProxyOptions) =>
async (request: Request): Promise<Response> => {
const {
key,
account,
containers,
auth,
handlers,
fallback,
suffix,
} = config(
options,
);

if (!account) {
return notConfigured();
}

const method = request.method.toUpperCase();
const methods = new Set(handlers.keys());
if (!methods.has(method)) {
return notAllowedFactory(methods)(request);
}

const unauthorized = auth ? await auth(request) : undefined;
if (unauthorized && unauthorized?.status >= 400) {
return unauthorized;
}

const match = containerPattern.exec(request.url);

if (match?.pathname) {
const { container, path } = match.pathname.groups;

if (containers && !containers.has(container)) {
return unauthorizedContainer();
}

const handler = handlers.get(method);
if (!handler) {
return notAllowedFactory(methods)(request);
}
const storage = new AzureStorage(
buildConnectionString({ account, key, suffix }),
);
return handler({ request, storage, container, path });
}
// No matching container, provide fallback or 405
if (fallback) {
return fallback(request);
}
return notAllowedFactory(methods)(request);
};
6 changes: 6 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"importMap": "imports.json",
"tasks": {
"dev": "deno run --watch --unstable --allow-env --allow-net dev.ts"
}
}
4 changes: 4 additions & 0 deletions dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createAzureBlobProxy } from "./mod.ts";
if (import.meta.main) {
Deno.serve(createAzureBlobProxy());
}
30 changes: 30 additions & 0 deletions documentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CreateProxyOptions } from "./types.ts";
export const documentation =
({ account, containers, suffix, handlers }: CreateProxyOptions) =>
(_request: Request): Response => {
const methods = new Set(handlers.keys());

const methodsList = [...methods].map((m) => `\n<li>${m}</li>`).join("");

const containerList = [...containers].map((c) =>
`\n<li><a href="/${c}?list">${c}</a></li>`
).join("");

return new Response(
`<!DOCTYPE html><html lang="en">
<head><meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body><h1>azure_blob_proxy</h1>
<dl>
<dt>Account</dt>
<dd>${account}</dd>
<dt>Suffix</dt>
<dd>${suffix}</dd>
<dt>Methods</dt>
<dd><ul>${methodsList}</ul></dd>
<dt>Containers</dt>
<dd><dd><ul>${containerList}</ul></dd>
</dl>
</body></html>`,
{ headers: { "content-type": "text/html; charset=utf-8" } },
);
};
17 changes: 17 additions & 0 deletions errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const notAllowedFactory = (methods: Set<string>) => (request: Request) =>
new Response(
`405 ${request.method} not allowed\nOnly: [${[...methods].join(",")}]`,
{ status: 405 },
);

export const notConfigured = () =>
new Response(
"503 Service Unavailable\nAzure account or key is missing in config\n",
{ status: 503 },
);

export const notImplemented = (request: Request) =>
new Response(`501 ${request.method} not implemented\n`, { status: 501 });

export const unauthorizedContainer = () =>
new Response("Unauthorized container\n", { status: 403 });
7 changes: 7 additions & 0 deletions imports.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"imports": {
"std/": "https://deno.land/[email protected]/",
"azure_storage_client/": "https://deno.land/x/[email protected]/",
"azure_blob_proxy/": "./"
}
}
5 changes: 5 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./config.ts";
export * from "./create_proxy.ts";
export * from "./errors.ts";
export * from "./storage_handlers.ts";
export * from "./types.ts";
117 changes: 117 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# azure_blob_proxy

Azure blob storage proxy for [Deno](https://deno.land) (Deploy)

## Use

Start a basic, safe proxy:

```js
import { createAzureBlobProxy } from "https://deno.land/x/azure_blob_proxy/mod.ts";
Deno.serve(createAzureBlobProxy());
```

Above code will require setting env variables `azure_account` and `azure_key`.

## Dev

```sh
deno task dev
```

Open http://localhost:9000/ and you should see: a `azure_blob_proxy` header,
followed by account name, suffix, methods, and containers configured.

## DELETE and PUT

By default, only safe HTTP methods (`GET`, `HEAD`, and `OPTIONS`) are
operational.

Add support for `DELETE` and `PUT`:

```js
import { config, createProxy, del, put } from "azure_blob_proxy/mod.ts";

const { handlers } = config(); // Get default handlers map
handlers.set("PUT", put); // Add (generic) PUT handler
handlers.set("DELETE", del); // Add (generic) DELETE handler
Deno.serve(createProxy(config({ handlers }))); // Inject updated handlers
```

## Auth

Bring your own authorization handler (that must return `401`/`403` or
`undefined`):

```js
import { users, authHandler } from "";

Deno.serve(createAzureBlobProxy({
auth: async (request) => await authHandler(request, users)
});
```
## Adding a POST handler
Using `POST` to create new blobs requires writing a POST storage handler.
The POST handler must build a path from the request Essentially could be as easy
as providing the default [PUT](./storage_handlers.ts#put) handler with a `path`:
and should return a `location` response header with the fresh new resource.
```ts
import { put } from "azure_blob_proxy/storage_handlers.ts";

const { handlers } = config();
handlers.set(
"POST",
({ request, storage, container, path }) =>
put({ request, storage, container, path: `text/${crypto.randomUUID()}` }),
); // Add POST handler
Deno.serve(createAzureBlobProxy(config({ handlers })));
```
See [text.ts](./text.ts) for a fully working production ready example.
## Config
Use `env` variables for basic configuration, extends with a
`AzureBlobProxyOptions` config object.
### env variables
```sh
azure_account="" # Account name
azure_key="" # Account key (or shared access signature)
azure_containers="" # Optional. Comma-separated list of allowed containers
```
### config object
```js
const options = {};
createAzureBlobProxy(config(options));
```
Available options:
```ts
export type AzureBlobProxyOptions = {
account: string; // Azure storage account name
key?: string; // Azure account key (or SAS)
containers: Set<string>; // Allowed containers
suffix: string; // Azure URL suffix
handlers: Map<string, StorageHandler>; // HTTP method handlers
fallback?: Handler; // Fallback request handler
auth?: Handler; // Brin your own authorization request handler
};
```
## Dependencies
See [imports.json][imports], but shootout to
[itte1/azure_storage_client][azure_storage_client]
[azure_storage_client]: https://github.com/itte1/azure_storage_client
[imports]: ./imports.json
60 changes: 60 additions & 0 deletions storage_handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { StorageHandler, StorageHandlerParams } from "./types.ts";
import { corsheaders, xmlheaders } from "./config.ts";
import {
// formatMediaType,
// getCharset,
parseMediaType,
} from "std/media_types/mod.ts";

export const del: StorageHandler = (
{ storage, container, path }: StorageHandlerParams,
): Promise<Response> => storage.container(container).delete(path);

// $ curl http://127.0.0.1:8000/container/dir/file.json --netrc-file .netrc
// $ curl http://127.0.0.1:8000/container/dir/file.json -u "$user:$secret"
export const get: StorageHandler = async (
{ request, storage, container, path }: StorageHandlerParams,
): Promise<Response> => {
const { searchParams } = new URL(request.url);
const list = searchParams.has("list") || searchParams.get("comp") === "list";

const response = list
? await storage.container(container).list(path)
: await storage.container(container).get(path);

const notFound = response.headers.get("x-ms-error-code") === "BlobNotFound"; // beware: unknown "directories" are not marked
if (notFound) {
return new Response(response.body, { status: 404, headers: xmlheaders });
}
if (list) {
return new Response(response.body, { headers: xmlheaders });
}
return response;
// @todo For text, json, xml: Append utf-8 as charset
// Below code works, but sometimes crashes on formayMediaType
// const [_mt, _charset] = parseMediaType(response.headers.get("content-type"));
// const charset = _charset?.length > 0 ? _charset : getCharset(_mt);
// const mediaType = formatMediaType(_mt, { charset });
// const headers = new Headers(response.headers);
// headers.set("content-type", mediaType);
// return new Response(response.body, { headers });
};
export const options: StorageHandler = (
//_params: StorageHandlerParams,
): Response =>
new Response(undefined, {
status: 204,
headers: corsheaders,
});

// $ curl -vXPUT http://127.0.0.1:8000/container/dir/file.json --netrc-file .netrc [email protected] -H "content-type: application/json"
export const put: StorageHandler = async (
{ request, storage, container, path }: StorageHandlerParams,
): Promise<Response> => {
const blob = await request.blob();
if (blob.size === 0) {
return new Response("Cannot PUT 0 bytes\n", { status: 400 });
}
const [mediaType] = parseMediaType(blob.type);
return await storage.container(container).put(path, blob, mediaType);
};
Loading

0 comments on commit 241b4ac

Please sign in to comment.