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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
hs_err_pid*
replay_pid*

##############################
## Javascript
##############################
node_modules/
.env

##############################
## Maven
##############################
Expand Down
4 changes: 4 additions & 0 deletions connectservice/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
docker-compose.yml
test.http
Readme.md
11 changes: 11 additions & 0 deletions connectservice/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM node:22
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000
USER 1000
CMD ["npm", "run", "start"]
21 changes: 21 additions & 0 deletions connectservice/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Connect Service

Connect Service is a sample implementation of a web service which records usage
of Open Integration Engine, and provides access to a notifications feed. The service
implements the operations called by the client in `ConnectServiceUtil.java`.

## Running and testing

To start the example, run `docker-compose build` in this directory, followed by
`docker-compose up -d`. You can use `http://localhost:3000` as `URL_CONNECT_SERVER`
in `ConnectServiceUtil.java`, and see recorded data
[in Kibana.](http://localhost:5601/app/management/data/index_management/indices/index_details?indexName=registration)

## Not fit for production use as-is

This example is purely for expository purposes only. Use as-is is not recommended.
At a minimum, it would be neccesary to:

- Use a reverse proxy to encrypt in-transit data
- Secure communication between the connectservice and elasticsearch
- Require authentication for elasticsearch and kibana
36 changes: 36 additions & 0 deletions connectservice/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: connectservice

volumes:
connectservice_elastic_data: {}

services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:9.0.1
environment:
- discovery.type=single-node
# Don't do this in prod.
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- connectservice_elastic_data:/usr/share/elasticsearch/data

connectservice:
build:
context: .
dockerfile: Dockerfile
environment:
- ELASTICSEARCH_URL=http://elasticsearch:9200
ports:
- "3000:3000"
depends_on:
- elasticsearch

kibana:
image: docker.elastic.co/kibana/kibana:9.0.1
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
13 changes: 13 additions & 0 deletions connectservice/elastic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Client } from '@elastic/elasticsearch';

export const client = new Client({
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
auth: process.env.ELASTICSEARCH_USERNAME ? {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD || 'changeme',
} : undefined,
tls: {
rejectUnauthorized: process.env.ELASTICSEARCH_IGNORETLSERRORS && !!JSON.parse(process.env.ELASTICSEARCH_IGNORETLSERRORS),
},
requestTimeout: process.env.ELASTICSEARCH_TIMEOUT ? JSON.parse(process.env.ELASTICSEARCH_TIMEOUT) : 30000,
});
31 changes: 31 additions & 0 deletions connectservice/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const REPO_URL = process.env.NOTIFICATIONS_URL ?? 'https://api.github.com/repos/OpenIntegrationEngine/engine/releases?per_page=10';

export async function getLatestReleases() {
const response = await fetch(REPO_URL, {
headers: {
'Accept': 'application/vnd.github.html+json',
'User-Agent': 'OIEConnectService',
},
});

if (!response.ok) {
throw new Error(`Failed to fetch latest release: ${response.statusText}`);
}

const releases = await response.json();
return releases.map(release => ({
id: release.id,
name: release.name,
body_html: release.body_html,
published_at: release.published_at,
}));
}

/*
interface Release {
id: number;
name: string;
body_html: string;
published_at: string;
}
*/
50 changes: 50 additions & 0 deletions connectservice/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import express from 'express';
import * as yup from 'yup';
import { postRegistration, postUsage, registrationBodySchema, usageBodySchema } from './usage.js';
import { postNotifications, notificationBodySchema } from './notifications.js';

const app = express();

app.use(express.urlencoded({ extended: true }));

app.post('/RegistrationServlet', [validateFormUrlEncoded, makeValidator(registrationBodySchema)], postRegistration);
app.post('/UsageStatisticsServlet', [validateFormUrlEncoded, makeValidator(usageBodySchema)], postUsage);

app.post('/NotificationServlet', [validateFormUrlEncoded, makeValidator(notificationBodySchema)], postNotifications);

const port = process.env.PORT || 3000;
app.listen(port, () =>
console.log(`connectservice available from http://localhost:${port}`),
);

function validateFormUrlEncoded(req, res, next) {
const contentType = req.headers['content-type'];
if (!contentType || !contentType.startsWith('application/x-www-form-urlencoded')) {
return res.status(415).send('Unsupported content-type');
}

next();
}

function makeValidator(schema) {
return async (req, res, next) => {
try {
// abortEarly: false collects all errors rather than stopping on the first
await schema.validate(req.body, { abortEarly: false });
next();
} catch (error) {
if (error instanceof yup.ValidationError) {
return res.status(400).json({
type: error.name,
message: error.message,
errors: error.inner.map(err => ({
path: err.path,
message: err.message,
type: err.type,
})),
});
}
next(error);
}
};
}
78 changes: 78 additions & 0 deletions connectservice/notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as yup from 'yup';
import { getLatestReleases } from './github.js';

export async function postNotifications(req, res) {
const notifications = await getLatestReleases();

if (req.body.op === 'getNotificationCount') {
return res.status(200).json(notifications.map(rel => rel.id));
} else if (req.body.op === 'getNotifications') {
return res.status(200).json(notifications.map(rel => ({
id: rel.id,
name: rel.name,
content: makeFullPageFromDiv(rel.body_html, rel.name),
date: rel.published_at,
})));
} else {
return res.status(400).json({
error: 'Invalid operation',
});
}
}

function makeFullPageFromDiv(div, name) {
name = htmlEncode(name);
return `<html>
<head>
<title>${name}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
<style>
body {
padding: 1em;
}
</style>
</head>
<body>
<main>
<h1>${name}</h1>
<div>
${div}
</div>
</main>
</body>
</html>`;
}

function htmlEncode(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

/*
POST https://connect.mirthcorp.com/NotificationServlet HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Apache-HttpClient/4.5.13 (Java/21.0.7)
Host: connect.mirthcorp.com
Content-Length: 2116

op=getNotificationCount&serverId=49885e13-4f2e-41a6-b66a-8e65f5492d9a&version=4.5.2&extensionVersions=url_encode_json
*/
export const notificationBodySchema = yup.object({
op: yup.string().oneOf(['getNotificationCount', 'getNotifications']).required(),
serverId: yup.string().required(),
version: yup.string().required(),
extensionVersions: yup.string().required(),
}).required();

/*
extensionVersions is a JSON object with the version of each extension:
{
"Server Log":"4.5.2",
"File Writer":"4.5.2",
...
}
*/
Loading