INTEGRITY Документация

Использование R2 в Workers

1. Создать новое приложение с помощью C3

C3 (create-cloudflare-cli) представляет собой инструмент командной строки, который помогает как можно быстрее настроить и развернуть приложения Workers & Pages в Cloudflare.

Чтобы начать, откройте окно терминала и выполните:

npm create cloudflare@latest -- r2-worker

Для настройки выберите следующие параметры:

Затем перейдите в только что созданный каталог:

cd r2-worker

2. Создать бакет

Создайте бакет, выполнив команду:

npx wrangler r2 bucket create <YOUR_BUCKET_NAME>

Чтобы проверить, что бакет был создан, выполните:

npx wrangler r2 bucket list

После выполнения list команду, вы увидите все имена бакетов, включая тот, который только что создали.

3. Привязать бакет к Worker

Вам нужно будет привязать бакет к Worker.

Чтобы привязать бакет R2 к своему Worker, добавьте следующее в файл Wrangler. Обновите binding свойство в допустимый идентификатор переменной JavaScript и bucket_name к <YOUR_BUCKET_NAME> , который вы использовали для создания бакета в шаг 2:

{
	"r2_buckets": [
		{
			"binding": "MY_BUCKET", // <~ valid JavaScript variable name
			"bucket_name": "<YOUR_BUCKET_NAME>"
		}
	]
}
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "<YOUR_BUCKET_NAME>"

Более подробные сведения о настройке Worker (например, если вы используете юрисдикции), см. Документация по конфигурации Wrangler.

4. Получить доступ к бакету R2 из Worker

Теперь в коде вашего Worker бакет доступен через MY_BUCKET переменную, после чего вы можете начать работу с ней.

Бакет R2 умеет читать, получать список, записывать и удалять объекты (READ, LIST, WRITE, DELETE). Пример всех этих операций с использованием синтаксиса Module Worker приведен ниже. Добавьте следующий фрагмент в файл проекта index.js файле:

import { WorkerEntrypoint } from "cloudflare:workers";

export default class extends WorkerEntrypoint<Env> {
  async fetch(request: Request) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);

    switch (request.method) {
      case "PUT": {
        await this.env.MY_BUCKET.put(key, request.body, {
          onlyIf: request.headers,
          httpMetadata: request.headers,
        });
        return new Response(`Put ${key} successfully!`);
      }
      case "GET": {
        const object = await this.env.MY_BUCKET.get(key, {
          onlyIf: request.headers,
          range: request.headers,
        });

        if (object === null) {
          return new Response("Object Not Found", { status: 404 });
        }

        const headers = new Headers();
        object.writeHttpMetadata(headers);
        headers.set("etag", object.httpEtag);

        // When no body is present, preconditions have failed
        return new Response("body" in object ? object.body : undefined, {
          status: "body" in object ? 200 : 412,
          headers,
        });
      }
      case "DELETE": {
        await this.env.MY_BUCKET.delete(key);
        return new Response("Deleted!");
      }
      default:
        return new Response("Method Not Allowed", {
          status: 405,
          headers: {
            Allow: "PUT, GET, DELETE",
          },
        });
    }
  }
};
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);

    switch (request.method) {
      case "PUT": {
        await env.MY_BUCKET.put(key, request.body, {
          onlyIf: request.headers,
          httpMetadata: request.headers,
        });
        return new Response(`Put ${key} successfully!`);
      }
      case "GET": {
        const object = await env.MY_BUCKET.get(key, {
          onlyIf: request.headers,
          range: request.headers,
        });

        if (object === null) {
          return new Response("Object Not Found", { status: 404 });
        }

        const headers = new Headers();
        object.writeHttpMetadata(headers);
        headers.set("etag", object.httpEtag);

        // When no body is present, preconditions have failed
        return new Response("body" in object ? object.body : undefined, {
          status: "body" in object ? 200 : 412,
          headers,
        });
      }
      case "DELETE": {
        await env.MY_BUCKET.delete(key);
        return new Response("Deleted!");
      }
      default:
        return new Response("Method Not Allowed", {
          status: 405,
          headers: {
            Allow: "PUT, GET, DELETE",
          },
        });
    }
  }
}
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse


class Default(WorkerEntrypoint):
	async def fetch(self, request):
		url = urlparse(request.url)
		key = url.path[1:]

		if request.method == "PUT":
			await self.env.MY_BUCKET.put(
				key,
				request.body,
				onlyIf=request.headers,
				httpMetadata=request.headers,
			)
			return Response(f"Put {key} successfully!")
		elif request.method == "GET":
			obj = await self.env.MY_BUCKET.get(
				key,
				onlyIf=request.headers,
				range=request.headers,
			)

			if obj is None:
				return Response("Object Not Found", status=404)

			# When no body is present, preconditions have failed
			body = obj.body if hasattr(obj, "body") else None
			status = 200 if hasattr(obj, "body") else 412

			headers = {"etag": obj.httpEtag}
			return Response(body, status=status, headers=headers)
		elif request.method == "DELETE":
			await self.env.MY_BUCKET.delete(key)
			return Response("Deleted!")
		else:
			return Response(
				"Method Not Allowed",
				status=405,
				headers={"Allow": "PUT, GET, DELETE"},
			)

