Hugoで作成したサイトをアクセス制限付きで公開したい
Hugoはとても簡単にサイトが作れて、かっこいいテーマもたくさんあって楽しいですね。
あるコミュニティ向けにちょっとした情報サイトを作ろうと思い、そうすると閲覧制限を付けたいので、どうすると良いのかなと考えました。
Hugoで作成したサイトを Google App Engine で公開します。
GETリクエストをFastAPIで処理して、特定のパスに対してはBasic認証を通さないとコンテンツを戻さないようにすることで、HUGOのコンテンツを、制限なし、制限付きに分けて公開するできるようにしました。
.
├── app.py
├── app.yaml
├── mysite
│ ├── archetypes
│ ├── assets
│ ├── config.toml
│ ├── content
│ ├── data
│ ├── layouts
│ ├── public
│ ├── resources
│ ├── static
│ └── themes
└── requirements.txt
最終的なファイル構成はこのようになります。 mysite
以下にHugoのサイトを置きます。
Hugoでサイトを作る
Hugoでサイトを作ります。例として、以下のようにしています。
.
├── content
│ ├── _index.md
│ ├── docs
│ │ ├── caution.md
│ │ ├── doc.md
│ │ └── secret
│ │ └── secret.md
docs/doc.md
は通常のページ。制限無しに公開するページです。
docs/secret
以下にあるページは制限付きのページにします。
docs/caution.md
は制限付きページの認証に失敗した際に表示するページです。
baseURL = "https://xxxxxxxxx.df.r.appspot.com/"
languageCode = "ja"
title = "おもしろサイト"
...
config.toml
の baseURL を GAEのURLに変えておきます。
GAE向けのファイルを用意する
runtime: python39
entrypoint: uvicorn app:app --port $PORT
app.yaml
シンプル。
fastapi
uvicorn
aiofiles
requirements.txt
aiofiles
はFastAPIが FileResponse
を返すのに必要みたいです。
webフレームワークはFastAPIでなくとも良いですが、FastAPIは起動が早いので気に入っています。
import secrets
from pathlib import Path
from fastapi import FastAPI, Depends, status, HTTPException
from fastapi.responses import FileResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
@app.get("/docs/secret/{file_path:path}")
async def secret_path(
file_path: str, credentials: HTTPBasicCredentials = Depends(security)
):
correct_username = secrets.compare_digest(credentials.username, "user")
correct_password = secrets.compare_digest(credentials.password, "pass")
if not (correct_username and correct_password):
response = FileResponse("mysite/public/docs/caution/index.html")
response.status_code = status.HTTP_401_UNAUTHORIZED
return response
target = Path("mysite/public/docs/secret/{}".format(file_path))
return read_file(target)
@app.get("/{file_path:path}")
async def public_path(file_path: str):
target = Path("mysite/public/{}".format(file_path))
return read_file(target)
def read_file(target: Path):
if not target.exists():
response = FileResponse("mysite/public/404.html")
response.status_code = status.HTTP_404_NOT_FOUND
elif target.is_dir():
response = FileResponse(target.joinpath("index.html"))
elif target.is_file():
response = FileResponse(target)
else:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
return response
説明
app.py
@app.get("/docs/secret/{file_path:path}")
async def secret_path(
file_path: str, credentials: HTTPBasicCredentials = Depends(security)
):
correct_username = secrets.compare_digest(credentials.username, "user")
correct_password = secrets.compare_digest(credentials.password, "pass")
if not (correct_username and correct_password):
response = FileResponse("mysite/public/docs/caution/index.html")
response.status_code = status.HTTP_401_UNAUTHORIZED
return response
target = Path("mysite/public/docs/secret/{}".format(file_path))
return read_file(target)
↑ まず制限付きページへのGETリクエストを受けるデコレータを、定義します。
Basic認証のログインダイアログを表示して、IDとPASSを受け取ります。
入力されたIDとPASSを検証して、一致していなければ、認証失敗を示すページをFileResponse
で返します。
ステータスコードは認証失敗を示す401
にしておきます。そうしておかないと、再度制限付きページにアクセスしたときにログインダイアログが表示されなくなります(一度入力したものが自動で使われてしまう?)。
ID/PASSが一致していれば、mysite/public
以下のファイルパスへの置き換えをしてFileResponse
を返します。
@app.get("/{file_path:path}")
async def public_path(file_path: str):
target = Path("mysite/public/{}".format(file_path))
return read_file(target)
↑ 制限のないページへのGETリクエストを受けるデコレータも定義します。
def read_file(target: Path):
if not target.exists():
response = FileResponse("mysite/public/404.html")
response.status_code = status.HTTP_404_NOT_FOUND
elif target.is_dir():
response = FileResponse(target.joinpath("index.html"))
elif target.is_file():
response = FileResponse(target)
else:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
return response
↑ mysite/public
以下のファイルパスをチェックして、もし存在していなければ404.html
を返します。(テーマによってはパスが違うのかもしれません)
ディレクトリであれば、その直下のindex.html
を戻します。ファイルであれば、そのまま返します。
さいごよくわからないものはとりあえず500 internal server error
にしておきました。