Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,31 @@ Built with **Node.js** and **Express**.

## 📚 API Reference

| Method | Path | Description |
| ------ | ------------------------ | ------------------------------------------------------------------------------------ |
| `ALL` | `/base64/encode` | Encodes a string value to Base64 format. |
| `ALL` | `/base64/decode` | Decodes a Base64 string to its original format. |
| `ALL` | `/error/timeout/` | Simulates a timeout by never sending a response. |
| `ALL` | `/error/network/` | Simulates a network error by closing the connection. |
| `ALL` | `/error/malformed-json/` | Returns malformed JSON response. |
| `ALL` | `/error/error/` | Throws an unhandled exception to trigger Express error handler. |
| `ALL` | `/mirror/` | Returns the request body as a response. |
| `ALL` | `/redirect/` | Returns a redirect response based on the `status` and `url` of the query parameters. |
| `ALL` | `/request/` | Return a structured JSON dump of the incoming request. |
| `ALL` | `/shutdown/` | GTriggers a shutdown of the server. Requires `ENABLE_SHUTDOWN=true`. |
| `ALL` | `/status/{status}` | Respond with arbitrary HTTP status. |
| `ALL` | `/uuid/` | Generate and return a random UUID (version 4). |
| Method | Path | Description |
| ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `ALL` | `/base64/encode` | Encodes a string value to Base64 format. |
| `ALL` | `/base64/decode` | Decodes a Base64 string to its original format. |
| `ALL` | `/basic-auth/` | Tests HTTP Basic Authentication by comparing credentials from Authorization header against query parameters. |
| `ALL` | `/error/timeout/` | Simulates a timeout by never sending a response. |
| `ALL` | `/error/network/` | Simulates a network error by closing the connection. |
| `ALL` | `/error/malformed-json/` | Returns malformed JSON response. |
| `ALL` | `/error/error/` | Throws an unhandled exception to trigger Express error handler. |
| `ALL` | `/mirror/` | Returns the request body as a response. |
| `ALL` | `/redirect/` | Returns a redirect response based on the `status` and `url` of the query parameters. |
| `ALL` | `/request/` | Return a structured JSON dump of the incoming request. |
| `ALL` | `/shutdown/` | GTriggers a shutdown of the server. Requires `ENABLE_SHUTDOWN=true`. |
| `ALL` | `/status/{status}` | Respond with arbitrary HTTP status. |
| `ALL` | `/uuid/` | Generate and return a random UUID (version 4). |

### Query Parameters

| Name | Type | Default | Description |
| -------- | ------ | ------- | ----------------------------------------------------------- |
| `delay` | Number | `0` | Delays the response by the specified value in milliseconds. |
| `status` | Number | — | HTTP status code for `/redirect/` or `/status/{status}`. |
| `url` | String | — | Target URL for `/redirect/`. |
| Name | Type | Default | Description |
| ---------- | ------ | ------- | ----------------------------------------------------------- |
| `delay` | Number | `0` | Delays the response by the specified value in milliseconds. |
| `status` | Number | — | HTTP status code for `/redirect/` or `/status/{status}`. |
| `url` | String | — | Target URL for `/redirect/`. |
| `user` | String | — | Expected username for `/basic-auth/` (required). |
| `password` | String | — | Expected password for `/basic-auth/` (required). |

---

Expand Down
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cors from 'cors';
import { delayMiddleware } from './middlewares/delay.js';
import { loggerMiddleware } from './middlewares/logger.js';
import { base64Router } from './routes/base64.js';
import { basicAuthRouter } from './routes/basic-auth.js';
import { errorRouter } from './routes/error.js';
import { indexRouter } from './routes/index.js';
import { mirrorRouter } from './routes/mirror.js';
Expand Down Expand Up @@ -35,6 +36,7 @@ app.use(delayMiddleware);

app.use('/', indexRouter);
app.use('/base64', base64Router);
app.use('/basic-auth', basicAuthRouter);
app.use('/error', errorRouter);
app.use('/mirror', mirrorRouter);
app.use('/redirect', redirectRouter);
Expand Down
58 changes: 58 additions & 0 deletions src/routes/basic-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Router } from 'express';
import { HttpStatusCodes } from '../utils/http.js';

const basicAuthRouter = Router();

