PHP | CDNリソース最適化とセキュリティ強化のためのプロキシスクリプト解説

はじめに

この記事では、Vue.js や TailwindCSS などのライブラリを安全に配信するためのセキュアなコンテンツ配信とキャッシュ最適化を実現するプロキシスクリプトについて解説します。このプロジェクトは、安全かつ効率的なリソース管理を目的としています。

CDNを利用した外部リソースの取り込みを安全に行うために、リソースの整合性チェックやキャッシュ管理、アクセス制限を提供します。


プロジェクト概要

このスクリプトは、外部リソースへのアクセス制御とキャッシュ管理を通じて、パフォーマンス向上とセキュリティ強化を実現します。

ファイル構成

project-root/
├── .htaccess       # アクセス制御やリダイレクトルールを設定
├── config.php      # 環境設定とアクセス制限の管理
├── proxy.php       # プロキシ機能の実装
├── cache/          # キャッシュファイル格納用ディレクトリ
└── logs/           # エラーログ格納用ディレクトリ

利用例

  1. Vue.js や TailwindCSS の安全な配信
  • 外部CDNリソースを事前に検証し、安全性を担保。
  1. レート制限によるアクセス制御
  • 高トラフィックサイトでの負荷軽減と悪用防止。
  1. キャッシュ最適化による高速配信
  • 頻繁にアクセスされるリソースをローカルキャッシュから提供。

実際の利用例

以下は、このプロキシスクリプトを用いた具体的な利用例です。

CDN直接アクセス例:

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

プロキシスクリプト経由のアクセス例:

<script src="/cdn_proxy/?url=https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.min.js"></script>

この例では、プロキシスクリプト経由でVue.jsの安全性とキャッシュ管理を行い、クライアントへの負荷軽減とセキュリティ強化を実現します。


ハッシュ値の取得方法

CDNリソースのハッシュ値を取得するには以下のコマンドを使用します。

$ curl -s https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.min.js -o vue.global.min.js
$ openssl dgst -sha256 -binary vue.global.min.js | openssl base64 -A

この結果をconfig.php内のhashesセクションに追加することで、整合性チェックを有効化します。


各ファイルの役割

1. .htaccess

このファイルは、アクセス制御やリダイレクトルールを管理します。

# config.php への直接アクセスを禁止
<Files "config.php">
    Require all denied
</Files>

# cacheディレクトリへのアクセスを禁止
RewriteEngine On
RewriteRule ^cache/ - [F,L]

# logsディレクトリへのアクセスを禁止
RewriteRule ^logs/ - [F,L]

# proxy.php をデフォルトスクリプトに設定
DirectoryIndex proxy.php

# URLパラメータを proxy.php に渡す
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ proxy.php [QSA,L]

# HTTPS強制(必要に応じて有効化)
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

2. config.php

環境設定や重要な変数を定義するファイルです。

<?php
return [
    'allowedClientIPs' => [],
    'allowedClientHosts' => [],
    'allowedResourceDomains' => [
        'cdn.jsdelivr.net',
        'unpkg.com',
        'cdnjs.cloudflare.com',
        'fonts.googleapis.com'
    ],
    'hashes' => [
        'https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.min.js' => 'FFUm1xEnvS6Pv0RAH/PxoyDkfQ1M5bLqklcDS7zNNm0=',
    ],
    'cacheLimit' => 50,          // 最大キャッシュファイル数
    'cacheTime' => 3600,         // キャッシュ有効期限(秒)
    'rateLimitTime' => 60,       // レートリミット期間(秒)
    'rateLimitRequests' => 20    // 最大リクエスト回数
];
?>

3. proxy.php

プロキシ機能を実装するスクリプトです。

<?php
// 設定ファイルの読み込み
$config = include(__DIR__ . '/config.php');

// 設定値を取得
$allowedClientIPs = $config['allowedClientIPs'] ?? [];
$allowedClientHosts = $config['allowedClientHosts'] ?? [];
$allowedResourceDomains = $config['allowedResourceDomains'];
$hashes = $config['hashes'];

