|
63 | 63 | skip(req, _res) { |
64 | 64 | return !!cache.get(req.params.login) |
65 | 65 | }, |
66 | | - message:"Too many requests", |
| 66 | + message:"Too many requests: retry later", |
| 67 | + headers:true, |
67 | 68 | ...ratelimiter, |
68 | 69 | })) |
69 | 70 | } |
|
74 | 75 | }) |
75 | 76 |
|
76 | 77 | //Base routes |
77 | | - const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60*1000}) |
| 78 | + const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60*1000, headers:false}) |
78 | 79 | const metadata = Object.fromEntries(Object.entries(conf.metadata.plugins) |
79 | | - .filter(([key]) => !["base", "core"].includes(key)) |
80 | | - .map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "categorie", "web", "supports"].includes(key)))])) |
81 | | - const enabled = Object.entries(metadata).map(([name]) => ({name, enabled:plugins[name]?.enabled ?? false})) |
| 80 | + .map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "categorie", "web", "supports"].includes(key)))]) |
| 81 | + .map(([key, value]) => [key, key === "core" ? {...value, web:Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value]))} : value])) |
| 82 | + const enabled = Object.entries(metadata).filter(([_name, {categorie}]) => categorie !== "core").map(([name]) => ({name, enabled:plugins[name]?.enabled ?? false})) |
82 | 83 | const templates = Object.entries(Templates).map(([name]) => ({name, enabled:(conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false})) |
83 | 84 | const actions = {flush:new Map()} |
84 | 85 | let requests = {limit:0, used:0, remaining:0, reset:NaN} |
|
119 | 120 | app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`)) |
120 | 121 | //Meta |
121 | 122 | app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version)) |
122 | | - app.get("/.requests", limiter, async(req, res) => res.status(200).json(requests)) |
| 123 | + app.get("/.requests", limiter, (req, res) => res.status(200).json(requests)) |
| 124 | + app.get("/.hosted", limiter, (req, res) => res.status(200).json(conf.settings.hosted || null)) |
123 | 125 | //Cache |
124 | | - app.get("/.uncache", limiter, async(req, res) => { |
| 126 | + app.get("/.uncache", limiter, (req, res) => { |
125 | 127 | const {token, user} = req.query |
126 | 128 | if (token) { |
127 | 129 | if (actions.flush.has(token)) { |
128 | 130 | console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`) |
129 | 131 | cache.del(actions.flush.get(token)) |
130 | 132 | return res.sendStatus(200) |
131 | 133 | } |
132 | | - return res.sendStatus(404) |
| 134 | + return res.sendStatus(400) |
133 | 135 | } |
134 | 136 | { |
135 | 137 | const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}` |
|
139 | 141 | }) |
140 | 142 |
|
141 | 143 | //Metrics |
| 144 | + const pending = new Set() |
| 145 | + app.get("/:login/:repository", ...middlewares, (req, res) => res.redirect(`/${req.params.login}?template=repository&repo=${req.params.repository}&${Object.entries(req.query).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&")}`)) |
142 | 146 | app.get("/:login", ...middlewares, async(req, res) => { |
143 | 147 | //Request params |
144 | 148 | const login = req.params.login?.replace(/[\n\r]/g, "") |
145 | 149 | if ((restricted.length)&&(!restricted.includes(login))) { |
146 | | - console.debug(`metrics/app/${login} > 403 (not in whitelisted users)`) |
147 | | - return res.sendStatus(403) |
| 150 | + console.debug(`metrics/app/${login} > 403 (not in allowed users)`) |
| 151 | + return res.status(403).send(`Forbidden: "${login}" not in allowed users`) |
148 | 152 | } |
149 | 153 | //Read cached data if possible |
150 | 154 | if ((!debug)&&(cached)&&(cache.get(login))) { |
151 | 155 | const {rendered, mime} = cache.get(login) |
152 | 156 | res.header("Content-Type", mime) |
153 | | - res.send(rendered) |
154 | | - return |
| 157 | + return res.send(rendered) |
155 | 158 | } |
156 | 159 | //Maximum simultaneous users |
157 | 160 | if ((maxusers)&&(cache.size()+1 > maxusers)) { |
158 | 161 | console.debug(`metrics/app/${login} > 503 (maximum users reached)`) |
159 | | - return res.sendStatus(503) |
| 162 | + return res.status(503).send("Service Unavailable: maximum users reached, only cached metrics are available") |
160 | 163 | } |
| 164 | + //Prevent multiples requests |
| 165 | + if (pending.has(login)) { |
| 166 | + console.debug(`metrics/app/${login} > 409 (multiple requests)`) |
| 167 | + return res.status(409).send(`Conflict: a request for "${login}" is being process, retry later`) |
| 168 | + } |
| 169 | + pending.add(login) |
161 | 170 |
|
162 | 171 | //Compute rendering |
163 | 172 | try { |
|
175 | 184 | cache.put(login, {rendered, mime}, cached) |
176 | 185 | //Send response |
177 | 186 | res.header("Content-Type", mime) |
178 | | - res.send(rendered) |
| 187 | + return res.send(rendered) |
179 | 188 | } |
180 | 189 | //Internal error |
181 | 190 | catch (error) { |
182 | 191 | //Not found user |
183 | 192 | if ((error instanceof Error)&&(/^user not found$/.test(error.message))) { |
184 | 193 | console.debug(`metrics/app/${login} > 404 (user/organization not found)`) |
185 | | - return res.sendStatus(404) |
| 194 | + return res.status(404).send(`Not found: unknown user or organization "${login}"`) |
186 | 195 | } |
187 | 196 | //Invalid template |
188 | 197 | if ((error instanceof Error)&&(/^unsupported template$/.test(error.message))) { |
189 | 198 | console.debug(`metrics/app/${login} > 400 (bad request)`) |
190 | | - return res.sendStatus(400) |
| 199 | + return res.status(400).send(`Bad request: unsupported template "${req.query.template}"`) |
191 | 200 | } |
192 | 201 | //General error |
193 | 202 | console.error(error) |
194 | | - res.sendStatus(500) |
| 203 | + return res.status(500).send("Internal Server Error: failed to process metrics correctly") |
| 204 | + } |
| 205 | + //After rendering |
| 206 | + finally { |
| 207 | + pending.delete(login) |
195 | 208 | } |
196 | 209 | }) |
197 | 210 |
|
|
0 commit comments