Skip to content

Commit

Permalink
v1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
crossphoton committed Dec 18, 2021
1 parent 72f58c9 commit 52c1761
Show file tree
Hide file tree
Showing 16 changed files with 321 additions and 50 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,5 @@ dist
.pnp.*

.vscode
yarn.lock
yarn.lock
*.env
101 changes: 101 additions & 0 deletions README.md
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
66 changes: 41 additions & 25 deletions api.js
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;
Binary file added assets/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/math.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@
"homepage": "https://github.com/crossphoton/captcha-microservice#readme",
"dependencies": {
"captchagen": "^1.2.0",
"compression": "^1.7.4",
"express": "^4.17.2",
"express-prometheus-middleware": "^1.2.0",
"express-winston": "^4.2.0",
"helmet": "^4.6.0",
"ioredis": "^4.28.2",
"jsonwebtoken": "^8.5.1",
"lightship": "^6.8.0",
"prom-client": "^14.0.1",
"rate-limiter-flexible": "^2.3.6",
"text-to-image": "^4.1.1",
"uuid": "^8.3.2",
"winston": "^3.3.3"
},
Expand Down
27 changes: 23 additions & 4 deletions server.js
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();
});
29 changes: 29 additions & 0 deletions src/imageCaptcha.js
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 };
15 changes: 0 additions & 15 deletions src/imageToText.js

This file was deleted.

3 changes: 2 additions & 1 deletion src/index.js
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,
};
4 changes: 3 additions & 1 deletion src/jwt.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const jwt = require("jsonwebtoken");

// create a token from a payload
function createJWTToken(payload) {
return jwt.sign(payload, process.env.JWT_SECRET || "secret", {
expiresIn: "30m",
expiresIn: Number(process.env.CAPTCHA_TIMEOUT) || "30m",
header: {
alg: "HS256",
typ: "JWT",
Expand All @@ -13,6 +14,7 @@ function createJWTToken(payload) {
});
}

// verify a token
function verifyJWTToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET || "secret", {
Expand Down
6 changes: 4 additions & 2 deletions src/logging.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const winston = require("winston");
const expressWinston = require("express-winston");

const consoleTransport = new winston.transports.Console({
format: winston.format.combine(
Expand All @@ -9,9 +10,10 @@ const consoleTransport = new winston.transports.Console({

/** @type {winston.LoggerOptions} */ const myWinstonOptions = {
transports: [consoleTransport],
defaultMeta: { service: "my-service-name" },
defaultMeta: { service: "captcha-service" },
};

const logger = winston.createLogger(myWinstonOptions);
const middleware = expressWinston.logger(myWinstonOptions);

module.exports = logger;
module.exports = { logger, middleware };
Loading

0 comments on commit 52c1761

Please sign in to comment.