今回やりたいこと
「リソースや静的なWebページを特定の相手にだけ提供する(望まない相手の直アクセスでは提供しない)」を実装する。
今回のファイル構造
┣📂 commonphp
┃ ┣📃 .htaccess
┃ ┗📃 LoginUtil.php
┣📂 data
┃ ┣📃 .htaccess
┃ ┗📃 data.php
┣📂 loginonly
┃ ┣📂 data
┃ ┃ ┣📃(特定の相手にだけ提供したいファイル)
┃ ┗📃 .htaccess
┣📃 login.php
┗📃 logout.php
今回ではログイン処理自体は本題ではないため簡略化しています。
(login.php、logout.php、およびcommonphp/LoginUtil.php)
そのため、ログイン処理とログイン判定処理は実際の環境に合わせて置き換えてください。
実装
login.php
セッションを使用して「ログインした」という状態を作る再現をするための簡素的な例です。アクセスするとログインしたということになります。
<?php
require_once(dirname(__FILE__) . "/commonphp/LoginUtil.php");
Login();
print("ログインしました");
?>
login.php
セッションを操作して「ログインした」という状態を解除する再現をするための簡素的な例です。アクセスするとログアウトしたということになります。
<?php
require_once(dirname(__FILE__) . "/commonphp/LoginUtil.php");
Logout();
print("ログアウトしました");
?>
commonphpディレクトリ内
.htaccess(commonphp/.htaccess)
今回はこのディレクトリにログイン関連処理を共通化して置いているため、HTTP経由でアクセスできないようにしています。
RewriteEngine On
Options -Indexes
Deny from All
LoginUtil.php
先述の通り簡易的な例であるため、セッションにログインしたという情報を持たせたり解除したり、それを判定するだけのものになっています。
<?php
if (session_status() == PHP_SESSION_NONE && !session_start())
{
// セッションが開始されていない時にセッションを開始できなかった場合
http_response_code(503);
exit;
}
/**
* ログインフラグのセッション変数名
*/
const LoginFlagName = "isServiceLogin";
/**
* ログイン処理
*/
function Login()
{
$_SESSION[LoginFlagName] = true;
}
/**
* ログアウト処理
*/
function Logout()
{
if (isset($_SESSION[LoginFlagName])) unset($_SESSION[LoginFlagName]);
}
/**
* ログインしているか判定する
* @return bool 判定結果
*/
function IsLogin()
{
return isset($_SESSION[LoginFlagName]) && $_SESSION[LoginFlagName] == true;
}
?>
dataディレクトリ内
.htaccess(data/.htaccess)
data以下のURLが指定された場合に、data/data.php?urI=(data以下のパス)へリダイレクトするようにしています。また、リダイレクトのために使用するクエリ「urI」が指定されている場合にはBad Requestを返すようにしています。
使用する際はRewriteBase (ドキュメントルートからdata/までのパス)を環境にあわせて変更してください。
RewriteEngine on
Options -Indexes
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires 0
# カレントディレクトリ指定
RewriteBase (ドキュメントルートからdata/までのパス)
# 指定されたURLのクエリに「urI」というキーが存在する場合はBad Requestを返す
RewriteCond %{REQUEST_URI} !^.*data.php$
RewriteCond %{QUERY_STRING} (^|&)urI=.*($|&)
RewriteRule ^.*$ - [R=400]
RewriteRule ^data\.php - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# クライアントから見たURLを変えずにdata.php?urI=[URLのdata以下]にリダイレクトする
RewriteRule ^(.*)$ data.php?urI=$1 [QSA,L]
# 指定されたURLのdata以下がない場合はNot Foundを返す
RewriteRule ^.*$ - [R=404]
data.php
指定されたURLを検査し、loginonly/data/以下に同名のファイルがあればそれを提供します。
また、ディレクトリ名の末尾に/がない場合は/を付与して再リダイレクトします。
加えて、念のためURLのdata/以下のパスが../によってdata以上を参照しようとしている場合にはBad Requestを返すようにしています。
使用する際には、// ログイン判定処理付近の行をお使いの判定処理に置き換えてください。
<?php
$url = parse_url($_SERVER["REQUEST_URI"]);
if ($url === false)
{
// 不正なURLにより呼び出された場合
http_response_code(400);
exit;
}
$scriptPath = $_SERVER["SCRIPT_NAME"];
$path = $url["path"];
if ($path == $scriptPath)
{
// このphpファイル自体を直接指定して呼び出した場合
http_response_code(400);
exit;
}
// ログイン判定処理
require_once(dirname(__FILE__) . "/../commonphp/LoginUtil.php");
if (!IsLogin())
{
// ログインしていない場合
http_response_code(403);
exit;
}
if (!isset($_GET["urI"]))
{
// クエリ「urI」が存在しない場合
http_response_code(400);
exit;
}
// 相対パスと各ディレクトリ末尾のピリオドを処理する
$rawPath = $_GET["urI"];
$rawPathLength = mb_strlen($rawPath);
$paths = [];
$pathStartIndex = 0;
for ($i = 0; $i < $rawPathLength; ++$i)
{
$character = mb_substr($rawPath, $i, 1, "UTF-8");
if ($i == $rawPathLength - 1 || $character === '/')
{
if ($i == $rawPathLength - 1 && $character !== '/') ++$i;
$rawCurrentPath = mb_substr($rawPath, $pathStartIndex, $i++ - $pathStartIndex);
if ($rawCurrentPath == "..")
{
if (count($paths) == 0)
{
// 指定されたパスが既定よりも上のディレクトリを参照しようとしている場合
http_response_code(400);
exit;
}
array_pop($paths);
}
else if ($rawCurrentPath != ".")
{
for ($excludePeriodIndex = mb_strlen($rawCurrentPath) - 1; $excludePeriodIndex >= 0; --$excludePeriodIndex)
{
$pathCharacter = mb_substr($rawCurrentPath, $excludePeriodIndex, 1, "UTF-8");
if ($pathCharacter != '.') break;
}
if (++$excludePeriodIndex <= 0)
{
// 有効な文字がないディレクトリが存在する場合
http_response_code(400);
exit;
}
$paths[] = mb_substr($rawCurrentPath, 0, $excludePeriodIndex, "UTF-8");
}
$pathStartIndex = $i;
}
}
$targetPath = ((count($paths) > 0) ? $paths[0] : "");
for ($i = 1; $i < count($paths); ++$i)
{
$targetPath .= "/" . $paths[$i];
}
$finalTargetPath = "../loginonly/data/" . $targetPath;
if (!file_exists(($finalTargetPath)))
{
// 指定されたファイルが存在しない場合
http_response_code(404);
exit;
}
if (is_dir($finalTargetPath))
{
// 指定されたURLがディレクトリである場合
if (mb_substr($path, -1, mb_strlen($path, "UTF-8"), "UTF-8") !== '/')
{
// 末尾にディレクトリ区切り文字が存在しない場合はスラッシュを入れてリダイレクトする
$newUrl = $path . '/';
if (isset($url["query"])) $newUrl .= '?' . $url["query"];
header("Location: " . $newUrl);
exit;
}
// インデックスファイルのファイル名
$IndexFileNames = [
"index.php", "index.html", "index.htm"
];
$isFound = false;
foreach ($IndexFileNames as $fileName)
{
$dirIndexFilePath = $finalTargetPath . '/' . $fileName;
if (file_exists(($dirIndexFilePath)))
{
// ディレクトリ内にインデックスファイルが存在する場合
$finalTargetPath = $dirIndexFilePath;
$isFound = true;
break;
}
}
if (!$isFound)
{
// ディレクトリ内にインデックスファイルが存在しない場合
http_response_code(404);
exit;
}
}
if (mb_strtolower(mb_substr($finalTargetPath, -4, 4, "UTF-8")) == ".php")
{
// リダイレクト先がPHPである場合
unset($_GET["urI"]);
require_once(dirname(__FILE__) . '/' . $finalTargetPath);
exit;
}
// 対象ファイルのMIMEタイプを取得する
$mime = mime_content_type($finalTargetPath);
if ($mime === false) $mime = "application/octet-stream";
// バイナリファイルも出力できるようにする
header("Content-Type: " . $mime);
header("Content-Length: " . filesize($finalTargetPath));
// ファイルを出力する
readfile($finalTargetPath);
?>
loginonlyディレクトリ内
.htaccess(loginonly/.htaccess)
内容はcommonphp内のものと同一です。この中のファイルはdata/data.phpによって提供されるためHTTP経由でアクセスできないようにしています。
RewriteEngine On
Options -Indexes
Deny from All
使い方・挙動
(今回のディレクトリ構造までのURL)/data/(loginonly/data/以下のファイルのパス)
にアクセスすると、ログインしている場合はloginonly/data/以下の同名のファイルを取得できます。(そのファイルが.phpである場合は実行します)
また、ログインしていない場合はForbiddenエラーを、ファイルが存在しない場合はNot Foundエラーを返します。
また、URLで指定されたphpを実行せず、その内容を取得させたい場合は、data/data.phpの
if (mb_strtolower(mb_substr($finalTargetPath, -4, 4, "UTF-8")) == ".php")
{
// リダイレクト先がPHPである場合
unset($_GET["urI"]);
require_once(dirname(__FILE__) . '/' . $finalTargetPath);
exit;
}
を除去してください。