5. Доступ к бакету и конфиденциальность

После добавления этого кода в Worker каждый входящий запрос сможет взаимодействовать с бакетом. Это означает, что бакет становится общедоступным, и посторонние могут получать доступ к его содержимому и изменять его.

Теперь вам нужно определить логику авторизации, которая будет решать, кто и какие действия может выполнять с вашим бакетом. Эта логика находится в коде вашего Worker, так как именно приложение отвечает за определение прав пользователей. Ниже приведён краткий список ресурсов по практикам доступа и авторизации:

  1. Базовая аутентификация: показывает, как ограничить доступ с помощью схемы HTTP Basic.
  2. Использование пользовательских заголовков: Разрешить или запретить запрос на основании известного заранее согласованного ключа в заголовке.

Теперь, когда у вас есть новый бакет и Worker, необходимо защитить все операции с бакетом.

Для PUT и DELETE запросов вы будете использовать новый AUTH_KEY_SECRET переменная окружения, которую вы позже определите как Wrangler secret.

Для GET запросов вы обеспечите возможность запроса только одного конкретного файла. Вся эта пользовательская логика выполняется внутри authorizeRequest функцию с hasValidHeader функцию, обрабатывающую логику пользовательского заголовка. Если проверка проходит успешно, операция разрешается.

const ALLOW_LIST = ["cat-pic.jpg"];

// Check requests for a pre-shared secret
const hasValidHeader = (request, env) => {
	return request.headers.get("X-Custom-Auth-Key") === env.AUTH_KEY_SECRET;
};

function authorizeRequest(request, env, key) {
	switch (request.method) {
		case "PUT":
		case "DELETE":
			return hasValidHeader(request, env);
		case "GET":
			return ALLOW_LIST.includes(key);
		default:
			return false;
	}
}

export default {
	async fetch(request, env, ctx) {
		const url = new URL(request.url);
		const key = url.pathname.slice(1);

		if (!authorizeRequest(request, env, key)) {
			return new Response("Forbidden", { status: 403 });
		}

		// ...
	},
};
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse

ALLOW_LIST = ["cat-pic.jpg"]

# Check requests for a pre-shared secret
def has_valid_header(request, env):
	return request.headers.get("X-Custom-Auth-Key") == env.AUTH_KEY_SECRET

def authorize_request(request, env, key):
	if request.method in ["PUT", "DELETE"]:
		return has_valid_header(request, env)
	elif request.method == "GET":
		return key in ALLOW_LIST
	else:
		return False

class Default(WorkerEntrypoint):
	async def fetch(self, request):
		url = urlparse(request.url)
		key = url.path[1:]

		if not authorize_request(request, self.env, key):
			return Response("Forbidden", status=403)

		# ...

Для этого необходимо создать секрет с помощью Wrangler:

npx wrangler secret put AUTH_KEY_SECRET

Эта команда предложит вам ввести секрет в терминале:

npx wrangler secret put AUTH_KEY_SECRET
Enter the secret text you'd like assigned to the variable AUTH_KEY_SECRET on the script named <YOUR_WORKER_NAME>:
*********
🌀  Creating the secret for script name <YOUR_WORKER_NAME>
✨  Success! Uploaded secret AUTH_KEY_SECRET.

Теперь этот секрет доступен как AUTH_KEY_SECRET на env параметр в вашем Worker.

6. Разверните Worker

После настройки Worker и бакета выполните команду npx wrangler deploy команда чтобы развернуть приложение в глобальной сети Cloudflare:

npx wrangler deploy

Проверить работу логики авторизации можно с помощью следующих команд, используя конечную точку развёрнутого Worker:

# Attempt to write an object without providing the "X-Custom-Auth-Key" header
curl https://your-worker.dev/cat-pic.jpg -X PUT --data-binary 'test'
#=> Forbidden
# Expected because header was missing

# Attempt to write an object with the wrong "X-Custom-Auth-Key" header value
curl https://your-worker.dev/cat-pic.jpg -X PUT --header "X-Custom-Auth-Key: hotdog" --data-binary 'test'
#=> Forbidden
# Expected because header value did not match the AUTH_KEY_SECRET value

# Attempt to write an object with the correct "X-Custom-Auth-Key" header value
# Note: Assume that "*********" is the value of your AUTH_KEY_SECRET Wrangler secret
curl https://your-worker.dev/cat-pic.jpg -X PUT --header "X-Custom-Auth-Key: *********" --data-binary 'test'
#=> Put cat-pic.jpg successfully!

# Attempt to read object called "foo"
curl https://your-worker.dev/foo
#=> Forbidden
# Expected because "foo" is not in the ALLOW_LIST

# Attempt to read an object called "cat-pic.jpg"
curl https://your-worker.dev/cat-pic.jpg
#=> test
# Note: This is the value that was successfully PUT above

Пройдя это руководство, вы успешно установили Wrangler и развернули свой бакет R2 в Cloudflare.

  1. Руководства Workers
  2. Примеры Workers