// キャッシュ設定
$cacheDir = __DIR__ . '/cache';
$cacheLimit = $config['cacheLimit']; // 最大50ファイルまで
$cacheTime = $config['cacheTime']; // キャッシュ有効期限(秒)

// レートリミット設定
session_start();
$timeLimit = $config['rateLimitTime']; // 秒
$requestLimit = $config['rateLimitRequests']; // 最大リクエスト回数

// ログ出力
function logError($message) {
    $logFile = __DIR__ . '/logs/error.log';
    $logEntry = date('Y-m-d H:i:s') . ' - ' . $message . "\n";
    file_put_contents($logFile, $logEntry, FILE_APPEND);
}

// レートリミットチェック
if (!isset($_SESSION['last_access'])) {
    $_SESSION['last_access'] = [];
}
$now = time();
$_SESSION['last_access'] = array_filter($_SESSION['last_access'], function ($timestamp) use ($now, $timeLimit) {
    return ($now - $timestamp) < $timeLimit;
});
if (count($_SESSION['last_access']) >= $requestLimit) {
    http_response_code(429);
    echo 'Error: Too many requests.';
    exit;
}
$_SESSION['last_access'][] = $now;

// クライアントIPチェック
$clientIP = $_SERVER['REMOTE_ADDR'];
if (!empty($allowedClientIPs) && !in_array($clientIP, $allowedClientIPs)) {
    logError("Unauthorized access attempt from $clientIP");
    http_response_code(403);
    echo 'Error: Access denied.';
    exit;
}

// クライアントホストチェック
$clientHost = gethostbyaddr($clientIP);
if (!empty($allowedClientHosts) && !in_array($clientHost, $allowedClientHosts)) {
    logError("Unauthorized host access attempt: $clientHost");
    http_response_code(403);
    echo 'Error: Host access denied.';
    exit;
}

// リクエストURL検証
$resourceUrl = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL);
if (!$resourceUrl) {
    http_response_code(400);
    echo 'Error: Invalid URL.';
    exit;
}
$parsedUrl = parse_url($resourceUrl);
if (!in_array($parsedUrl['host'], $allowedResourceDomains)) {
    logError("Unauthorized domain access attempt: {$parsedUrl['host']}");
    http_response_code(403);
    echo 'Error: Domain not allowed.';
    exit;
}

// キャッシュ管理
$cacheFile = $cacheDir . '/' . md5($resourceUrl) . '.cache';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
    $content = file_get_contents($cacheFile);
} else {
    $context = stream_context_create([
        'http' => ['header' => 'User-Agent: Mozilla/5.0']
    ]);
    $content = @file_get_contents($resourceUrl, false, $context);
    if ($content !== false) {
        file_put_contents($cacheFile, $content);
    } elseif (file_exists($cacheFile)) {
        $content = file_get_contents($cacheFile);
    } else {
        logError("Failed to fetch resource: $resourceUrl");
        http_response_code(500);
        echo 'Error: Unable to load resource and no cache available.';
        exit;
    }
}

// ハッシュ検証
if (isset($hashes[$resourceUrl])) {
    $contentHash = base64_encode(hash('sha256', $content, true));
    if ($contentHash !== $hashes[$resourceUrl]) {
        logError("Content integrity check failed: $resourceUrl");
        http_response_code(403);
        echo 'Error: Content integrity check failed.';
        exit;
    }
}

// MIMEタイプ検出
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$contentType = finfo_file($finfo, $cacheFile);
finfo_close($finfo);

// ヘッダー設定とリソース送信
header('Content-Type: ' . $contentType);
echo $content;

// キャッシュクリーンアップ
$files = glob($cacheDir . '/*.cache');
if (count($files) > $cacheLimit) {
    array_multisort(array_map('filemtime', $files), SORT_NUMERIC, $files);
    foreach (array_slice($files, 0, count($files) - $cacheLimit) as $file) {
        unlink($file);
    }
}
?>

まとめ

このスクリプトは、CDNリソースの安全性とキャッシュ管理を強化し、高速かつセキュアなコンテンツ配信を提供します。具体的な活用例と整合性チェックの方法を活用し、安心して利用できる環境を構築してください。

コメント

タイトルとURLをコピーしました