-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
135 lines (107 loc) · 3.97 KB
/
Copy pathserver.js
File metadata and controls
135 lines (107 loc) · 3.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* @fileoverview Express エントリポイント
* 環境変数の読み込み・バリデーション、Expressアプリの初期化、
* セキュリティヘッダー、静的ファイル配信、APIルートのマウントを行う。
*/
import express from 'express';
import { createRouter } from './src/routes.js';
import { createGiteaClient } from './src/gitea-client.js';
import { createCache } from './src/cache.js';
// ========================================
// 環境変数バリデーション
// ========================================
const GITEA_URL = process.env.GITEA_URL;
const GITEA_TOKEN = process.env.GITEA_TOKEN;
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || '';
const missing = [];
if (!GITEA_URL) missing.push('GITEA_URL');
if (!GITEA_TOKEN) missing.push('GITEA_TOKEN');
if (missing.length > 0) {
console.error(`エラー: 必須環境変数が未設定です: ${missing.join(', ')}`);
process.exit(1);
}
const PORT = parseInt(process.env.PORT, 10) || 3100;
const CACHE_TTL = parseInt(process.env.CACHE_TTL, 10) || 300;
// ========================================
// アプリケーション初期化
// ========================================
const giteaClient = createGiteaClient(GITEA_URL, GITEA_TOKEN);
const cache = createCache(CACHE_TTL);
const app = express();
// ========================================
// セキュリティヘッダー
// ========================================
app.use((req, res, next) => {
// XSS保護
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
// クリックジャッキング防止
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
// CSP — D3.js CDN + dagre CDN を許可
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self' https://d3js.org https://unpkg.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"connect-src 'self'",
"font-src 'self'",
"frame-ancestors 'self'",
].join('; '));
// キャッシュ制御(APIはno-cache、静的ファイルはブラウザキャッシュ許可)
if (req.path.startsWith('/api/')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
}
// Giteaトークンがレスポンスに漏れないよう確認
res.removeHeader('X-Powered-By');
next();
});
// ========================================
// Rate Limiting(簡易実装)
// ========================================
const rateLimitMap = new Map();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1分
const RATE_LIMIT_MAX = 120; // 1分あたり120リクエスト
app.use('/api/', (req, res, next) => {
const ip = req.ip || req.socket.remoteAddress || 'unknown';
const now = Date.now();
const entry = rateLimitMap.get(ip);
if (!entry || now - entry.start > RATE_LIMIT_WINDOW) {
rateLimitMap.set(ip, { start: now, count: 1 });
return next();
}
entry.count++;
if (entry.count > RATE_LIMIT_MAX) {
return res.status(429).json({ error: 'Too many requests. Try again later.' });
}
next();
});
// 定期的にrate limitマップをクリーンアップ
setInterval(() => {
const now = Date.now();
for (const [ip, entry] of rateLimitMap) {
if (now - entry.start > RATE_LIMIT_WINDOW * 2) {
rateLimitMap.delete(ip);
}
}
}, RATE_LIMIT_WINDOW);
// ========================================
// 静的ファイル配信
// ========================================
app.use(express.static('public', {
maxAge: '1h',
etag: true,
}));
// ========================================
// API ルート
// ========================================
const router = createRouter(giteaClient, cache);
app.use('/api', router);
// ========================================
// サーバー起動
// ========================================
app.listen(PORT, () => {
console.log(`サーバー起動: http://localhost:${PORT}`);
console.log(`Gitea URL: ${GITEA_URL}`);
console.log(`キャッシュ TTL: ${CACHE_TTL}秒`);
// トークンはログに出さない
});