この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
#26 無し
#27 Setup lint to enable code styles check
eslintの設定。let -> const
お、またmetrueさんからのPR。どこからやること見つけてくるのかなと思ったら、isseuでやりとりしていた。
なるほど、issueを見に行って、できそうなものを手伝っていくやり方もあるんだなぁ。自分でバグとか機能のisseu立てなきゃコントリビュートできないと思っていたけど、ちゃんと相手の迷惑ならない形でisseuを消化していくサポートであれば喜ばれそう。
#28 feat(basic-auth): basic-auth middleware
HTTP Basic認証を実装するミドルウェアを追加
こちらもmetrueさんからのPR。
Basic認証とはもおさらいしておく。
point認証機能だよ
pointIDとパスワードで認証するよ
pointHTTPにあらかじめ用意されている仕組みだよ
"Basic" HTTP 認証方式は RFC 7617 で定義されており、資格情報を送信するのに、ユーザー ID とパスワードの組を base64 を使用してエンコードします。
Basic 認証の安全性
ユーザー ID とパスワードは、ネットワークを介してクリアテキストとして渡されるため(base64 でエンコードされますが、 base64 は可逆エンコードです)、 Basic 認証方式は安全ではありません。 Basic 認証と組み合わせて HTTPS/TLS を使用する必要があります。これらの追加のセキュリティ強化機能がない場合は、機密情報や重要な情報を保護するために Basic 認証を使用しないでください。
- ユーザーが認証が必要なページにアクセス
↓ - サーバーが 401 Unauthorized を返す
WWW-Authenticate: Basic realm="Secure Area"
↓ - ブラウザがログインダイアログを表示
ユーザー名: hono
パスワード: acoolproject
↓ - ブラウザが認証情報をBase64エンコードして送信
Authorization: Basic aG9ubzphY29vbHByb2plY3Q=
↓ - サーバーが認証情報を検証
↓ - 認証成功 → ページを表示
export const basicAuth = (options: {
username: string;
password: string;
realm?: string;
}) => {
if (!options.realm) {
options.realm = 'Secure Area'
}
return async (ctx: Context, next: Function) => {
const user = auth(ctx.req)
const usernameEqual = user && await timingSafeEqual(options.username, user.username)
const passwordEqual = user && await timingSafeEqual(options.password, user.password)
if (!user || !usernameEqual || !passwordEqual) {
ctx.res = new Response('Unauthorized', {
status: 401,
headers: {
'WWW-Authenticate':
'Basic realm="' + options.realm.replace(/"/g, '\\"') + '"',
},
})
return
}
return next()
}
}
- Authorizationヘッダーから認証情報を取得
- ユーザー名とパスワードを検証
- 認証成功 → 次のミドルウェア/ハンドラーへ
- 認証失敗 → 401 Unauthorizedレスポンス
使い方
import { Hono, Middleware } from 'hono'
const app = new Hono()
// Basic認証を特定のパスに適用
app.use(
'/auth/*',
Middleware.basicAuth({
username: 'hono',
password: 'acoolproject',
})
)
'WWW-Authenticate'ってなんだっけ。
リソースへのアクセス権を得るために使われる HTTP 認証メソッド (またはチャレンジ) を定義します。
サーバーがクライアントに「認証が必要です」と伝えるためのレスポンスヘッダーぽい。
timingSafeEqualってなんぞや
src/util.ts
export const timingSafeEqual = async (a: any, b: any) => {
const sa = await crypto.subtle.digest(
{
name: 'SHA-256',
},
new TextEncoder().encode(String(a))
)
const sb = await crypto.subtle.digest(
{
name: 'SHA-256',
},
new TextEncoder().encode(String(b))
)
return bufferEqual(sa, sb) && a === b
}
タイミング攻撃対策。タイミング攻撃対策ってなんだっけ(ポンコツ)
タイミング攻撃(Timing Attack)とは、システムや暗号化プロセスの処理時間のわずかな違いを観察・分析することで、秘密情報を推測する攻撃手法です。サイドチャネル攻撃の一種で、システムが異なる入力データに対して異なる時間をかけて処理を行う性質を利用し、その処理時間から暗号鍵やパスワードといった機密情報を推測します。タイミング攻撃は、システムの動作時間や通信ラグなど微細な時間の違いに依存するため、特に注意が必要です。
パスワードを1文字ずつ推測しながら、レスポンス時間を測定
-> 時間が長い = より多くの文字が一致している
攻撃者は時間を測定しながら、1文字ずつ正解を推測できる
もう一度timingSafeEqualを見てみる
src/util.ts
export const timingSafeEqual = async (a: any, b: any) => {
// SHA-256ハッシュ化
const sa = await crypto.subtle.digest(
{
name: 'SHA-256',
},
new TextEncoder().encode(String(a))
)
const sb = await crypto.subtle.digest(
{
name: 'SHA-256',
},
new TextEncoder().encode(String(b))
)
return bufferEqual(sa, sb) && a === b
}
bufferEqualは以下の通り
src/util.ts
const bufferEqual = (a: ArrayBuffer, b: ArrayBuffer) => {
// aとbが全く同じメモリ上のオブジェクト(同じメモリ領域)を指しているか?」をチェック
// これにより、自己比較(同じバッファ同士の比較)を高速化
if (a === b) {
return true
}
// バッファの長さが異なる場合、内容が異なると判断
if (a.byteLength !== b.byteLength) {
return false
}
// DataView: ArrayBufferの内容を読み書きするためのビュー
// getUint8(index)で指定位置の1バイト(0-255)を取得できる
const va = new DataView(a)
const vb = new DataView(b)
// iにバッファの長さ(バイト数)をセット
// 例: 32バイトのハッシュなら i = 32
let i = va.byteLength
// while (i--): 後置デクリメント
// i=32なら、31, 30, 29, ... 1, 0 の順に処理(逆順)
// i=0になるとループ終了
while (i--) {
if (va.getUint8(i) !== vb.getUint8(i)) {
return false
}
}
return true
}
逆順なのはなぜだろう。スッキリ書けるから?なんか速いとかあるんだろうか。
#29 Chained route without route method
src/hono.ts
addRoute(method: string, arg: string | Handler, ...args: Handler[]): Hono {
method = method.toUpperCase()
if (typeof arg === 'string') {
+ this.tempPath = arg // ← この1行を追加
this.router.add(method, arg, args)
} else {
args.unshift(arg)
this.router.add(method, this.tempPath, args)
}
return this
}
何が変わったか??
変更前(動かないコード)
const app = new Hono()
// これが動かなかった
app
.get('/users', () => new Response('GET /users'))
.post(() => new Response('POST /users')) // ❌ エラー
.put(() => new Response('PUT /users')) // ❌ エラー
-
.post()にパスがないのでthis.tempPathが空 -
this.tempPathが更新されないまま使われる
this.tempPath = arg を追加すると
- パスが明示的に指定された場合、tempPath を更新
- 次のチェーンでパスを省略しても、前のパスが使われる
具体例。
app
.get('/users', handler1)
.post(handler2)
.put(handler3)
// ステップ1: app.get('/users', handler1)
// - arg = '/users' (string)
// - this.tempPath = '/users' ← 更新
// - this.router.add('GET', '/users', [handler1])
// - return this
// ステップ2: .post(handler2)
// - arg = handler2 (function)
// - args.unshift(handler2)
// - this.router.add('POST', this.tempPath, [handler2])
// ^^^^^^^^^^^^^^^^
// '/users' が使われる
// - return this
// ステップ3: .put(handler3)
// - arg = handler3 (function)
// - args.unshift(handler3)
// - this.router.add('PUT', this.tempPath, [handler3])
// ^^^^^^^^^^^^^^^^
// '/users' が使われる
// - return this
ふむ。1行だけでも利便性が大きく変わるのね....フレームワークって細やか。
#30 Add SSR with Nono-JSX example
HonoとJSXを使ったServer-Side Rendering(SSR)のサンプルプロジェクトを追加
Nano-JSXという軽量なJSXライブラリを使用して、Cloudflare Workers上でサーバーサイドレンダリングを実現する例
Nano-JSX知らなかったけど"Lightweight 1kB JSX library"とトップにあるように軽量で速そう。2025年12月現在でstar1.6k。今度使ってみよう。
現在のHonoは独自で提供されていた気がして調べたら、やはり'hono/jsx-renderer'があった。
https://hono.dev/docs/middleware/builtin/jsx-renderer
ソースコードちらっとみたけどNano-JSXへの依存はなく完全独自実装に移行している様子。これもどこかのPRで出てくるかな。楽しみ。