-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
443 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } }, | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/": "./" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
Oops, something went wrong.