Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validation of HTTP response codes #61

Merged
merged 5 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 5 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2.1
parameters:
node-version:
type: string
default: "16.13.2"
default: "18.13.0"
orbs:
node: circleci/[email protected]
slack: circleci/[email protected]
Expand Down Expand Up @@ -72,7 +72,7 @@ commands:
jobs:
test:
docker:
- image: cimg/node:16.16.0
- image: cimg/node:18.13.0
steps:
- checkout
- node/install:
Expand All @@ -88,7 +88,7 @@ jobs:
command: npm test
build:
docker:
- image: cimg/node:16.16.0
- image: cimg/node:18.13.0
user: root
steps:
- checkout
Expand Down Expand Up @@ -118,6 +118,8 @@ workflows:
jobs:
- build:
name: "Build and publish docker image"
context:
- componentspusher
filters:
branches:
ignore: /.*/
Expand Down
33 changes: 10 additions & 23 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use strict';

const ERROR = 'error';
const WARN = 'warn';
const ALWAYS = 'always';
Expand All @@ -17,17 +15,18 @@ module.exports = {
},
extends: 'airbnb-base',
rules: {
indent: [
'indent': [
ERROR,
2,
{
SwitchCase: 1,
},
],
'import/no-extraneous-dependencies': ['error', { devDependencies: ['spec/**/*', 'spec-integration/**/*'] }],
'max-len': ['error', { code: 180 }],
'linebreak-style': ERROR,
quotes: [WARN, 'single'],
semi: [ERROR, ALWAYS],
'quotes': [WARN, 'single'],
'semi': [ERROR, ALWAYS],
'func-names': 0,
'no-shadow': 0,
'no-empty': ERROR,
Expand Down Expand Up @@ -59,30 +58,18 @@ module.exports = {
'space-in-parens': ERROR,
'comma-dangle': 0,
'no-trailing-spaces': ERROR,
yoda: ERROR,
camelcase: [
ERROR,
{
properties: 'never',
},
],
'yoda': ERROR,
'camelcase': 0,
'new-cap': [
WARN,
{
capIsNewExceptions: ['Q'],
},
],
'comma-style': ERROR,
curly: ERROR,
'curly': ERROR,
'object-curly-spacing': [WARN, ALWAYS],
'object-curly-newline': [
ERROR,
{
ObjectPattern: {
minProperties: 5,
},
},
],
'object-curly-newline': 0,
'object-property-newline': ERROR,
'template-curly-spacing': ERROR,
'dot-notation': ERROR,
Expand All @@ -104,8 +91,8 @@ module.exports = {
'no-unreachable': ERROR,
'no-sparse-arrays': ERROR,
'array-callback-return': ERROR,
strict: [WARN, 'global'],
eqeqeq: ERROR,
'strict': [WARN, 'global'],
'eqeqeq': ERROR,
'no-use-before-define': WARN,
'no-undef': ERROR,
'no-unused-vars': WARN,
Expand Down
10 changes: 10 additions & 0 deletions .grype-ignore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ignore:
- vulnerability: CVE-2023-2650
package:
name: libssl3
version: 3.1.0-r4

- vulnerability: CVE-2023-2650
package:
name: libcrypto3
version: 3.1.0-r4
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.3.0 (June 30, 2023)
* Added `Response Status Code` validation
* Updated README
* Get rid of vulnerabilities and unused libraries in dependencies

## 1.2.7 (November 04, 2022)
* Updated the sailor version to 2.7.1

Expand Down
60 changes: 32 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
# request-reply-component
# HTTP Reply Component

## General information
## Table of Contents

* [Description](#description)
* [Actions](#actions)
* [Reply](#reply)
* [Reply With Attachment](#reply-with-attachment)

### Description

The component replies with messages to the client requested a webhook.

This component takes the incoming message body and applies the configured JSONata tranformation on it, if present, and return a message back to the client requested a webhook of a given flow.

### Environment variables

No required environment variables

## Credentials

This component requires no authentication.
This component takes the incoming message body and applies the configured JSONata transformation on it, if present, and return a message back to the client requested a webhook of a given flow.

## Actions

### Reply

List of Expected Config fields:
#### Configuration Fields
* **Custom HTTP Headers** - (string, optional): Provides with possibility to set additional headers separated by comma (e.g `Content-Language, User-Agent`)

#### Input Metadata
* **Content Type (Defaults to 'application/json')** - (string, optional, defaults to `application/json`): Header value tells the client what the content type of the returned content actually is.
* **Response Body** - (string/Object, required): Body to send as the response
* **Response Status Code** - (number, optional, defaults to `200`): Integer number between `200` and `999` (ore info about status codes in [rfc7231](https://datatracker.ietf.org/doc/html/rfc7231#section-6) standart)

If provided `Custom HTTP Headers` there will be additional field:
* **customHeaders**, contains:
* **Header <header name provided in "Custom HTTP Headers">** - you can provide value to your custom header here

- `Custom HTTP Headers` - not required, provides with possibility to set additional headers (e.g `Content-Language`)
- `Content Type (Defaults to 'application/json')` - not required, header value tells the client what the content type of the returned content actually is. The action supports only types with `text/...` or `application/...` in the beginning of the header name.
- `Response Body` - required, supports JSONata expressions. Max length of a JSONata expression is 1000 symbols.
- `Response Status Code` - not required,user may specify response code, if needed

![image](https://user-images.githubusercontent.com/36419533/115863191-cb8e6800-a43d-11eb-83f2-c859b854db44.png)
#### Output Metadata
Same as `Input Metadata`

### Reply With Attachment

List of Expected Config fields:
#### Configuration Fields
* **Custom HTTP Headers** - (string, optional): Provides with possibility to set additional headers separated by comma (e.g `Content-Language, User-Agent`)

- `Custom HTTP Headers` - non-required, provides with possibility to set additional headers (e.g `Content-Language`)
- `Content Type (Defaults to 'application/json')` - the `non-required` header value tells the client what the content type of the returned content actually is
- `Attachment URL` - required, supported are attachments from `stewart` microservice by URL and external attachments URL, Max field length is 1000 symbols.
- `Response Status Code` - not required, user may specify response code, if needed
#### Input Metadata
* **Content Type (Defaults to 'application/json')** - (string, optional, defaults to `application/json`): Header value tells the client what the content type of the returned content actually is.
* **Attachment URL** - (string, required): Link to file (on platform or external) that will be used as response
* **Response Status Code** - (number, optional, defaults to `200`): Integer number between `200` and `999` (ore info about status codes in [rfc7231](https://datatracker.ietf.org/doc/html/rfc7231#section-6) standart)

![image](https://user-images.githubusercontent.com/36419533/115863277-e8c33680-a43d-11eb-9819-667c369b141c.png)
If provided `Custom HTTP Headers` there will be additional field:
* **customHeaders**, contains:
* **Header <header name provided in "Custom HTTP Headers">** - you can provide value to your custom header here

*Note:* Please be advised that the action does not actually write an attachment when the sample is retrieved.

## Known limitations
No.
#### Output Metadata
Same as `Input Metadata`

## Documentation links
More information and some examples of JSONata expressions can be found [here](http://docs.jsonata.org/).
10 changes: 5 additions & 5 deletions component.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"title": "HTTP Reply",
"service": "request-reply",
"description": "Used to reply to HTTP webhooks",
"version": "1.2.7",
"version": "1.3.0-dev.7",
"actions": {
"reply": {
"title": "Reply",
"main": "./reply.js",
"main": "./lib/actions/replyWithBody.js",
"fields": {
"customHeaders": {
"label": "Custom HTTP Headers",
Expand Down Expand Up @@ -43,12 +43,12 @@
}
}
},
"out": {}
"out": "./lib/schemas/actions/replyWithBody.out.json"
}
},
"replyWithAttachment": {
"title": "Reply With Attachment",
"main": "./replyWithAttachment.js",
"main": "./lib/actions/replyWithAttachment.js",
"fields": {
"customHeaders": {
"label": "Custom HTTP Headers",
Expand Down Expand Up @@ -85,7 +85,7 @@
}
}
},
"out": {}
"out": "./lib/schemas/actions/replyWithAttachment.out.json"
}
}
}
Expand Down
86 changes: 86 additions & 0 deletions lib/ReplyClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { messages } = require('elasticio-node');
const commons = require('@elastic.io/component-commons-library');
const utils = require('./utils');

const DEFAULT_CONTENT_TYPE = 'application/json';
const DEFAULT_STATUS_CODE = 200;
const HEADER_CONTENT_TYPE = 'Content-Type';
const HEADER_ROUTING_KEY = 'X-EIO-Routing-Key';
const HEADER_STATUS_CODE = 'x-eio-status-code';
const HEADER_OBJECT_STORAGE = 'x-ipaas-object-storage-id';

class ReplyClient {
constructor(context, msg) {
const {
contentType = DEFAULT_CONTENT_TYPE,
statusCode = DEFAULT_STATUS_CODE,
customHeaders = {},
} = msg.body;

const {
reply_to
} = msg.headers;

if (utils.isNumberNaN(statusCode) || Number(statusCode) < 200 || Number(statusCode) > 999) {
const errorMessage = `"Response Status Code" must be valid number between 200 and 999, got ${this.statusCode}`;
utils.logAndThrowError(context.logger, errorMessage);
}

this.reply_to = reply_to;
this.contentType = contentType;
this.statusCode = Number(statusCode);
this.customHeaders = customHeaders;
this.replyBody = {};
this.logger = context.logger;
this.context = context;
this.msg = msg;
}

prepareReply() {
const reply = messages.newMessageWithBody(this.replyBody);
reply.headers[HEADER_ROUTING_KEY] = this.reply_to;
reply.headers[HEADER_CONTENT_TYPE] = this.contentType;
reply.headers[HEADER_STATUS_CODE] = this.statusCode;
if (this.objectId) reply.headers[HEADER_OBJECT_STORAGE] = this.objectId;
Object.assign(reply.headers, this.customHeaders);
this.reply = reply;
}

async emitReply() {
if (!this.reply_to) return;
this.prepareReply();
this.logger.info('Sending reply');
await this.context.emit('data', this.reply);
}

async emitData() {
this.logger.info('Emitting data...');
await this.context.emit('data', messages.newMessageWithBody(this.msg.body));
}

async replyWithBody() {
this.replyBody = this.msg.body.responseBody;
if (!this.msg.body.responseBody) {
this.logger.warn('Field "Response Body" on the message body was empty, we will reply with the whole message body');
this.replyBody = this.msg.body;
}
await this.emitReply();
await this.emitData();
}

async replyWithAttachment() {
const { responseUrl } = this.msg.body;
if (!responseUrl) {
const errorMessage = '"Attachment URL" field can not be empty!';
utils.logAndThrowError(this.logger, errorMessage);
}
const attachmentProcessor = new commons.AttachmentProcessor(utils.getUserAgent('component-commons-library'), this.msg.id);
const objectStorage = utils.getObjectStorage();
const getDataStream = async () => (await attachmentProcessor.getAttachment(responseUrl, 'stream')).data;
this.objectId = await objectStorage.add(getDataStream);
await this.emitReply();
await this.emitData();
}
}

module.exports.ReplyClient = ReplyClient;
8 changes: 8 additions & 0 deletions lib/actions/replyWithAttachment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { ReplyClient } = require('../ReplyClient');

exports.process = async function (msg) {
this.logger.info('"Reply With Attachment" action started');
const replyClient = new ReplyClient(this, msg);
await replyClient.replyWithAttachment();
this.logger.info('Processing "Reply With Attachment" action finished successfully');
};
8 changes: 8 additions & 0 deletions lib/actions/replyWithBody.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { ReplyClient } = require('../ReplyClient');

exports.process = async function (msg) {
this.logger.info('"Reply" action started');
const replyClient = new ReplyClient(this, msg);
await replyClient.replyWithBody();
this.logger.info('Processing "Reply" action finished successfully');
};
19 changes: 19 additions & 0 deletions lib/schemas/actions/replyWithAttachment.out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "object",
"properties": {
"statusCode": {
"type": "integer"
},
"responseUrl": {
"type": "string"
},
"contentType": {
"type": "string"
}
},
"required": [
"statusCode",
"responseUrl",
"contentType"
]
}
19 changes: 19 additions & 0 deletions lib/schemas/actions/replyWithBody.out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "object",
"properties": {
"contentType": {
"type": "string"
},
"responseBody": {
"type": "object"
},
"statusCode": {
"type": "integer"
}
},
"required": [
"contentType",
"responseBody",
"statusCode"
]
}
Loading