basicAuthRouter.all('/', (req, res) => {
const { user, password } = req.query;

// Validate that both user and password are provided and non-empty
if (
!user ||
!password ||
typeof user !== 'string' ||
typeof password !== 'string' ||
user.trim() === '' ||
password.trim() === ''
Comment on lines +7 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The user and password are being extracted directly from the query parameters. This is not secure as it exposes sensitive information in the URL. It's recommended to use HTTP Basic Authentication which sends credentials in the Authorization header encoded with Base64. The existing code already handles this case starting from line 26, so the extraction of credentials from query parameters can be removed.

-  const { user, password } = req.query;
-
-  // Validate that both user and password are provided and non-empty
-  if (
-    !user ||
-    !password ||
-    typeof user !== 'string' ||
-    typeof password !== 'string' ||
-    user.trim() === '' ||
-    password.trim() === ''
-  ) {
-    res.status(HttpStatusCodes.BAD_REQUEST).json({
-      error: {
-        message: 'Missing user or password query parameter',
-      },
-    });
-    return;
-  }

) {
res.status(HttpStatusCodes.BAD_REQUEST).json({
error: {
message: 'Missing user or password query parameter',
},
});
return;
}

const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
res.status(HttpStatusCodes.UNAUTHORIZED).json({
authenticated: false,
message: 'Authentication required',
});
return;
}
Comment on lines +26 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

This block of code is repeated again in lines 48-50. To follow the DRY principle (Don't Repeat Yourself), you could extract this into a separate function.

+ const sendUnauthorizedResponse = (res) => {
+   res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
+   res.status(HttpStatusCodes.UNAUTHORIZED).json({
+     authenticated: false,
+     message: 'Authentication required',
+   });
+ }

-   if (!authHeader || !authHeader.startsWith('Basic ')) {
-     res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
-     res.status(HttpStatusCodes.UNAUTHORIZED).json({
-       authenticated: false,
-       message: 'Authentication required',
-     });
-     return;
-   }
+   if (!authHeader || !authHeader.startsWith('Basic ')) {
+     sendUnauthorizedResponse(res);
+     return;
+   }


const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials as string, 'base64').toString('utf-8');
const [providedUser, providedPassword] = credentials.split(':');

if (providedUser === user && providedPassword === password) {
res.status(HttpStatusCodes.OK).json({
authenticated: true,
message: 'Authentication successful',
user,
password,
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Returning the user's password in the response is a security risk, even if it's successful authentication. Passwords should never be returned in responses. Remove the password from the response.

-       user,
-       password,

});
} else {
res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
res.status(HttpStatusCodes.UNAUTHORIZED).json({
authenticated: false,
Comment on lines +48 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Use the sendUnauthorizedResponse function here as well.

-   } else {
-     res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
-     res.status(HttpStatusCodes.UNAUTHORIZED).json({
-       authenticated: false,
-       message: 'Authentication failed',
-       user: providedUser,
-       password: providedPassword,
-     });
-   }
+   } else {
+     sendUnauthorizedResponse(res);
+   }

message: 'Authentication failed',
user: providedUser,
password: providedPassword,
Comment on lines +52 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Similarly, do not return the provided password when authentication fails.

-       user: providedUser,
-       password: providedPassword,

});
}
});

export { basicAuthRouter };
Comment on lines +1 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The code is generally well-written, but there are a few areas that could be improved for better security and maintainability.

134 changes: 134 additions & 0 deletions tests/api/api-test-collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,140 @@
}
]
},
{
"name": "Basic Auth - Successful Authentication",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Basic dGVzdHVzZXI6dGVzdHBhc3M="
}
],
"url": {
"raw": "{{baseUrl}}/basic-auth?user=testuser&password=testpass",
"host": ["{{baseUrl}}"],
"path": ["basic-auth"],
"query": [
{
"key": "user",
"value": "testuser"
},
{
"key": "password",
"value": "testpass"
}
]
}
},
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test('Authentication is successful', function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property('authenticated', true);",
" pm.expect(jsonData).to.have.property('user', 'testuser');",
" pm.expect(jsonData).to.have.property('password', 'testpass');",
" pm.expect(jsonData).to.have.property('message', 'Authentication successful');",
"});"
]
}
}
]
},
{
"name": "Basic Auth - Failed Authentication",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Basic d3JvbmdUZXN0Ondyb25nUGFzcw=="
}
],
"url": {
"raw": "{{baseUrl}}/basic-auth?user=expected&password=expected",
"host": ["{{baseUrl}}"],
"path": ["basic-auth"],
"query": [
{
"key": "user",
"value": "expected"
},
{
"key": "password",
"value": "expected"
}
]
}
},
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 401', function () {",
" pm.response.to.have.status(401);",
"});",
"",
"pm.test('Authentication failed response', function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property('authenticated', false);",
" pm.expect(jsonData).to.have.property('message', 'Authentication failed');",
" pm.expect(jsonData).to.have.property('user', 'wrongTest');",
" pm.expect(jsonData).to.have.property('password', 'wrongPass');",
"});"
]
}
}
]
},
{
"name": "Basic Auth - Empty Parameters",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/basic-auth?user=&password=",
"host": ["{{baseUrl}}"],
"path": ["basic-auth"],
"query": [
{
"key": "user",
"value": ""
},
{
"key": "password",
"value": ""
}
]
}
},
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 400', function () {",
" pm.response.to.have.status(400);",
"});",
"",
"pm.test('Empty parameter error', function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property('error');",
" pm.expect(jsonData.error).to.have.property('message', 'Missing user or password query parameter');",
"});"
]
}
}
]
},
{
"name": "Mirror Route",
"request": {
Expand Down
Loading
Loading