BFFを作成します
This commit is contained in:
parent
1779a68eab
commit
46b8cfddf4
68
src/bff/simpleBFF.php
Normal file
68
src/bff/simpleBFF.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* 簡易BFF(stream版)
|
||||
* APIサーバーへのリクエストを中継しつつ、dist以下の静的ファイルも返す
|
||||
* 大容量100MB:非推奨
|
||||
* 軽量な用途向け(小規模サイトやAPI中継のみなど)
|
||||
*/
|
||||
// ===== 設定 =====
|
||||
$API_BASE_URL = getenv('API_BASE_URL'); // 例: https://xxxxx-uc.a.run.app
|
||||
if (!$API_BASE_URL) {
|
||||
http_response_code(500);
|
||||
echo 'API_BASE_URL is not set';
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===== リクエスト情報 =====
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = $_SERVER['REQUEST_URI']; // クエリ含む
|
||||
$path = parse_url($uri, PHP_URL_PATH);
|
||||
|
||||
// /api/* だけ中継
|
||||
if (strpos($path, '/api') === 0) {
|
||||
$upstreamPath = substr($uri, 4); // /api/foo?x=1 -> /foo?x=1
|
||||
if ($upstreamPath === '') $upstreamPath = '/';
|
||||
|
||||
$url = rtrim($API_BASE_URL, '/') . $upstreamPath;
|
||||
|
||||
// ヘッダそのまま(最低限)
|
||||
$headers = [];
|
||||
foreach (getallheaders() as $k => $v) {
|
||||
if (strtolower($k) === 'host') continue;
|
||||
$headers[] = "$k: $v";
|
||||
}
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'header' => implode("\r\n", $headers),
|
||||
'content' => in_array($method, ['POST', 'PUT', 'PATCH'])
|
||||
? file_get_contents('php://input')
|
||||
: null,
|
||||
'ignore_errors' => true, // 4xx/5xxも受け取る
|
||||
]
|
||||
]);
|
||||
|
||||
$response = file_get_contents($url, false, $context);
|
||||
|
||||
// ステータスコードをそのまま返す
|
||||
if (isset($http_response_header[0])) {
|
||||
preg_match('#HTTP/\d\.\d\s+(\d+)#', $http_response_header[0], $m);
|
||||
if (!empty($m[1])) {
|
||||
http_response_code((int)$m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// ヘッダも最低限返す
|
||||
foreach ($http_response_header as $h) {
|
||||
if (stripos($h, 'Transfer-Encoding:') === 0) continue;
|
||||
if (stripos($h, 'Connection:') === 0) continue;
|
||||
header($h, false);
|
||||
}
|
||||
|
||||
echo $response;
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===== それ以外は静的(例: dist/index.html) =====
|
||||
readfile(__DIR__ . '/dist/index.html');
|
||||
78
src/bff/smallBFF.php
Normal file
78
src/bff/smallBFF.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
// =========================
|
||||
// 設定
|
||||
// =========================
|
||||
$API_BASE_URL = getenv('API_BASE_URL'); // 例: https://xxxxx-uc.a.run.app
|
||||
if (!$API_BASE_URL) {
|
||||
http_response_code(500);
|
||||
echo 'API_BASE_URL is not set';
|
||||
exit;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// リクエスト情報
|
||||
// =========================
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = $_SERVER['REQUEST_URI']; // クエリ含む
|
||||
$path = parse_url($uri, PHP_URL_PATH);
|
||||
|
||||
// =========================
|
||||
// /api/* → Cloud Runへ中継
|
||||
// =========================
|
||||
if (strpos($path, '/api') === 0) {
|
||||
|
||||
// /api/foo?x=1 → /foo?x=1
|
||||
$upstreamPath = substr($uri, 4);
|
||||
if ($upstreamPath === '') {
|
||||
$upstreamPath = '/';
|
||||
}
|
||||
|
||||
$url = rtrim($API_BASE_URL, '/') . $upstreamPath;
|
||||
|
||||
// ヘッダそのまま転送(最低限)
|
||||
$headers = [];
|
||||
foreach (getallheaders() as $k => $v) {
|
||||
if (strtolower($k) === 'host') continue;
|
||||
$headers[] = "$k: $v";
|
||||
}
|
||||
|
||||
// body(非ストリーミング:全部読む)
|
||||
$body = null;
|
||||
if (!in_array($method, ['GET', 'HEAD'])) {
|
||||
$body = file_get_contents('php://input');
|
||||
}
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'header' => implode("\r\n", $headers),
|
||||
'content' => $body,
|
||||
'ignore_errors' => true, // 4xx/5xxも取得
|
||||
]
|
||||
]);
|
||||
|
||||
// ===== 実行 =====
|
||||
$responseBody = file_get_contents($url, false, $context);
|
||||
|
||||
// ===== ステータスコード =====
|
||||
if (isset($http_response_header[0])) {
|
||||
if (preg_match('#HTTP/\d\.\d\s+(\d+)#', $http_response_header[0], $m)) {
|
||||
http_response_code((int)$m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== レスポンスヘッダ =====
|
||||
foreach ($http_response_header as $h) {
|
||||
if (stripos($h, 'Transfer-Encoding:') === 0) continue;
|
||||
if (stripos($h, 'Connection:') === 0) continue;
|
||||
header($h, false);
|
||||
}
|
||||
|
||||
echo $responseBody;
|
||||
exit;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// それ以外は静的(SPA想定)
|
||||
// =========================
|
||||
readfile(__DIR__ . '/dist/index.html');
|
||||
162
src/bff/streamBFF.php
Normal file
162
src/bff/streamBFF.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// ===== 設定 =====
|
||||
$API_BASE_URL = getenv('API_BASE_URL'); // 例: https://xxxxx-uc.a.run.app
|
||||
if (!$API_BASE_URL) {
|
||||
http_response_code(response_code: 500);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo "API_BASE_URL is not set\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
// distのパス(このindex.phpが public/ にある想定)
|
||||
$DIST_DIR = realpath(__DIR__ . '/../dist') ?: (__DIR__ . '/../dist');
|
||||
|
||||
// ===== ユーティリティ =====
|
||||
function send_text(int $code, string $text): void {
|
||||
http_response_code($code);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo $text;
|
||||
exit;
|
||||
}
|
||||
|
||||
function is_hop_by_hop(string $headerNameLower): bool {
|
||||
static $hop = [
|
||||
'transfer-encoding' => true,
|
||||
'connection' => true,
|
||||
'keep-alive' => true,
|
||||
'proxy-authenticate' => true,
|
||||
'proxy-authorization' => true,
|
||||
'te' => true,
|
||||
'trailers' => true,
|
||||
'upgrade' => true,
|
||||
];
|
||||
return isset($hop[$headerNameLower]);
|
||||
}
|
||||
|
||||
// ===== ルーティング =====
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
// クエリ含むREQUEST_URIからパス部分だけ抽出
|
||||
$path = parse_url($uri, PHP_URL_PATH) ?? '/';
|
||||
|
||||
// -------- /api 配下はプロキシ --------
|
||||
if (preg_match('#^/api(?:/.*)?$#', $path)) {
|
||||
$upstreamPath = preg_replace('#^/api#', '', $uri); // クエリ含めてそのまま
|
||||
if ($upstreamPath === '') $upstreamPath = '/';
|
||||
|
||||
// URL結合(API_BASE_URL + upstreamPath)
|
||||
$base = rtrim($API_BASE_URL, '/');
|
||||
$targetUrl = $base . $upstreamPath;
|
||||
|
||||
// 受けたヘッダを基本そのまま転送(Hostは除外)
|
||||
$headers = [];
|
||||
if (function_exists('getallheaders')) {
|
||||
foreach (getallheaders() as $k => $v) {
|
||||
$lk = strtolower($k);
|
||||
if ($lk === 'host') continue;
|
||||
if ($lk === 'content-length') continue; // ストリーム時は基本付けない
|
||||
$headers[] = $k . ': ' . $v;
|
||||
}
|
||||
}
|
||||
|
||||
// 転送用のcURL
|
||||
$ch = curl_init($targetUrl);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// レスポンスをストリームでクライアントに返す
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $headerLine) {
|
||||
$len = strlen($headerLine);
|
||||
$headerLineTrim = trim($headerLine);
|
||||
if ($headerLineTrim === '') return $len;
|
||||
|
||||
// HTTP/1.1 200 OK みたいな行は無視(ステータスは後でセット)
|
||||
if (stripos($headerLineTrim, 'HTTP/') === 0) return $len;
|
||||
|
||||
$parts = explode(':', $headerLineTrim, 2);
|
||||
if (count($parts) !== 2) return $len;
|
||||
$name = trim($parts[0]);
|
||||
$value = trim($parts[1]);
|
||||
|
||||
if (is_hop_by_hop(strtolower($name))) return $len;
|
||||
header($name . ': ' . $value, false);
|
||||
return $len;
|
||||
});
|
||||
|
||||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
|
||||
echo $data;
|
||||
return strlen($data);
|
||||
});
|
||||
|
||||
// bodyはphp://inputをそのまま流す(GET/HEADは除外)
|
||||
if (!in_array($method, ['GET', 'HEAD'], true)) {
|
||||
$in = fopen('php://input', 'rb');
|
||||
if ($in === false) send_text(500, "Failed to open php://input\n");
|
||||
|
||||
// Content-Lengthが分かるなら設定(無理なら未設定でOK)
|
||||
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? null;
|
||||
if ($contentLength !== null && ctype_digit((string)$contentLength)) {
|
||||
curl_setopt($ch, CURLOPT_INFILESIZE, (int)$contentLength);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_UPLOAD, true);
|
||||
curl_setopt($ch, CURLOPT_INFILE, $in);
|
||||
}
|
||||
|
||||
// ステータスコードをクライアントへ反映
|
||||
$ok = curl_exec($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE) ?: 502;
|
||||
|
||||
if ($ok === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
http_response_code(502);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'Bad Gateway', 'detail' => $err], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
http_response_code($status);
|
||||
exit;
|
||||
}
|
||||
|
||||
// -------- 静的配信(dist) --------
|
||||
// dist内ファイルがあれば返す(パストラバーサル対策でrealpathで検証)
|
||||
$rel = ltrim($path, '/');
|
||||
$maybe = $DIST_DIR . '/' . $rel;
|
||||
|
||||
if ($rel !== '' && file_exists($maybe)) {
|
||||
$real = realpath($maybe);
|
||||
if ($real !== false && str_starts_with($real, $DIST_DIR) && is_file($real)) {
|
||||
// 雑にMIME(必要なら拡張)
|
||||
$ext = strtolower(pathinfo($real, PATHINFO_EXTENSION));
|
||||
$mime = match ($ext) {
|
||||
'js' => 'application/javascript; charset=utf-8',
|
||||
'css' => 'text/css; charset=utf-8',
|
||||
'html' => 'text/html; charset=utf-8',
|
||||
'json' => 'application/json; charset=utf-8',
|
||||
'png' => 'image/png',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'svg' => 'image/svg+xml',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
header('Content-Type: ' . $mime);
|
||||
readfile($real);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// SPA fallback(index.html)
|
||||
$index = $DIST_DIR . '/index.html';
|
||||
if (is_file($index)) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
readfile($index);
|
||||
exit;
|
||||
}
|
||||
|
||||
send_text(404, "dist/index.html not found\n");
|
||||
Loading…
x
Reference in New Issue
Block a user