-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
72f58c9
commit 52c1761
Showing
16 changed files
with
321 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,4 +127,5 @@ dist | |
.pnp.* | ||
|
||
.vscode | ||
yarn.lock | ||
yarn.lock | ||
*.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,103 @@ | ||
# captcha-microservice | ||
REST based captcha generating microservice | ||
|
||
Consists of the following types: | ||
- Image To Text | ||
|
||
![Math Challenge](./assets/image.png) | ||
|
||
(Ans: dfl22V) | ||
|
||
- Math Challenge | ||
|
||
![Math Challenge](./assets/math.png) | ||
|
||
(Ans: 64) | ||
|
||
## Workflow | ||
- Request captcha using one of the methods | ||
- Response body is the raw body of the captcha (image/sound) and Captcha Id will be provided in `X-Captcha-Session-Id` header | ||
- Answer can be verified using the `/verify` endpoint providing a JWT token | ||
- JWT token can be used to confirm authenticity on server side or client side with `/verify` endpoint | ||
## **Methods** | ||
|
||
## Random Captcha | ||
## `/captcha/captcha` | ||
|
||
Get a random captcha from all the methods supported | ||
|
||
Header: `X-Captcha-Session-Id` - contains the captcha id | ||
|
||
Body is raw content of the captcha (image/sound) | ||
|
||
## Image Captcha | ||
## `/captcha/imageCaptcha` | ||
|
||
Get image captcha | ||
|
||
Header: `X-Captcha-Session-Id` - contains the captcha id | ||
|
||
Body is raw image in png format | ||
|
||
![Image Captcha](./assets/image.png) | ||
|
||
## Math Captcha | ||
## `/captcha/mathCaptcha` | ||
|
||
Get math challenge which gives a math equation | ||
|
||
Header: `X-Captcha-Session-Id` - contains the captcha id | ||
|
||
Body is raw image in png format | ||
|
||
![Math Challenge](./assets/math.png) | ||
|
||
## Verify | ||
## `/verify?sessionId=<captchaId>&solution=<solution>` | ||
|
||
Verify captcha answer. A JWT token is returned if the answer is correct (`200` status code), else `401` status code is returned | ||
|
||
## Validate | ||
## `/validate?sessionId=<captchaId>&token=<token>` | ||
|
||
Can be used server side to validate the verification | ||
|
||
# **Usage** | ||
|
||
## As a service | ||
## *Environment Variables* | ||
``` | ||
PORT= Application port | ||
JWT_SECRET= JWT signing secret | ||
HOST= Hostname to use in JWT token | ||
PROMETHEUS_SECRET= Prometheus secret (Use with header | ||
Authorization: Bearer <PROMETHEUS_SECRET>) | ||
RATE_LIMIT_DURATION= Rate limit duration in seconds | ||
RATE_LIMIT_POINTS= Rate limit points for given duration | ||
REDIS_HOST= Redis host | ||
REDIS_PORT= Redis port | ||
REDIS_PASSWORD= Redis password | ||
REDIS_DB= Redis database | ||
REDIS_USERNAME= Redis username | ||
CAPTCHA_TIMEOUT= Captcha timeout in seconds | ||
``` | ||
|
||
## Docker | ||
`docker run -d --name email-microservice --env-file app.env -p 5555:5555 crossphoton/email-microservice:v1.0.0` | ||
|
||
## Kubernetes | ||
> TODO | ||
## Locally | ||
1. Clone repository | ||
2. Run `npm install` | ||
3. Run `dotenv -e app.env -- npm start` | ||
|
||
## Additional Parts | ||
- **Prometheus** : `/metrics` endpoint with proper authorization can be used to collect metrics | ||
- **Logging** : is done using [winston](https://www.npmjs.com/package/winston). | ||
- **Shutdown management** : done using [lightship](https://www.npmjs.com/package/lightship) | ||
|
||
|
||
## License | ||
MIT License |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,68 @@ | ||
const express = require("express"); | ||
const app = express(); | ||
const { image_text, verify } = require("./src"); | ||
const logger = require("./src/logging"); | ||
const compression = require("compression"); | ||
const { image_text, math_text, verify } = require("./src"); | ||
const { logger, middleware: logMiddleWare } = require("./src/logging"); | ||
const { verifyJWTToken } = require("./src/jwt"); | ||
|
||
app.disable("x-powered-by"); | ||
app.use(express.json()); | ||
app.use(express.urlencoded({ extended: true })); | ||
app.enable("trust proxy"); | ||
app.use(logMiddleWare); | ||
app.use( | ||
compression({ | ||
filter: shouldCompress, | ||
level: 9, | ||
}) | ||
); | ||
|
||
app.get("/captcha/imageCaptcha.png", async (_req, res) => { | ||
var captcha; | ||
try { | ||
captcha = await image_text.generate(); | ||
} catch (error) { | ||
return res.sendStatus(500); | ||
} | ||
|
||
res.setHeader("Content-Type", "image/png"); | ||
res.setHeader("X-Captcha-Session-Id", captcha.sessionId); | ||
function shouldCompress(req, res) { | ||
if (req.headers["x-no-compression"]) return false; | ||
return compression.filter(req, res); | ||
} | ||
|
||
res.send(captcha.captcha); | ||
}); | ||
|
||
app.get("/verify", async (req, res) => { | ||
app.get("/verify", (req, res) => { | ||
const { sessionId, solution } = req.query; | ||
if (!sessionId || !solution) { | ||
return res.sendStatus(400); | ||
} | ||
try { | ||
const valid = await verify(sessionId, solution); | ||
valid ? res.send(valid) : res.sendStatus(401); | ||
verify(sessionId, solution).then((valid) => { | ||
valid ? res.send(valid) : res.sendStatus(401); | ||
}); | ||
} catch (err) { | ||
logger.error(err); | ||
logger.error(err.message); | ||
res.send(null).statusCode(401); | ||
} | ||
}); | ||
|
||
app.get("/validate", async (req, res) => { | ||
app.get("/validate", (req, res) => { | ||
const { sessionId, token } = req.query; | ||
if (!token || !sessionId) { | ||
return res.sendStatus(400); | ||
} | ||
try { | ||
const valid = await verifyJWTToken(token); | ||
valid && valid.sessionId === sessionId | ||
? res.sendStatus(200) | ||
: res.sendStatus(401); | ||
verifyJWTToken(token).then((valid) => { | ||
valid && valid.sessionId === sessionId | ||
? res.sendStatus(200) | ||
: res.sendStatus(401); | ||
}); | ||
} catch (err) { | ||
logger.error(err.message); | ||
res.sendStatus(500); | ||
} | ||
}); | ||
|
||
app.get("/captcha/imageCaptcha", image_text.middleware); | ||
|
||
app.get("/captcha/mathCaptcha", math_text.middleware); | ||
|
||
// Pick a random captcha method | ||
const randomCaptcha = (req, res, next) => { | ||
const map = [image_text, math_text]; | ||
return map[Math.floor(Math.random() * map.length)].middleware(req, res, next); | ||
}; | ||
|
||
app.get("/captcha/captcha", randomCaptcha); | ||
|
||
module.exports = app; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,34 @@ | ||
const expressApp = require("./api"); | ||
const logger = require("./src/logging"); | ||
const helmet = require("helmet"); | ||
const { createLightship } = require("lightship"); | ||
const rateLimiter = require("./src/rateLimit"); | ||
const { logger } = require("./src/logging"); | ||
const prometheus = require("./src/prometheus"); | ||
const PORT = process.env.PORT || 5000; | ||
|
||
// Add prometheus middleware | ||
prometheus(expressApp); | ||
// Add middleware | ||
prometheus(expressApp); // Add prometheus middleware | ||
expressApp.use(helmet()); // Add helmet middleware | ||
expressApp.use(rateLimiter); // Add rate limiter middleware | ||
|
||
// Health check | ||
expressApp.get("/healthz", (_req, res) => { | ||
res.send("OK"); | ||
}); | ||
|
||
// Start the server | ||
expressApp.listen(PORT, () => logger.info(`listening on ${PORT}`)); | ||
const server = expressApp | ||
.listen(PORT, () => { | ||
logger.info(`listening on ${PORT}`); | ||
lightship.signalReady(); | ||
}) | ||
.on("error", () => { | ||
logger.info(`shutting down server`); | ||
lightship.shutdown(); | ||
}); | ||
|
||
const lightship = createLightship(); | ||
|
||
lightship.registerShutdownHandler(() => { | ||
server.close(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
var captchagen = require("captchagen"); | ||
const { store } = require("./store"); | ||
|
||
const generate = async () => { | ||
const captcha = captchagen.create(); | ||
captcha.generate(); | ||
const sessionId = await store(captcha.text()); | ||
|
||
return { | ||
/** @type {Buffer} */ captcha: captcha.buffer(), | ||
/** @type {string} */ sessionId, | ||
}; | ||
}; | ||
|
||
const middleware = (_req, res) => { | ||
try { | ||
generate().then((captcha) => { | ||
res.setHeader("Content-Type", "image/png"); | ||
res.setHeader("X-Captcha-Session-Id", captcha.sessionId); | ||
|
||
res.send(captcha.captcha); | ||
}); | ||
} catch (error) { | ||
logger.error(error.message); | ||
res.sendStatus(500); | ||
} | ||
}; | ||
|
||
module.exports = { middleware }; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
module.exports = { | ||
image_text: require("./imageToText"), | ||
image_text: require("./imageCaptcha"), | ||
math_text: require("./mathCaptcha"), | ||
verify: require("./store").verify, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.