├── packaging ├── .gitignore ├── targets │ └── trusty │ │ ├── debian │ │ ├── compat │ │ ├── install │ │ ├── prerm │ │ ├── config │ │ ├── preinst │ │ ├── templates │ │ ├── control │ │ ├── rules │ │ └── postinst │ │ ├── home │ │ └── openhim │ │ │ └── bin │ │ │ └── install_node.sh │ │ └── etc │ │ └── init │ │ └── openhim-core.conf ├── build-release-zip.sh ├── README.md └── build-docker-centos-rpm.sh ├── test ├── resources │ ├── files │ │ ├── mac.txt │ │ ├── unix.txt │ │ └── msdos.txt │ ├── openhim-logo-green.png │ ├── replica-set-test │ │ ├── setup.sh │ │ ├── mongo-volumes │ │ │ └── .gitignore │ │ ├── tear-down.sh │ │ ├── README.md │ │ └── docker-compose.yml │ ├── protected │ │ ├── test.crt │ │ └── test.key │ ├── server-tls │ │ ├── cert.pem │ │ └── key.pem │ ├── trust-tls │ │ ├── cert1.pem │ │ ├── cert2.pem │ │ ├── key1.pem │ │ ├── key2.pem │ │ └── chain │ │ │ ├── ca.key.pem │ │ │ ├── intermediate.key.pem │ │ │ ├── test.openhim.org.key.pem │ │ │ ├── test.openhim.org.cert.pem │ │ │ ├── ca.cert.pem │ │ │ └── intermediate.cert.pem │ └── client-tls │ │ ├── invalid-cert.pem │ │ ├── cert.pem │ │ ├── key.pem │ │ └── invalid-key.pem ├── setupTest.js ├── unit │ ├── proxyTest.js │ ├── appsTest.js │ ├── apiAuthenticationTest.js │ ├── tcpAdapterTest.js │ ├── kafkaProducerTest.js │ ├── utilsTest.js │ ├── passportTest.js │ ├── jwksCacheTest.js │ ├── metadataTest.js │ └── customTokenAuthenticationTest.js ├── integration │ ├── aboutAPITests.js │ ├── restartAPITests.js │ └── urlRewriting.js └── constants.js ├── .nycrc.json ├── src ├── config │ ├── index.js │ ├── connection.js │ └── config.js ├── protocols │ ├── index.js │ ├── basic.js │ └── local.js ├── middleware │ ├── retrieveTCPTransaction.js │ ├── proxy.js │ ├── pollingBypassAuthorisation.js │ ├── tcpBypassAuthentication.js │ ├── pollingBypassAuthentication.js │ ├── rerunBypassAuthentication.js │ ├── rerunBypassAuthorisation.js │ ├── customTokenAuthentication.js │ ├── sessionStore.js │ ├── authorisation.js │ ├── basicAuthentication.js │ ├── rerunUpdateTransactionTask.js │ └── jwtAuthentication.js ├── model │ ├── dbVersion.js │ ├── index.js │ ├── autoRetry.js │ ├── apps.js │ ├── contactGroups.js │ ├── alerts.js │ ├── clients.js │ ├── keystore.js │ ├── events.js │ ├── mediators.js │ ├── tasks.js │ ├── metrics.js │ ├── visualizer.js │ └── audits.js ├── jwksCache.js ├── api │ ├── about.js │ ├── events.js │ ├── heartbeat.js │ ├── authorisation.js │ ├── logs.js │ ├── restart.js │ ├── metrics.js │ └── certificateAuthority.js ├── winston-transport-workaround.js ├── jwtSecretOrPublicKeyCache.js ├── migrateMetrics.js ├── kafkaProducerManager.js ├── bodyCull.js ├── kafkaProducer.js ├── constants.js └── polling.js ├── .eslintignore ├── resources ├── certs │ ├── ihe │ │ ├── cert.p12 │ │ ├── cert.zip │ │ ├── keystore.jks │ │ ├── dn.txt │ │ ├── password.txt │ │ ├── HOW_TO_CONFIGURE_CERT.md │ │ ├── cert.pem │ │ ├── cacert.MIR2014-16.pem │ │ ├── key.pem │ │ └── README.TXT │ └── default │ │ ├── cert.pem │ │ └── key.pem ├── scripts │ └── delete-unused-status-index.js ├── openhim-configuration-export.sh ├── openhim-core.service ├── rotate-log.sh ├── tail-events.sh ├── testCurlCmds.md ├── openhim-api-curl.sh └── sampleRecords │ └── audit.json ├── .env.test ├── .prettierrc.yaml ├── performance ├── mediator │ ├── package.json │ ├── generate_certs.sh │ ├── package-lock.json │ ├── Dockerfile │ ├── configure_openhim.sh │ ├── README.md │ ├── body-stream.js │ ├── tcp-handler.js │ ├── tls │ │ ├── cert.pem │ │ └── key.pem │ ├── http-handler.js │ └── server.js ├── auth.js ├── stress.js ├── volume.js ├── transactionsWithoutFilters.js ├── load.js └── metrics.js ├── config ├── production.json ├── development.json └── test.json ├── infrastructure ├── centos │ ├── Dockerfile │ ├── mongodb-org-3.4.repo │ └── entrypoint.sh └── docker-compose.yml ├── babel.config.js ├── .npmignore ├── .dockerignore ├── .gitignore ├── .eslintrc.json ├── Dockerfile ├── .travis.yml ├── .github └── workflows │ ├── docker-image-scan.yml │ ├── master.yml │ └── tags.yml ├── .travis └── build_docker.sh ├── docker-compose.deps.yml └── bin └── openhim-core.js /packaging/.gitignore: -------------------------------------------------------------------------------- 1 | .npm 2 | .node-gyp 3 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/compat: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /test/resources/files/mac.txt: -------------------------------------------------------------------------------- 1 | this 2 | has 3 | mac 4 | line 5 | endings -------------------------------------------------------------------------------- /test/resources/files/unix.txt: -------------------------------------------------------------------------------- 1 | this 2 | has 3 | unix 4 | line 5 | endings -------------------------------------------------------------------------------- /test/resources/files/msdos.txt: -------------------------------------------------------------------------------- 1 | this 2 | has 3 | msdos 4 | line 5 | endings -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "include": [ 4 | "src/**" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './config' 4 | export * from './connection' 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/* 2 | packaging/* 3 | infrastructure/* 4 | performance/* 5 | resources/* 6 | lib/* 7 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/install: -------------------------------------------------------------------------------- 1 | home/openhim/* home/openhim 2 | etc/* etc 3 | /usr/share/openhim-core 4 | -------------------------------------------------------------------------------- /resources/certs/ihe/cert.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jembi/openhim-core-js/HEAD/resources/certs/ihe/cert.p12 -------------------------------------------------------------------------------- /resources/certs/ihe/cert.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jembi/openhim-core-js/HEAD/resources/certs/ihe/cert.zip -------------------------------------------------------------------------------- /resources/certs/ihe/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jembi/openhim-core-js/HEAD/resources/certs/ihe/keystore.jks -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | NODE_TLS_REJECT_UNAUTHORIZED=0 3 | NODE_OPTIONS=--tls-cipher-list=\"ECDHE-RSA-AES128-GCM-SHA256\" 4 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | bracketSpacing: false 4 | trailingComma: "none" 5 | arrowParens: "avoid" 6 | -------------------------------------------------------------------------------- /performance/mediator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jembi/openhim-load", 3 | "version": "0.1.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /test/resources/openhim-logo-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jembi/openhim-core-js/HEAD/test/resources/openhim-logo-green.png -------------------------------------------------------------------------------- /test/resources/replica-set-test/setup.sh: -------------------------------------------------------------------------------- 1 | (cd ./test/resources/replica-set-test ; docker-compose build && docker-compose up -d) 2 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "logger": { 3 | "level": "warn", 4 | "logToDB": true, 5 | "capDBLogs": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/resources/replica-set-test/mongo-volumes/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /infrastructure/centos/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos 2 | 3 | LABEL Jembi Health Systems NPC 4 | 5 | COPY entrypoint.sh / 6 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /test/resources/replica-set-test/tear-down.sh: -------------------------------------------------------------------------------- 1 | docker stop replica1 replica2 replica3 replica-core && 2 | docker rm replica1 replica2 replica3 replica-core -------------------------------------------------------------------------------- /resources/certs/ihe/dn.txt: -------------------------------------------------------------------------------- 1 | ZA 2 | KZN 3 | Durban 4 | Jembi Health Systems NPC 5 | eHealth 6 | openhim 7 | ryan@jembi.org 8 | password 9 | IHE Connectathon 10 | -------------------------------------------------------------------------------- /resources/scripts/delete-unused-status-index.js: -------------------------------------------------------------------------------- 1 | /* global db */ 2 | 3 | db.transactions.dropIndex('status_1') 4 | 5 | db.transactions.dropIndex('channelID_1') 6 | -------------------------------------------------------------------------------- /performance/mediator/generate_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | openssl req -x509 -nodes -newkey rsa:4096 -keyout tls/key.pem -out tls/cert.pem -days 3650 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | '@babel/env', 4 | { 5 | targets: { 6 | node: true 7 | } 8 | } 9 | ] 10 | ] 11 | 12 | module.exports = {presets} 13 | -------------------------------------------------------------------------------- /resources/certs/ihe/password.txt: -------------------------------------------------------------------------------- 1 | #***************************************************** 2 | # This is your password for cert.p12 and keystore.jks 3 | #***************************************************** 4 | password 5 | -------------------------------------------------------------------------------- /resources/openhim-configuration-export.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | collections=(users channels clients contactGroups mediators) 4 | 5 | for c in ${collections[@]} 6 | do 7 | mongodump --db openhim --collection $c 8 | done -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USERNAME=openhim 4 | HOME=/home/$USERNAME 5 | 6 | service openhim-core stop || true 7 | sleep 2 8 | userdel $USERNAME 9 | rm -r $HOME 10 | 11 | exit 0 12 | -------------------------------------------------------------------------------- /infrastructure/centos/mongodb-org-3.4.repo: -------------------------------------------------------------------------------- 1 | [mongodb-org-3.4] 2 | name=MongoDB Repository 3 | baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.4/x86_64/ 4 | gpgcheck=1 5 | enabled=1 6 | gpgkey=https://www.mongodb.org/static/pgp/server-3.4.asc -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /openhim-js.sublime-project 3 | /openhim-js.sublime-workspace 4 | .idea/** 5 | packaging/.gnupg 6 | packaging/.npm 7 | packaging/builds/* 8 | packaging/.gitconfig 9 | \#* 10 | *~ 11 | .#* 12 | \#*\# 13 | packaging/builds 14 | .otto 15 | *.log 16 | Vagrantfile 17 | -------------------------------------------------------------------------------- /performance/mediator/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jembi/openhim-load", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@jembi/openhim-load", 9 | "version": "0.1.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/resources/replica-set-test/README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Fires up a mongo replica set in docker and runs the OpenHIM tests against the replica set 3 | 4 | ### Start 5 | ``` 6 | npm run test:replica:set 7 | ``` 8 | 9 | ### Clean up 10 | ``` 11 | npm run test:replica:set:cleanup 12 | ``` 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | .travis 3 | .nyc_output 4 | .vscode 5 | .git 6 | .gitignore 7 | node_modules 8 | lib/* 9 | performance 10 | packaging 11 | infrastructure 12 | test 13 | .env.test 14 | .eslintrc.json 15 | .eslintignore 16 | .npmignore 17 | .nycrc.json 18 | .prettierrc.yaml 19 | .travis.yml 20 | -------------------------------------------------------------------------------- /src/protocols/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * as local from './local' 4 | export * as basic from './basic' 5 | export * as openid from './openid' 6 | import * as tokenProtocol from './token' 7 | 8 | /** 9 | * @deprecated 10 | * token protocol is deprecated 11 | */ 12 | export const token = tokenProtocol 13 | -------------------------------------------------------------------------------- /test/setupTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | require('../src/config/config') 6 | 7 | import nconf from 'nconf' 8 | 9 | import {SERVER_PORTS} from './constants' 10 | 11 | // Set the router http port to the mocked constant value for the tests 12 | nconf.set('router', {httpPort: SERVER_PORTS.httpPort}) 13 | -------------------------------------------------------------------------------- /performance/mediator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:carbon-alpine 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | ARG NODE_ENV 7 | ENV NODE_ENV $NODE_ENV 8 | 9 | COPY performance/mediator/package.json /usr/src/app/ 10 | RUN npm install && npm cache clean --force 11 | COPY performance/mediator /usr/src/app 12 | 13 | CMD ["npm", "start"] 14 | -------------------------------------------------------------------------------- /resources/openhim-core.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenHIM Core 3 | Documentation=http://openhim.org 4 | After=network.target 5 | 6 | [Service] 7 | Environment=NODE_ENV=production 8 | Type=simple 9 | User= 10 | ExecStart=/path/to/node /path/to/openhim-core/lib/server.js 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /src/middleware/retrieveTCPTransaction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as tcpAdapter from '../tcpAdapter' 4 | 5 | export async function koaMiddleware(ctx, next) { 6 | // the body contains the key 7 | const transaction = tcpAdapter.popTransaction(ctx.body) 8 | 9 | ctx.body = transaction.data 10 | ctx.authorisedChannel = transaction.channel 11 | 12 | await next() 13 | } 14 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/config: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /usr/share/debconf/confmodule 4 | 5 | db_input high openhim-core-js/watch-fs-for-cert || true 6 | db_go || true 7 | 8 | db_get openhim-core-js/watch-fs-for-cert 9 | if [ "$RET" = "true" ]; then 10 | db_input high openhim-core-js/certPath || true 11 | db_input high openhim-core-js/keyPath || true 12 | db_go || true 13 | fi 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | /openhim-js.sublime-project 4 | /openhim-js.sublime-workspace 5 | .idea/** 6 | packaging/.gnupg 7 | packaging/.npm 8 | packaging/builds/* 9 | packaging/.gitconfig 10 | packaging/.vagrant 11 | \#* 12 | *~ 13 | .#* 14 | \#*\# 15 | packaging/builds 16 | .otto 17 | *.log 18 | .DS_Store 19 | .vscode 20 | .vagrant 21 | artillery 22 | 23 | coverage.lcov 24 | .nyc_output 25 | .data 26 | coverage/ 27 | DockerfileARM 28 | -------------------------------------------------------------------------------- /resources/certs/ihe/HOW_TO_CONFIGURE_CERT.md: -------------------------------------------------------------------------------- 1 | How to setup OpenHIM with the IHE certs 2 | ======================================= 3 | 4 | 1. Copy cert.pem and key.pem to the /tls folder 5 | 2. Create a client ensure the client domain name equals the dn of the client, cert can be anything it doesn't get checked... 6 | 3. Create another client with the CA certificate. It doesn't matter what this clients is called. It just adds the CA cert to the list of CAs in node. -------------------------------------------------------------------------------- /src/model/dbVersion.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | const dbVersionSchema = new Schema({ 8 | version: Number, 9 | lastUpdated: Date 10 | }) 11 | 12 | export const dbVersionModelAPI = connectionAPI.model( 13 | 'dbVersion', 14 | dbVersionSchema 15 | ) 16 | export const DbVersionModel = connectionDefault.model( 17 | 'dbVersion', 18 | dbVersionSchema 19 | ) 20 | -------------------------------------------------------------------------------- /src/protocols/basic.js: -------------------------------------------------------------------------------- 1 | import * as local from './local' 2 | 3 | /** 4 | * Basic Authentication Protocol 5 | * 6 | * the application sends a username and password with every request 7 | * 8 | * For more information on basic authentication in Passport.js, check out: 9 | * https://www.passportjs.org/packages/passport-http/ 10 | */ 11 | 12 | export const login = async function (username, password, next) { 13 | return await local.login(username, password, next) 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parser": "@babel/eslint-parser", 7 | "parserOptions": { 8 | "ecmaVersion": 2017, 9 | "sourceType": "module" 10 | }, 11 | "plugins": ["prettier"], 12 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 13 | "rules": { 14 | "require-atomic-updates": "off" // should be defaulted to "off" soon: https://github.com/eslint/eslint/issues/11899 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.21.3-alpine as build 2 | 3 | WORKDIR /build 4 | 5 | COPY . . 6 | 7 | RUN npm install && npm run build 8 | 9 | FROM node:14.21.3-alpine 10 | 11 | ENV NODE_ENV=production 12 | 13 | RUN apk upgrade --update-cache --available && \ 14 | apk add openssl && \ 15 | rm -rf /var/cache/apk/* 16 | 17 | WORKDIR /app 18 | 19 | COPY --from=build ./build/lib ./lib 20 | 21 | COPY . . 22 | 23 | RUN npm install --production 24 | 25 | CMD ["node", "lib/server.js"] 26 | -------------------------------------------------------------------------------- /packaging/targets/trusty/home/openhim/bin/install_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | OPENHIM_VERSION= 5 | 6 | USERNAME=openhim 7 | HOME=/home/$USERNAME 8 | CURL=/usr/bin/curl 9 | SH=/bin/bash 10 | 11 | cd $HOME 12 | 13 | # Install NVM 14 | echo "Installing node version manager for "$USERNAME" user ..." 15 | $CURL -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | $SH > /dev/null 16 | . $HOME/.nvm/nvm.sh 17 | 18 | # Install node 19 | nvm install --lts || true 20 | 21 | exit 0 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: jammy 3 | node_js: 4 | - "lts/fermium" 5 | - "lts/gallium" 6 | - "node" 7 | matrix: 8 | fast_finish: true 9 | allow_failures: 10 | - node_js: 'node' 11 | services: 12 | - mongodb 13 | before_install: 14 | - export TZ=Africa/Johannesburg 15 | script: 16 | - npm test 17 | after_success: 18 | - npm run coverage 19 | notifications: 20 | slack: 21 | rooms: 22 | - jembihealthsystems:mlQYVFbijxcZkesCt7G5VBoM 23 | on_success: change 24 | on_failure: always 25 | -------------------------------------------------------------------------------- /packaging/targets/trusty/etc/init/openhim-core.conf: -------------------------------------------------------------------------------- 1 | # OpenHIM server upstart config 2 | 3 | description "OpenHIM server" 4 | 5 | # logs to /var/log/upstart/openhim-core.log 6 | console log 7 | 8 | start on runlevel [2345] 9 | stop on runlevel [!2345] 10 | 11 | respawn 12 | 13 | setuid openhim 14 | setgid openhim 15 | 16 | env OPENHIM=/usr/share/openhim-core 17 | 18 | script 19 | cd $OPENHIM 20 | exec bash -c 'source /home/openhim/.nvm/nvm.sh && nvm use --lts && exec bin/openhim-core.js --conf=/etc/openhim/config.json' 21 | end script 22 | -------------------------------------------------------------------------------- /src/jwksCache.js: -------------------------------------------------------------------------------- 1 | import JwksRsa from 'jwks-rsa' 2 | 3 | import * as configIndex from './config' 4 | 5 | let keys = [] 6 | 7 | export const populateCache = async () => { 8 | const jwksUri = configIndex.config.get('authentication:jwt:jwksUri') 9 | 10 | const client = JwksRsa({jwksUri}) 11 | keys = await client.getSigningKeys() 12 | } 13 | 14 | export const getKey = async kid => { 15 | const key = keys.find(key => key.kid === kid) 16 | if (!key) { 17 | // if cache miss, populate the cache and try again 18 | await populateCache() 19 | } 20 | return keys.find(key => key.kid === kid) 21 | } 22 | -------------------------------------------------------------------------------- /src/model/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Mongoose from 'mongoose' 4 | 5 | Mongoose.Promise = Promise 6 | 7 | export * from './alerts' 8 | export * from './audits' 9 | export * from './autoRetry' 10 | export * from './channels' 11 | export * from './clients' 12 | export * from './contactGroups' 13 | export * from './dbVersion' 14 | export * from './events' 15 | export * from './keystore' 16 | export * from './mediators' 17 | export * from './tasks' 18 | export * from './transactions' 19 | export * from './users' 20 | export * from './visualizer' 21 | export * from './metrics' 22 | export * from './passport' 23 | -------------------------------------------------------------------------------- /performance/auth.js: -------------------------------------------------------------------------------- 1 | import crypto from 'k6/crypto' 2 | 3 | const rootUser = { 4 | email: 'root@jembi.org', 5 | hash: '669c981d4edccb5ed61f4d77f9fcc4bf594443e2740feb1a23f133bdaf80aae41804d10aa2ce254cfb6aca7c497d1a717f2dd9a794134217219d8755a84b6b4e', 6 | salt: '22a61686-66f6-483c-a524-185aac251fb0' 7 | } 8 | 9 | export function getTestAuthHeaders () { 10 | const timestamp = new Date().toISOString() 11 | return { 12 | 'auth-username': rootUser.email, 13 | 'auth-ts': timestamp, 14 | 'auth-salt': rootUser.salt, 15 | 'auth-token': crypto.sha512(rootUser.hash + rootUser.salt + timestamp, 'hex') 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/middleware/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export function setupProxyHeaders(ctx) { 4 | function setOrAppendHeader(ctx, header, value) { 5 | if (!value) { 6 | return 7 | } 8 | if (ctx.header[header]) { 9 | ctx.header[header] = `${ctx.header[header]}, ${value}` 10 | } else { 11 | ctx.header[header] = `${value}` 12 | } 13 | } 14 | 15 | setOrAppendHeader(ctx, 'X-Forwarded-For', ctx.request.ip) 16 | return setOrAppendHeader(ctx, 'X-Forwarded-Host', ctx.request.host) 17 | } 18 | 19 | export async function koaMiddleware(ctx, next) { 20 | exports.setupProxyHeaders(ctx) 21 | await next() 22 | } 23 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USERNAME=openhim 4 | HOME=/home/$USERNAME 5 | USERADD=/usr/sbin/useradd 6 | ADDGROUP=/usr/sbin/addgroup 7 | ADDUSER=/usr/sbin/adduser 8 | 9 | service openhim-core stop || true 10 | 11 | # Create user and group 12 | if ! getent group $USERNAME >/dev/null; then 13 | echo "Creating group $USERNAME" 14 | $ADDGROUP --quiet --system $USERNAME 15 | fi 16 | 17 | 18 | if id -u $USERNAME >/dev/null 2>&1; then 19 | echo "user $USERNAME exists." 20 | else 21 | echo "user $USERNAME does not exist. adding." 22 | $USERADD $USERNAME -g $USERNAME -m -s /bin/bash 23 | fi 24 | 25 | exit 0 26 | -------------------------------------------------------------------------------- /performance/mediator/configure_openhim.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | username=$1 6 | password=$2 7 | mediator_dir=$(dirname $0) 8 | curl=$mediator_dir/../../resources/openhim-api-curl.sh 9 | 10 | # Import the configurations 11 | $curl "$username" "$password" \ 12 | -H "Content-Type:application/json" \ 13 | -d @$mediator_dir/openhim-insert.json \ 14 | "https://localhost:8080/metadata" 15 | 16 | # Add the certificate to the trust store 17 | jq -n "{\"cert\":\"$(cat $mediator_dir/tls/cert.pem)\"}" | \ 18 | $curl "$username" "$password" \ 19 | -H "Content-Type:application/json" \ 20 | -d @- \ 21 | "https://localhost:8080/keystore/ca/cert" 22 | -------------------------------------------------------------------------------- /src/api/about.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | 5 | import * as utils from '../utils' 6 | import {version as currentCoreVersion} from '../../package.json' 7 | 8 | export async function getAboutInformation(ctx) { 9 | try { 10 | ctx.body = {currentCoreVersion, serverTimezone: utils.serverTimezone()} 11 | ctx.status = 200 12 | logger.info( 13 | `User ${ctx.authenticated.email} successfully fetched 'about' information` 14 | ) 15 | } catch (e) { 16 | utils.logAndSetResponse( 17 | ctx, 18 | 500, 19 | `Could not fetch 'about' info via the API ${e}`, 20 | 'error' 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/rotate-log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # A simple log roller script 3 | # 4 | # Usage: 5 | # Ensure the MAIN_LOG_FILE variable points to your target log file 6 | # Schedule using cron daily, e.g. on Ubuntu: 7 | # sudo crontab -e 8 | # 0 0 * * * /some/location/rotate-log.sh 9 | 10 | MAIN_LOG_FILE=/var/log/openhim-core.log 11 | 12 | DATE=`date +%Y-%m-%d` 13 | ROLLED_LOG_FILE=`echo $MAIN_LOG_FILE | sed "s/\.log/-$DATE\.log/"` 14 | ROLLED_LOG_FILE_GZ="$ROLLED_LOG_FILE.gz" 15 | 16 | if [ ! -f $ROLLED_LOG_FILE_GZ ]; then 17 | cp $MAIN_LOG_FILE $ROLLED_LOG_FILE; 18 | gzip $ROLLED_LOG_FILE; 19 | echo "[rolled over $ROLLED_LOG_FILE_GZ]" > $MAIN_LOG_FILE; 20 | fi 21 | -------------------------------------------------------------------------------- /performance/mediator/README.md: -------------------------------------------------------------------------------- 1 | ## Instructions 2 | 3 | Generate some certificates to be used by the HTTPS and TLS servers: 4 | 5 | ```bash 6 | ./generate_certs.sh 7 | ``` 8 | 9 | Start the server: 10 | 11 | ```bash 12 | npm start 13 | ``` 14 | 15 | Set up the OpenHIM: 16 | 17 | 1. Log in to the console and navigate to `Export/Import`. 18 | 2. Import the `openhim-insert.json` file under the `Import Data` section. 19 | 3. Navigate to `Certificates`. 20 | 4. Import `tls/cert.pem` under the `Trusted Certificates` section. 21 | 5. Navigate to `Channels`. 22 | 6. Edit the route for each HTTPS or TLS channel and set the certificate to the 23 | one that was just imported. 24 | -------------------------------------------------------------------------------- /src/model/autoRetry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | const AutoRetrySchema = new Schema({ 8 | transactionID: { 9 | type: Schema.Types.ObjectId, 10 | required: true 11 | }, 12 | channelID: { 13 | type: Schema.Types.ObjectId, 14 | required: true 15 | }, 16 | requestTimestamp: { 17 | type: Date, 18 | required: true 19 | } 20 | }) 21 | 22 | export const AutoRetryModelAPI = connectionAPI.model( 23 | 'AutoRetry', 24 | AutoRetrySchema 25 | ) 26 | export const AutoRetryModel = connectionDefault.model( 27 | 'AutoRetry', 28 | AutoRetrySchema 29 | ) 30 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/templates: -------------------------------------------------------------------------------- 1 | Template: openhim-core-js/watch-fs-for-cert 2 | Type: boolean 3 | Default: false 4 | Description: Do you want this installation of the OpenHIM to watch a particular folder for it's certificate and key? (this works well with letsencrypt.org for free secure certificates) 5 | 6 | Template: openhim-core-js/certPath 7 | Type: string 8 | Default: /etc/letsencrypt/live//cert.pem 9 | Description: What is the absolute path to the server certificate in .pem format? 10 | 11 | Template: openhim-core-js/keyPath 12 | Type: string 13 | Default: /etc/letsencrypt/live//privkey.pem 14 | Description: What is the absolute path to the server key in .pem format? 15 | -------------------------------------------------------------------------------- /src/middleware/pollingBypassAuthorisation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {promisify} from 'util' 4 | 5 | import {ChannelModel} from '../model/channels' 6 | 7 | export function authoriseUser(ctx, done) { 8 | return ChannelModel.findOne( 9 | {_id: ctx.request.header['channel-id']}, 10 | (err, channel) => { 11 | if (err) { 12 | return done(err) 13 | } 14 | ctx.authorisedChannel = channel 15 | return done(null, channel) 16 | } 17 | ) 18 | } 19 | 20 | /* 21 | * Koa middleware for bypassing authorisation for polling 22 | */ 23 | export async function koaMiddleware(ctx, next) { 24 | const _authoriseUser = promisify(authoriseUser) 25 | await _authoriseUser(ctx) 26 | await next() 27 | } 28 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo": { 3 | "url": "mongodb://localhost/openhim-development", 4 | "atnaUrl": "mongodb://localhost/openhim-development" 5 | }, 6 | "logger": { 7 | "level": "debug" 8 | }, 9 | "auditing": { 10 | "servers": { 11 | "udp": { 12 | "enabled": true, 13 | "port": 5050 14 | }, 15 | "tls": { 16 | "enabled": true, 17 | "port": 5051 18 | }, 19 | "tcp": { 20 | "enabled": true, 21 | "port": 5052 22 | } 23 | } 24 | }, 25 | "certificateManagement": { 26 | "watchFSForCert": false, 27 | "certPath": "resources/certs/default/cert.pem", 28 | "keyPath": "resources/certs/default/key.pem" 29 | }, 30 | "openhimConsoleBaseUrl": "http://localhost:9000" 31 | } 32 | -------------------------------------------------------------------------------- /src/api/events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as authorisation from './authorisation' 4 | import * as utils from '../utils' 5 | import {EventModelAPI} from '../model/events' 6 | 7 | export async function getLatestEvents(ctx, receivedTime) { 8 | const authorised = await utils.checkUserPermission(ctx, 'getEvents', 'visualizer-view') 9 | 10 | if (!authorised) return 11 | 12 | try { 13 | const rtDate = new Date(Number(receivedTime)) 14 | const results = await EventModelAPI.find({ 15 | created: {$gte: rtDate} 16 | }).sort({normalizedTimestamp: 1}) 17 | ctx.body = {events: results} 18 | } catch (err) { 19 | utils.logAndSetResponse( 20 | ctx, 21 | 500, 22 | `Could not fetch the latest events via the API: ${err}`, 23 | 'error' 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/control: -------------------------------------------------------------------------------- 1 | Source: openhim-core-js 2 | Maintainer: Ryan Crichton 3 | Section: web 4 | Priority: optional 5 | Standards-Version: 3.9.1 6 | Build-Depends: debhelper (>= 7) 7 | Homepage: http://www.openhim.org/ 8 | 9 | 10 | Package: openhim-core-js 11 | Architecture: amd64 12 | Depends: curl, mongodb-org, git 13 | Recommends: build-essential, libkrb5-dev 14 | Description: OpenHIM Core 15 | The Open Health Information Mediator (OpenHIM) is a middleware software component designed to ease interoperability between disparate information systems. It provides a security communications and data governance as well as support for routing, orchestrating and translating messages as they flow between systems. The HIM can be though of as the "lock" within a health information exchange. 16 | -------------------------------------------------------------------------------- /src/model/apps.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { Schema } from 'mongoose' 3 | import {connectionAPI, connectionDefault} from '../config' 4 | 5 | const AppSchema = new Schema({ 6 | name: { 7 | type: String, 8 | unique: true, 9 | required: true 10 | }, 11 | description: String, 12 | icon: String, 13 | type: { 14 | type: String, 15 | enum: ['internal', 'external', 'esmodule'] 16 | }, 17 | category: String, 18 | access_roles: [String], 19 | url: { 20 | type: String, 21 | unique: true, 22 | required: true 23 | }, 24 | showInPortal: { 25 | type: Boolean, 26 | default: true 27 | }, 28 | showInSideBar: Boolean 29 | }) 30 | 31 | export const AppModelAPI = connectionAPI.model('App', AppSchema) 32 | export const AppModel = connectionDefault.model('App', AppSchema) 33 | -------------------------------------------------------------------------------- /src/middleware/tcpBypassAuthentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {promisify} from 'util' 4 | 5 | import {ClientModel} from '../model/clients' 6 | 7 | const dummyClient = new ClientModel({ 8 | clientID: 'DUMMY-TCP-USER', 9 | clientDomain: 'openhim.org', 10 | name: 'DUMMY-TCP-USER', 11 | roles: ['tcp'] 12 | }) 13 | 14 | export function authenticateUser(ctx, done) { 15 | ctx.authenticated = dummyClient 16 | return done(null, dummyClient) 17 | } 18 | 19 | /* 20 | * Koa middleware for bypassing authentication for tcp requests 21 | */ 22 | export async function koaMiddleware(ctx, next) { 23 | const _authenticateUser = promisify(authenticateUser) 24 | await _authenticateUser(ctx) 25 | 26 | if (ctx.authenticated != null) { 27 | await next() 28 | } else { 29 | ctx.response.status = 401 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/middleware/pollingBypassAuthentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {promisify} from 'util' 4 | 5 | import {ClientModel} from '../model/clients' 6 | 7 | const dummyClient = new ClientModel({ 8 | clientID: 'DUMMY-POLLING-USER', 9 | clientDomain: 'openhim.org', 10 | name: 'DUMMY-POLLING-USER', 11 | roles: ['polling'] 12 | }) 13 | 14 | export function authenticateUser(ctx, done) { 15 | ctx.authenticated = dummyClient 16 | return done(null, dummyClient) 17 | } 18 | 19 | /* 20 | * Koa middleware for bypassing authentication for polling requests 21 | */ 22 | export async function koaMiddleware(ctx, next) { 23 | const _authenticateUser = promisify(authenticateUser) 24 | await _authenticateUser(ctx) 25 | 26 | if (ctx.authenticated != null) { 27 | await next() 28 | } else { 29 | ctx.response.status = 401 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /resources/tail-events.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if (( $# < 2)); then 4 | echo "Tails the OpenHIM events API"; 5 | echo "Usage: $0 USERNAME PASSWORD [https://host:port[/base]]"; 6 | exit 0; 7 | fi 8 | 9 | username=$1; 10 | pass=$2; 11 | server=$3; 12 | 13 | if [[ -z $server ]]; then 14 | server="https://localhost:8080" 15 | fi 16 | 17 | sync="${server}/heartbeat" 18 | eventsBase="${server}/events" 19 | 20 | serverNow=`curl -s $sync | jq -M '.now'` 21 | lastCheck=`node -e 'console.log(Date.now())'` 22 | diff=$(($serverNow - $lastCheck)) 23 | 24 | while true; do 25 | events=`./openhim-api-curl.sh $username $pass -s "$eventsBase/$(($lastCheck + $diff))" | jq -M '.events'` 26 | if [[ "$events" != "[]" ]]; then 27 | echo $events | jq -C '.[]' 28 | fi 29 | lastCheck=`node -e 'console.log(Date.now())'` 30 | sleep 1 31 | done 32 | -------------------------------------------------------------------------------- /performance/stress.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | import { check } from 'k6' 3 | 4 | const BASE_URL = __ENV.BASE_URL || 'http://localhost:5001/http' 5 | 6 | export const options = { 7 | vus: 100, 8 | duration: '2m', 9 | thresholds: { 10 | http_req_duration: ['p(95)<60'] 11 | }, 12 | noVUConnectionReuse: true, 13 | discardResponseBodies: true 14 | } 15 | 16 | function makeGetRequest () { 17 | const response = http.get( 18 | `${BASE_URL}/immediate`, 19 | { 20 | headers: { 21 | Accept: 'application/json', 22 | Authorization: 'Basic cGVyZm9ybWFuY2U6cGVyZm9ybWFuY2U=' 23 | }, 24 | tags: { 25 | name: 'Get request' 26 | } 27 | } 28 | ) 29 | check(response, { 30 | 'status code is 200': r => r.status === 200 31 | }) 32 | } 33 | 34 | export default function () { 35 | makeGetRequest() 36 | } 37 | -------------------------------------------------------------------------------- /performance/mediator/body-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {Readable} = require('stream') 4 | const crypto = require('crypto') 5 | 6 | const RANDOM_BUFFER = crypto.randomBytes(2 * 1024 * 1024) 7 | 8 | class BodyStream extends Readable { 9 | constructor (length) { 10 | super({encoding: 'hex'}) 11 | this.remainingLength = length / 2 12 | } 13 | 14 | _read (size) { 15 | const length = Math.min(size, this.remainingLength) 16 | const lastChunk = length === this.remainingLength 17 | this.remainingLength -= length 18 | 19 | let remaining = length 20 | while (remaining > 0) { 21 | const chunkSize = Math.min(remaining, RANDOM_BUFFER.length) 22 | remaining -= chunkSize 23 | this.push(RANDOM_BUFFER.slice(0, chunkSize)) 24 | } 25 | 26 | if (lastChunk) { 27 | this.push(null) 28 | } 29 | } 30 | } 31 | 32 | module.exports = exports = BodyStream 33 | -------------------------------------------------------------------------------- /src/model/contactGroups.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | export const ContactUserDef = { 8 | user: { 9 | type: String, 10 | required: true 11 | }, 12 | method: { 13 | type: String, 14 | required: true, 15 | enum: ['email', 'sms'] 16 | }, 17 | maxAlerts: { 18 | type: String, 19 | enum: ['no max', '1 per hour', '1 per day'], 20 | default: 'no max' 21 | } 22 | } 23 | 24 | const ContactGroupSchema = new Schema({ 25 | group: { 26 | type: String, 27 | required: true, 28 | unique: true 29 | }, 30 | users: [ContactUserDef] 31 | }) 32 | 33 | export const ContactGroupModelAPI = connectionAPI.model( 34 | 'ContactGroup', 35 | ContactGroupSchema 36 | ) 37 | export const ContactGroupModel = connectionDefault.model( 38 | 'ContactGroup', 39 | ContactGroupSchema 40 | ) 41 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-scan.yml: -------------------------------------------------------------------------------- 1 | name: Scan latest docker image 2 | 3 | on: 4 | schedule: 5 | - cron: '0 5 * * *' 6 | 7 | jobs: 8 | scan-images: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Build an image from Dockerfile 15 | run: | 16 | docker build -t jembi/openhim-core:${{ github.sha }} . 17 | 18 | - name: Run trivy vulnerability scanner for the OpenHIM core image 19 | uses: aquasecurity/trivy-action@master 20 | with: 21 | image-ref: jembi/openhim-core:${{ github.sha }} 22 | format: 'sarif' 23 | output: 'trivy-results.sarif' 24 | 25 | - name: Upload Trivy scan results to Github Security tab 26 | uses: github/codeql-action/upload-sarif@v2 27 | if: always() 28 | with: 29 | sarif_file: 'trivy-results.sarif' 30 | -------------------------------------------------------------------------------- /resources/testCurlCmds.md: -------------------------------------------------------------------------------- 1 | Test https with mutual auth to JsonStub 2 | ======================================= 3 | ``` 4 | curl -v --insecure --key test/resources/client-tls/key.pem --cert test/resources/client-tls/cert.pem https://localhost:5000/sample/api -H "JsonStub-User-Key: 0582582f-89b8-436e-aa76-ba5444fc219d" -H "JsonStub-Project-Key: 1a841ebc-405e-474e-a8fa-9c401c823ae6" 5 | ``` 6 | 7 | Other useful commands 8 | ===================== 9 | 10 | ``` 11 | openssl s_client -connect localhost:5000 -key test/resources/trust-tls/key1.pem -cert test/resources/trust-tls/cert1.pem -CAfile resources/certs/default/cert.pem 12 | ``` 13 | 14 | ``` 15 | loadtest -n 5 https://localhost:5000/load-test --insecure --key test/resources/trust-tls/key1.pem --cert test/resources/trust-tls/cert1.pem -P 'TEST POST BODY' 16 | ``` 17 | 18 | ``` 19 | loadtest -n 5 http://localhost:5001/load-test -H Authorization:'Basic dGVzdDp0ZXN0' -P 'TEST POST BODY' 20 | ``` -------------------------------------------------------------------------------- /performance/volume.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | import {check} from 'k6' 3 | 4 | const BASE_URL = __ENV.BASE_URL || 'http://localhost:5001/http' 5 | 6 | export const options = { 7 | vus: 10, 8 | iterations: 1000, 9 | thresholds: { 10 | http_req_receiving: ['p(95)<100'], 11 | http_req_duration: ['p(95)<100'] 12 | }, 13 | noVUConnectionReuse: true, 14 | discardResponseBodies: true 15 | } 16 | 17 | function makeGetRequest() { 18 | const response = http.get( 19 | `${BASE_URL}/body`, 20 | { 21 | headers: { 22 | Accept: 'application/json', 23 | 'Accept-Encoding': 'identity', 24 | Authorization: 'Basic cGVyZm9ybWFuY2U6cGVyZm9ybWFuY2U=' 25 | }, 26 | tags: { 27 | name: 'Get request' 28 | } 29 | } 30 | ) 31 | check(response, { 32 | 'status code is 200': r => r.status === 200 33 | }) 34 | } 35 | 36 | export default function() { 37 | makeGetRequest() 38 | } 39 | -------------------------------------------------------------------------------- /.travis/build_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 4 | curl -H "Content-Type: application/json" --data '{"source_type": "Branch", "source_name": "core"}' -X POST https://registry.hub.docker.com/u/jembi/openhim-core/trigger/5cd6f182-c523-409e-ae68-9ab5de1f2849/ 5 | elif [ "$TRAVIS_BRANCH" == "test" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 6 | curl -H "Content-Type: application/json" --data '{"docker_tag": "test"}' -X POST https://registry.hub.docker.com/u/jembi/openhim-core/trigger/5cd6f182-c523-409e-ae68-9ab5de1f2849/ 7 | elif [ "$TRAVIS_BRANCH" == "staging" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 8 | curl -H "Content-Type: application/json" --data '{"docker_tag": "staging"}' -X POST https://registry.hub.docker.com/u/jembi/openhim-core/trigger/5cd6f182-c523-409e-ae68-9ab5de1f2849/ 9 | else 10 | echo "Docker image will only be built for commits to master/test/staging" 11 | fi 12 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # Uncomment this to turn on verbose mode. 3 | export DH_VERBOSE=1 4 | 5 | REVISION := $(shell head -1 debian/changelog | sed 's/.*(//;s/).*//;s/.*-//') 6 | 7 | build: build-stamp 8 | build-stamp: 9 | dh_testdir 10 | touch build-stamp 11 | 12 | clean: 13 | dh_testdir 14 | dh_testroot 15 | rm -f build-stamp 16 | dh_clean 17 | 18 | install: build 19 | dh_testdir 20 | dh_testroot 21 | dh_prep 22 | dh_installdirs 23 | dh_install 24 | 25 | # Build architecture-independent files here. 26 | binary-indep: build install 27 | dh_testdir 28 | dh_testroot 29 | dh_installchangelogs 30 | dh_installdocs 31 | dh_installdebconf 32 | dh_link 33 | dh_compress 34 | dh_fixperms 35 | dh_installdeb 36 | dh_gencontrol 37 | dh_md5sums 38 | dh_builddeb 39 | 40 | # Build architecture-dependent files here. 41 | binary-arch: 42 | 43 | binary: binary-indep binary-arch 44 | .PHONY: build clean binary-indep binary-arch binary install -------------------------------------------------------------------------------- /docker-compose.deps.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | zookeeper: 5 | image: docker.io/bitnami/zookeeper:3.8 6 | ports: 7 | - "2181:2181" 8 | volumes: 9 | - "zookeeper_data:/bitnami" 10 | environment: 11 | - ALLOW_ANONYMOUS_LOGIN=yes 12 | 13 | kafka: 14 | image: docker.io/bitnami/kafka:3.4 15 | ports: 16 | - "9092:9092" 17 | volumes: 18 | - "kafka_data:/bitnami" 19 | environment: 20 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 21 | - ALLOW_PLAINTEXT_LISTENER=yes 22 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092 23 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 24 | depends_on: 25 | - zookeeper 26 | 27 | mongo-db: 28 | container_name: mongo-db 29 | image: mongo:4.0 30 | ports: 31 | - "27017:27017" 32 | volumes: 33 | - "mongo-data:/data/db" 34 | restart: unless-stopped 35 | 36 | volumes: 37 | mongo-data: 38 | zookeeper_data: 39 | kafka_data: 40 | -------------------------------------------------------------------------------- /src/middleware/rerunBypassAuthentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {promisify} from 'util' 4 | 5 | import {ClientModel} from '../model/clients' 6 | 7 | export function authenticateUser(ctx, done) { 8 | return ClientModel.findOne( 9 | {_id: ctx.request.header.clientid}, 10 | (err, client) => { 11 | if (err) { 12 | return done(err) 13 | } 14 | ctx.authenticated = client 15 | ctx.parentID = ctx.request.header.parentid 16 | ctx.taskID = ctx.request.header.taskid 17 | return done(null, client) 18 | } 19 | ) 20 | } 21 | 22 | /* 23 | * Koa middleware for authentication by basic auth 24 | */ 25 | export async function koaMiddleware(ctx, next) { 26 | const _authenticateUser = promisify(authenticateUser) 27 | await _authenticateUser(ctx) 28 | 29 | if (ctx.authenticated != null) { 30 | await next() 31 | } else { 32 | ctx.authenticated = {ip: '127.0.0.1'} 33 | // This is a public channel, allow rerun 34 | await next() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/resources/protected/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIChTCCAe4CCQDbGP0P9s69azANBgkqhkiG9w0BAQsFADCBhjELMAkGA1UEBhMC 3 | WlcxGzAZBgNVBAgMEk1hdGViZWxlbGFuZCBOb3J0aDERMA8GA1UEBwwIQnVsYXdh 4 | eW8xDDAKBgNVBAoMA0J5bzELMAkGA1UECwwCSVQxDzANBgNVBAMMBmJ5by56dzEb 5 | MBkGCSqGSIb3DQEJARYMYWRtaW5AYnlvLnp3MB4XDTE1MDQxMDA5NTM1OVoXDTE2 6 | MDQwOTA5NTM1OVowgYYxCzAJBgNVBAYTAlpXMRswGQYDVQQIDBJNYXRlYmVsZWxh 7 | bmQgTm9ydGgxETAPBgNVBAcMCEJ1bGF3YXlvMQwwCgYDVQQKDANCeW8xCzAJBgNV 8 | BAsMAklUMQ8wDQYDVQQDDAZieW8uencxGzAZBgkqhkiG9w0BCQEWDGFkbWluQGJ5 9 | by56dzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0q4cDxLRslRKvG2pw5Gx 10 | 7fsq7YyiTokoBaDavNold5Vvayi02Mc7dqIanr/ckc0AqgiDd5Kw2bBAYmQpWiDn 11 | pZxp79JIV+gGh7pkiB4wDzvRBXMcew72B9uuYEeUQ8eonE/Yro0ZnD0ZGmBiuk/6 12 | 5xyWNikBhfPLnb2V+Cr/EKsCAwEAATANBgkqhkiG9w0BAQsFAAOBgQAwigr+o+Y+ 13 | 5BiL6MuHLTaC+lcv9r2GTHb7wNea674Db3Bi3u6FgmyMsZ8npPSlR0t/YZcFWtRM 14 | y4uXmw5zUutGZTbO/JFxts6kPV+a76mnNm71fF4QINkemmojvJyPZq9N+hV16dba 15 | v1m+dTK7hDZvpd/sM5bMtqWEq4aHkGhujw== 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /src/middleware/rerunBypassAuthorisation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {promisify} from 'util' 4 | 5 | import {ChannelModel} from '../model/channels' 6 | import {TransactionModel} from '../model/transactions' 7 | 8 | export function authoriseUser(ctx, done) { 9 | // Use the original transaction's channel to setup the authorised channel 10 | TransactionModel.findOne({_id: ctx.parentID}, (err, originalTransaction) => { 11 | if (err) { 12 | return done(err) 13 | } 14 | ChannelModel.findOne( 15 | {_id: originalTransaction.channelID}, 16 | (err, authorisedChannel) => { 17 | if (err) { 18 | return done(err) 19 | } 20 | ctx.authorisedChannel = authorisedChannel 21 | return done() 22 | } 23 | ) 24 | }) 25 | } 26 | 27 | /* 28 | * Koa middleware for authentication by basic auth 29 | */ 30 | export async function koaMiddleware(ctx, next) { 31 | const authoriseUser = promisify(exports.authoriseUser) 32 | await authoriseUser(ctx) 33 | await next() 34 | } 35 | -------------------------------------------------------------------------------- /src/winston-transport-workaround.js: -------------------------------------------------------------------------------- 1 | // TODO REMOVE WHEN FIXED 2 | // necessary hacks for winston 3.1.0 3 | // https://github.com/winstonjs/winston/issues/1130 4 | 5 | let Transport = require('winston-transport') 6 | 7 | Transport.prototype.normalizeQuery = function (options) { 8 | options = options || {} 9 | 10 | // limit 11 | options.rows = options.rows || options.limit || 10 12 | 13 | // starting row offset 14 | options.start = options.start || 0 15 | 16 | // now 17 | options.until = options.until || new Date() 18 | if (typeof options.until !== 'object') { 19 | options.until = new Date(options.until) 20 | } 21 | 22 | // now - 24 23 | options.from = options.from || options.until - 24 * 60 * 60 * 1000 24 | if (typeof options.from !== 'object') { 25 | options.from = new Date(options.from) 26 | } 27 | 28 | // 'asc' or 'desc' 29 | options.order = options.order || 'desc' 30 | 31 | return options 32 | } 33 | 34 | Transport.prototype.formatResults = function (results) { 35 | return results 36 | } 37 | -------------------------------------------------------------------------------- /performance/transactionsWithoutFilters.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | import { check } from 'k6' 3 | import {getTestAuthHeaders} from './auth.js' 4 | 5 | const BASE_URL = __ENV.BASE_URL || 'https://127.0.0.1:8080' 6 | 7 | export const options = { 8 | vus: 1, 9 | iterations: 100, 10 | thresholds: { 11 | http_req_duration: ['p(95)<600'] 12 | }, 13 | insecureSkipTLSVerify: true 14 | } 15 | 16 | function makeGetRequest () { 17 | const response = http.get( 18 | // Have to limit transactions to 100 as request times out for all transactions 19 | `${BASE_URL}/transactions?filterLimit=100`, 20 | { 21 | headers: Object.assign(getTestAuthHeaders(), { 22 | Accept: 'application/json', 23 | 'Content-Type': 'apllication/json' 24 | }), 25 | tags: { 26 | name: 'Transactions without filters' 27 | } 28 | } 29 | ) 30 | 31 | check(response, { 32 | 'status code is 200': r => r.status === 200 33 | }) 34 | } 35 | 36 | export default function () { 37 | makeGetRequest() 38 | } 39 | -------------------------------------------------------------------------------- /test/resources/protected/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,8D7649F2550A3AD6 4 | 5 | iD68IbbRZDJuiYBqiEMLSfQQqcbXr4CeHFVkAYxCKN785tg+icShBeYXP2H2Oa7m 6 | dmFBNrtuPclUjk1VFeDcRhd1WwabuSbKJkV26bqIwYKSa1blDcgviSvIc8OgN06G 7 | K7kX+3jQzUMeSj+B1phyysCSoSU059oJvcNknBOEe67ersYZhXWFDOxVOeCnpsVA 8 | xDPRGVWa5g5jj0WqcXey/oC/hUccX8w8RZJLSBurIxfWBGy1QtsnopNRaxs5gqXj 9 | FCucQtYTMwRzA0c65/6yDrYrnv3v0mmCKBKXoTvhcmxkE/Ejrk0+ydxlgWXoMjOD 10 | CzuDy8xQ2KN2/62fb0Eb6LwnI3EHceXAO9GbgpCTXDZt0yoKMBDZFYA1+TuYBXJj 11 | RRcKDcF+gt1VdloSkAWIaDwypnPi0xngsJbzNIHKoSKYPuxWdUXB5rThbSdPki5x 12 | r8La88LBK6/WarJevce8Ggg/KCAx5ng/w9XzQDlMSNVT6Ht+gGe6+69XjBA5IyIo 13 | bymu9PlwRMMYEeEcH53tGXcCgSkzlDs2Cc+1+JUypZpX4oggNV5YhsmLZS5BtPFs 14 | 9i3lA7RsxXoV0u1BTgHIqrH58IKaXN9xs0ceie3cLR5tcSiSsU9sh3wpjZ2AZ/Q5 15 | zjoHuyzQVxY14qlE3uWzI9vJtc/kCkQ4D8wdBRVM8uRzf+YGYKRbI6vvsSICeLuW 16 | n82E2zin77HnIOGVHPJ5aNaTQD91Ubxxv7lVeuxZf2tgejs2DbJraKOtOmHLd9GA 17 | Zb5G9TdFMxVcXq18xUMm/Rbkj8ugALxRy8CGNSrTFTPHaEwYxqhnAQ== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /resources/openhim-api-curl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if (( $# < 2)); then 4 | echo "OpenHIM API: Curl wrapper that sets up the appropriate OpenHIM authentication headers"; 5 | echo "Usage: $0 USERNAME PASSWORD [CURL_ARGS...]"; 6 | exit 0; 7 | fi 8 | 9 | username=$1; 10 | pass=$2; 11 | shift; 12 | shift; 13 | 14 | # which server? 15 | server="" 16 | for arg in $@; do 17 | match=`echo $arg | grep http | perl -pe 's|(https?://.*?)/.*|\1|'`; 18 | if [ "$match" ]; then 19 | server=$match; 20 | fi 21 | done 22 | 23 | if [ ! "$server" ]; then 24 | echo "OpenHIM server not specified"; 25 | exit 0; 26 | fi 27 | 28 | auth=`curl -k -s $server/authenticate/$username`; 29 | salt=`echo $auth | perl -pe 's|.*"salt":"(.*?)".*|\1|'`; 30 | ts=`echo $auth | perl -pe 's|.*"ts":"(.*?)".*|\1|'`; 31 | 32 | passhash=`echo -n "$salt$pass" | shasum -a 512 | awk '{print $1}'`; 33 | token=`echo -n "$passhash$salt$ts" | shasum -a 512 | awk '{print $1}'`; 34 | 35 | curl -k -H "auth-username: $username" -H "auth-ts: $ts" -H "auth-salt: $salt" -H "auth-token: $token" $@; 36 | echo ""; 37 | -------------------------------------------------------------------------------- /src/model/alerts.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | // A collection for keeping a day-long log of any alerts that got sent out to users 8 | // It is used for the user max-alert policies 9 | const AlertSchema = new Schema({ 10 | user: { 11 | type: String, 12 | required: true 13 | }, 14 | method: { 15 | type: String, 16 | required: true 17 | }, 18 | timestamp: { 19 | type: Date, 20 | required: true, 21 | default: Date.now, 22 | expires: '1d' 23 | }, 24 | channelID: { 25 | type: String, 26 | required: true 27 | }, 28 | condition: { 29 | type: String, 30 | required: true 31 | }, 32 | status: { 33 | type: String, 34 | required: true 35 | }, 36 | alertStatus: { 37 | type: String, 38 | required: true, 39 | enum: ['Failed', 'Completed'] 40 | } 41 | }) 42 | 43 | export const AlertModelAPI = connectionAPI.model('Alert', AlertSchema) 44 | export const AlertModel = connectionDefault.model('Alert', AlertSchema) 45 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Push OpenHIM Core Docker Image On Commit To Master 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v2 23 | 24 | - name: Login to Docker Hub 25 | uses: docker/login-action@v2 26 | with: 27 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 28 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 29 | 30 | - name: Set up Docker Buildx 31 | id: buildx 32 | uses: docker/setup-buildx-action@v2 33 | 34 | - name: Build and push 35 | id: docker_build 36 | uses: docker/build-push-action@v4 37 | with: 38 | context: ./ 39 | file: ./Dockerfile 40 | platforms: linux/amd64,linux/arm64 41 | push: true 42 | tags: jembi/openhim-core:latest 43 | -------------------------------------------------------------------------------- /src/model/clients.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | const ClientSchema = new Schema({ 8 | clientID: { 9 | type: String, 10 | required: true, 11 | unique: true, 12 | index: true 13 | }, 14 | clientDomain: { 15 | type: String, 16 | index: true 17 | }, 18 | name: { 19 | type: String, 20 | required: true 21 | }, 22 | roles: [{type: String, required: true}], 23 | customTokenID: { 24 | type: String, 25 | index: { 26 | unique: true, 27 | partialFilterExpression: {customTokenID: {$type: 'string'}} 28 | } 29 | }, 30 | passwordAlgorithm: String, 31 | passwordHash: String, 32 | passwordSalt: String, 33 | certFingerprint: String, 34 | organization: String, 35 | location: String, 36 | softwareName: String, 37 | description: String, 38 | contactPerson: String, 39 | contactPersonEmail: String 40 | }) 41 | 42 | export const ClientModelAPI = connectionAPI.model('Client', ClientSchema) 43 | export const ClientModel = connectionDefault.model('Client', ClientSchema) 44 | -------------------------------------------------------------------------------- /bin/openhim-core.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const forever = require('forever-monitor') 3 | const path = require('path') 4 | const fs = require('fs') 5 | 6 | const args = process.argv 7 | args.splice(0, 2) 8 | 9 | /* Check for version flag */ 10 | const root = path.join(__dirname, '..') 11 | 12 | if (args.indexOf('-v') >= 0 || args.indexOf('--version') >= 0) { 13 | const pkgF = path.join(root, 'package.json') 14 | const pkg = JSON.parse(fs.readFileSync(pkgF)) 15 | console.log(`OpenHIM Core version ${pkg.version}`) 16 | process.exit(0) 17 | } 18 | 19 | const child = new forever.Monitor('lib/server.js', { 20 | sourceDir: root, 21 | command: 'node', 22 | args, 23 | watch: process.env.NODE_ENV !== 'production', 24 | watchDirectory: 'lib' 25 | }) 26 | 27 | child.on('watch:restart', info => { 28 | console.error(`Restarting script because ${info.file} changed`) 29 | }) 30 | 31 | child.on('restart', () => { 32 | console.error(`Forever restarting script for ${child.times} time`) 33 | }) 34 | 35 | child.on('exit:code', code => { 36 | console.error(`Forever detected script exited with code ${code}`) 37 | }) 38 | 39 | child.start() 40 | -------------------------------------------------------------------------------- /performance/mediator/tcp-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BodyStream = require('./body-stream') 4 | 5 | const DELAY = +(process.env.DELAY || 500) 6 | 7 | function sendHttpHeaders (conn) { 8 | conn.write('HTTP/1.1 200 OK\r\n') 9 | conn.write('Connection: close\r\n') 10 | conn.write('Content-Encoding: identity\r\n') 11 | conn.write('Content-Type: text/plain; charset=utf-8\r\n') 12 | conn.write('\r\n') 13 | } 14 | 15 | exports.handleBodyRequest = (conn) => { 16 | conn.on('error', console.error) 17 | conn.once('data', () => { 18 | sendHttpHeaders(conn) 19 | new BodyStream(2 * 1024 * 1024).pipe(conn) 20 | }) 21 | } 22 | 23 | exports.handleDelayRequest = (conn) => { 24 | conn.on('error', console.error) 25 | conn.once('data', () => { 26 | sendHttpHeaders(conn) 27 | conn.write('Delay start') 28 | setTimeout(() => { 29 | conn.end(`Waited for ${DELAY}ms`) 30 | }, DELAY) 31 | }) 32 | } 33 | 34 | exports.handleImmediateRequest = (conn) => { 35 | conn.on('error', console.error) 36 | conn.once('data', () => { 37 | sendHttpHeaders(conn) 38 | conn.end(`Immediate tcp response`) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/model/keystore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | const certificate = { 8 | country: String, 9 | state: String, 10 | locality: String, 11 | organization: String, 12 | organizationUnit: String, 13 | commonName: String, 14 | emailAddress: String, 15 | validity: { 16 | start: Date, 17 | end: Date 18 | }, 19 | data: String, 20 | fingerprint: String 21 | } 22 | 23 | const CertificateSchema = new Schema(certificate) 24 | 25 | const KeystoreSchema = new Schema({ 26 | key: String, 27 | passphrase: String, 28 | cert: certificate, 29 | ca: [certificate] 30 | }) 31 | 32 | // Model for storing the server key and cert as well as trusted certificates 33 | export const KeystoreModelAPI = connectionAPI.model('Keystore', KeystoreSchema) 34 | export const CertificateModelAPI = connectionAPI.model( 35 | 'Certificate', 36 | CertificateSchema 37 | ) 38 | export const KeystoreModel = connectionDefault.model('Keystore', KeystoreSchema) 39 | export const CertificateModel = connectionDefault.model( 40 | 'Certificate', 41 | CertificateSchema 42 | ) 43 | -------------------------------------------------------------------------------- /test/resources/replica-set-test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | replica1: 5 | container_name: replica1 6 | image: khezen/mongo:slim 7 | environment: 8 | RS_NAME: repset1 9 | volumes: 10 | - ./mongo-volumes/replica1:/data/db 11 | 12 | replica2: 13 | container_name: replica2 14 | image: khezen/mongo:slim 15 | environment: 16 | RS_NAME: repset1 17 | volumes: 18 | - ./mongo-volumes/replica2:/data/db 19 | 20 | replica3: 21 | container_name: replica3 22 | image: khezen/mongo:slim 23 | depends_on: 24 | - replica1 25 | - replica2 26 | environment: 27 | RS_NAME: repset1 28 | MASTER: replica3 29 | SLAVES: replica1 replica2 30 | volumes: 31 | - ./mongo-volumes/replica3:/data/db 32 | 33 | replica-core: 34 | build: ../../../ 35 | container_name: replica-core 36 | depends_on: 37 | - replica3 38 | command: /bin/bash -c "npm run build && npm test" 39 | environment: 40 | - mongo_url=mongodb://replica3:27017/openhim-test?replicaSet=repset1 41 | - mongo_atnaUrl=mongodb://replica3:27017/openhim-test?replicaSet=repset1 -------------------------------------------------------------------------------- /src/jwtSecretOrPublicKeyCache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | import * as configIndex from './config' 7 | 8 | let secretOrPublicKey = null 9 | 10 | export const populateCache = () => { 11 | let secretOrPublicKeyConfig = configIndex.config.get( 12 | 'authentication:jwt:secretOrPublicKey' 13 | ) 14 | 15 | try { 16 | const publicKeyFilePath = path.resolve( 17 | __dirname, 18 | '..', 19 | 'resources', 20 | 'certs', 21 | 'jwt', 22 | secretOrPublicKeyConfig 23 | ) 24 | 25 | // Check file exists 26 | if ( 27 | fs.existsSync(publicKeyFilePath) && 28 | fs.lstatSync(publicKeyFilePath).isFile() 29 | ) { 30 | secretOrPublicKey = fs.readFileSync(publicKeyFilePath).toString() 31 | } else { 32 | secretOrPublicKey = secretOrPublicKeyConfig 33 | } 34 | } catch (error) { 35 | throw new Error(`Could not read JWT public key file: ${error.message}`) 36 | } 37 | } 38 | 39 | export const getSecretOrPublicKey = () => { 40 | return secretOrPublicKey 41 | } 42 | 43 | export const clearSecretOrPublicKey = () => { 44 | secretOrPublicKey = null 45 | } 46 | -------------------------------------------------------------------------------- /src/api/heartbeat.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import moment from 'moment' 4 | import {promisify} from 'util' 5 | 6 | import * as server from '../server' 7 | import * as utils from '../utils' 8 | import {MediatorModelAPI} from '../model/mediators' 9 | 10 | export async function getHeartbeat(ctx) { 11 | try { 12 | const uptime = promisify(server.getUptime) 13 | const result = await uptime() 14 | 15 | const mediators = await MediatorModelAPI.find().exec() 16 | for (const mediator of Array.from(mediators)) { 17 | if (!result.mediators) { 18 | result.mediators = {} 19 | } 20 | 21 | if ( 22 | mediator._lastHeartbeat != null && 23 | mediator._uptime != null && 24 | // have we received a heartbeat within the last minute? 25 | moment().diff(mediator._lastHeartbeat, 'seconds') <= 60 26 | ) { 27 | result.mediators[mediator.urn] = mediator._uptime 28 | } else { 29 | result.mediators[mediator.urn] = null 30 | } 31 | } 32 | 33 | result.now = Date.now() 34 | ctx.body = result 35 | } catch (e) { 36 | return utils.logAndSetResponse(ctx, 500, `Error: ${e}`, 'error') 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/tags.yml: -------------------------------------------------------------------------------- 1 | name: Push OpenHIM Core Docker Image On Tag 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v2 22 | 23 | - name: Set env 24 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 25 | 26 | - name: Login to Docker Hub 27 | uses: docker/login-action@v2 28 | with: 29 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 30 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 31 | 32 | - name: Set up Docker Buildx 33 | id: buildx 34 | uses: docker/setup-buildx-action@v2 35 | 36 | - name: Build and push 37 | id: docker_build 38 | uses: docker/build-push-action@v4 39 | with: 40 | context: ./ 41 | file: ./Dockerfile 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: jembi/openhim-core:${{ env.RELEASE_VERSION }} 45 | -------------------------------------------------------------------------------- /src/config/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import mongoose from 'mongoose' 4 | import uriFormat from 'mongodb-uri' 5 | 6 | import {config} from './' 7 | 8 | config.mongo = config.get('mongo') 9 | 10 | export const connectionAgenda = mongoose.createConnection( 11 | encodeMongoURI(config.mongo.url) 12 | ) 13 | export const connectionAPI = mongoose.createConnection( 14 | encodeMongoURI(config.mongo.url), 15 | getMongoOptions() 16 | ) 17 | export const connectionATNA = mongoose.createConnection( 18 | encodeMongoURI(config.mongo.atnaUrl) 19 | ) 20 | export const connectionDefault = mongoose.createConnection( 21 | encodeMongoURI(config.mongo.url) 22 | ) 23 | 24 | function encodeMongoURI(urlString) { 25 | if (urlString) { 26 | let parsed = uriFormat.parse(urlString) 27 | urlString = uriFormat.format(parsed) 28 | } 29 | return urlString 30 | } 31 | 32 | function getMongoOptions() { 33 | return { 34 | readPreference: config.mongo.openHIMApiReadPreference, 35 | readConcern: {level: config.mongo.openHIMApiReadConcern}, 36 | w: config.mongo.openHIMApiWriteConcern 37 | } 38 | } 39 | 40 | if (process.env.NODE_ENV === 'test') { 41 | exports.encodeMongoURI = encodeMongoURI 42 | } 43 | -------------------------------------------------------------------------------- /test/unit/proxyTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | import * as proxy from '../../src/middleware/proxy' 6 | 7 | describe('Proxy', () => { 8 | const ctx = {} 9 | ctx.header = {} 10 | ctx.request = {} 11 | ctx.request.ip = '192.168.1.42' 12 | ctx.request.host = 'localhost:5000' 13 | ctx.request.protocol = 'https' 14 | 15 | describe('.setupProxyHeaders', () => { 16 | it('should set the X-Forwarded-* headers if not present', done => { 17 | delete ctx.header['X-Forwarded-For'] 18 | delete ctx.header['X-Forwarded-Host'] 19 | proxy.setupProxyHeaders(ctx) 20 | ctx.header['X-Forwarded-For'].should.equal('192.168.1.42') 21 | ctx.header['X-Forwarded-Host'].should.equal('localhost:5000') 22 | return done() 23 | }) 24 | 25 | it('should append values to the X-Forwarded-* headers if already present', done => { 26 | ctx.header['X-Forwarded-For'] = '192.168.2.34' 27 | ctx.header['X-Forwarded-Host'] = 'someserver.com' 28 | proxy.setupProxyHeaders(ctx) 29 | ctx.header['X-Forwarded-For'].should.equal('192.168.2.34, 192.168.1.42') 30 | ctx.header['X-Forwarded-Host'].should.equal( 31 | 'someserver.com, localhost:5000' 32 | ) 33 | return done() 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/migrateMetrics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Progress from 'progress' 4 | import logger from 'winston' 5 | 6 | import {MetricModel, TransactionModel} from './model' 7 | import {recordTransactionMetrics} from './metrics' 8 | 9 | export async function aggregateTransactionToMetrics() { 10 | await MetricModel.deleteMany() 11 | const query = {response: {$exists: true}} 12 | const totalTrans = await TransactionModel.countDocuments(query) 13 | if (totalTrans === 0) { 14 | logger.info('No transactions to aggregate to metrics, skipping.') 15 | return 16 | } 17 | 18 | const transactionProgress = new Progress( 19 | `Aggregating transactions [:bar] :rate/trans per sec :percent :etas`, 20 | { 21 | total: totalTrans 22 | } 23 | ) 24 | const cursor = TransactionModel.find(query).batchSize(100).cursor() 25 | let transaction = await cursor.next() 26 | logger.log(`transactions`, transaction) 27 | while (transaction != null) { 28 | await recordTransactionMetrics(transaction) 29 | transactionProgress.tick() 30 | transaction = await cursor.next() 31 | } 32 | } 33 | 34 | if (!module.parent) { 35 | aggregateTransactionToMetrics() 36 | .then(() => process.exit(0)) 37 | .catch(err => { 38 | console.err(err) 39 | process.exit(1) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /packaging/build-release-zip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | if (( $# < 1)); then 6 | echo "OpenHIM release build: Builds a specific tagged release ready for deployment"; 7 | echo "Usage: $0 TAG"; 8 | exit 0; 9 | fi 10 | 11 | tag=$1; 12 | shift; 13 | 14 | echo "NB!" 15 | echo "To create the tagged build, various git interactions need to take place. " 16 | echo "This will create a temporary branch as well as remove any changes you have havent yet committed" 17 | read -p "Do you wish to proceed? [Y/y]" -n 1 -r 18 | 19 | echo "" 20 | 21 | if [[ $REPLY =~ ^[Yy]$ ]]; then 22 | cd ../ 23 | 24 | echo "Git: setup branch/tag" 25 | git checkout -- . 26 | git checkout master 27 | git pull origin master 28 | git fetch --tags 29 | git checkout tags/$tag -b "build-release-$tag" 30 | 31 | echo "npm: clean and build package" 32 | rm -rf node_modules 33 | npm install 34 | npm run build 35 | 36 | echo "zip: build release version: $tag" 37 | zip \ 38 | -i 'lib/*' 'config/*' 'node_modules/*' 'resources/*' 'CHANGELOG.md' 'LICENSE' 'package.json' 'package-lock.json' 'README.md' \ 39 | -r packaging/build.openhim-core.$tag.zip . 40 | 41 | echo "Git cleanup" 42 | git checkout -- . 43 | git checkout master 44 | git branch -D "build-release-$tag" 45 | 46 | echo "New OpenHIM Core build zipped"; 47 | fi 48 | -------------------------------------------------------------------------------- /src/kafkaProducerManager.js: -------------------------------------------------------------------------------- 1 | import {KafkaProducer} from './kafkaProducer.js' 2 | 3 | export class KafkaProducerManager { 4 | static kafkaSet = {} 5 | 6 | static async getProducer(channelName, clientId, timeout) { 7 | const kafkaInstance = this.findOrAddConnection( 8 | channelName, 9 | clientId, 10 | timeout 11 | ) 12 | if (!kafkaInstance.isConnected) await kafkaInstance.connect() 13 | 14 | return kafkaInstance.producer 15 | } 16 | 17 | static findOrAddConnection(channelName, clientId, timeout) { 18 | let kafkaInstance = this.getKafkaInstance(channelName, clientId, timeout) 19 | if (!kafkaInstance) { 20 | kafkaInstance = new KafkaProducer(clientId, timeout) 21 | this.kafkaSet[`urn:${channelName}:${clientId}:${timeout}`] = kafkaInstance 22 | } 23 | 24 | return kafkaInstance 25 | } 26 | 27 | static async removeConnection(channelName, clientId, timeout) { 28 | const kafkaInstance = this.getKafkaInstance(channelName, clientId, timeout) 29 | 30 | if (kafkaInstance) { 31 | if (kafkaInstance.isConnected) await kafkaInstance.disconnect() 32 | delete this.kafkaSet[`urn:${channelName}:${clientId}:${timeout}`] 33 | } 34 | } 35 | 36 | static getKafkaInstance(channelName, clientId, timeout) { 37 | return this.kafkaSet[`urn:${channelName}:${clientId}:${timeout}`] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/middleware/customTokenAuthentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | 5 | import * as client from '../model/clients' 6 | import {CUSTOM_TOKEN_PATTERN} from '../constants' 7 | 8 | async function authenticateClient(customTokenID) { 9 | return client.ClientModel.findOne({customTokenID}).then(client => { 10 | if (!client) { 11 | throw new Error('Client does not exist') 12 | } 13 | return client 14 | }) 15 | } 16 | 17 | async function authenticateToken(ctx) { 18 | if (ctx.authenticated) { 19 | return 20 | } 21 | 22 | const authHeader = ctx.request.header.authorization || '' 23 | const token = CUSTOM_TOKEN_PATTERN.exec(authHeader) 24 | 25 | if (!token) { 26 | logger.debug(`Missing or invalid Custom Token 'Authorization' header`) 27 | return 28 | } 29 | 30 | try { 31 | const client = await authenticateClient(token[1]) 32 | logger.info(`Client (${client.name}) is Authenticated`) 33 | ctx.authenticated = client 34 | ctx.authenticationType = 'token' 35 | } catch (error) { 36 | logger.error(`Custom Token could not be verified: ${error.message}`) 37 | return 38 | } 39 | } 40 | 41 | export async function koaMiddleware(ctx, next) { 42 | await authenticateToken(ctx) 43 | if (ctx.authenticated && ctx.authenticated.clientID) { 44 | ctx.header['X-OpenHIM-ClientID'] = ctx.authenticated.clientID 45 | } 46 | await next() 47 | } 48 | -------------------------------------------------------------------------------- /test/resources/server-tls/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDqDCCApACCQCLPx1aZMwI3DANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC 3 | WkExDDAKBgNVBAgMA0taTjEPMA0GA1UEBwwGRHVyYmFuMSEwHwYDVQQKDBhKZW1i 4 | aSBIZWFsdGggU3lzdGVtcyBOUEMxEDAOBgNVBAsMB2VIZWFsdGgxEjAQBgNVBAMM 5 | CWxvY2FsaG9zdDEdMBsGCSqGSIb3DQEJARYOcnlhbkBqZW1iaS5vcmcwIBcNMjQw 6 | NTE0MTE0NTA2WhgPMjEyNDA0MjAxMTQ1MDZaMIGUMQswCQYDVQQGEwJaQTEMMAoG 7 | A1UECAwDS1pOMQ8wDQYDVQQHDAZEdXJiYW4xITAfBgNVBAoMGEplbWJpIEhlYWx0 8 | aCBTeXN0ZW1zIE5QQzEQMA4GA1UECwwHZUhlYWx0aDESMBAGA1UEAwwJbG9jYWxo 9 | b3N0MR0wGwYJKoZIhvcNAQkBFg5yeWFuQGplbWJpLm9yZzCCASIwDQYJKoZIhvcN 10 | AQEBBQADggEPADCCAQoCggEBAL4844OVtH+851QC4byZxnUEqjoxj/uDxVEKEifl 11 | DZy2BpC9xyBNvXJ/7A8ZcUxuVd/eSDR7SPPEqaXBYwi4BFrnw2VS0GfgdTIWlTbn 12 | hxGZ1WQMWyarcW6dZpmoHrlCKXFmauDx/TttA6N8FItagn2XX4DtTQyG2Ze1zh9E 13 | VhE3ZO+MsSBx7RClqo2xh3XIVWpLQ908ugofZwwmYqN/HX9qbnad13E7UbGp5Kvh 14 | cQsyx0b7P7G9QXk945C8qblnjOQdcZELlaJac77b1vz5Bkp66qHmXwuqUYN81Rta 15 | 8TbP0mVJYPgTT7umpMY3MRqvOBeOtD/hRm45pzc2IczR4VsCAwEAATANBgkqhkiG 16 | 9w0BAQsFAAOCAQEASV/up52Ed+FZyWsu55/aoyJbEaa2feN+LYnpxPzmVzQNtYLm 17 | XzIN5OurSSxhEiYHSEoUSghZk8uaRVXcdMsTNaz0Mtp1Hthl7zPTXze82/+XOyu8 18 | M1V9GrFPkaeCeMB9MHiYCh/1wVRlR8K/gyBFiWqcSqsENJZvpDaWSbG8/0SrcW9P 19 | vrN2bgtSjGou5bGKh4Vo+nTuiyWkSseiii6QiBlouqlzAThn7Hrp8CNNDJ7fY1VI 20 | x2WCyBshQBDEV910L26UV7xPWM+bRtd6s8oZ0zOVM1unVqHw+0wWJ1yuwdcB047C 21 | fgKf0/pfie1cMmnqZbukSZyX5dOmW4wa4LbGkA== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /src/api/authorisation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {ChannelModelAPI} from '../model/channels' 4 | import { RoleModelAPI } from '../model/role' 5 | 6 | export function inGroup(group, user) { 7 | return user.groups.indexOf(group) >= 0 8 | } 9 | 10 | const getUserChannelsByPermissions = (user, allPermission, specifiedPermission) => 11 | RoleModelAPI.find({name: {$in: user.groups}}).then(roles => { 12 | if (roles.find(role => role.permissions[allPermission] || role.permissions[allPermission.replace('view', 'manage')])) { 13 | return ChannelModelAPI.find({}).exec() 14 | } 15 | const specifiedChannels = roles.reduce((prev, curr) => 16 | prev.concat(curr.permissions[specifiedPermission], curr.permissions[specifiedPermission.replace('view', 'manage')]), 17 | [] 18 | ) 19 | return ChannelModelAPI.find({_id: {$in: specifiedChannels}}).exec() 20 | }) 21 | 22 | /** 23 | * A promise returning function that returns the list 24 | * of viewable channels for a user. 25 | */ 26 | export function getUserViewableChannels(user) { 27 | return getUserChannelsByPermissions(user, 'channel-view-all', 'channel-view-specified') 28 | } 29 | 30 | /** 31 | * A promise returning function that returns the list 32 | * of rerunnable channels for a user. 33 | */ 34 | export function getUserRerunableChannels(user) { 35 | return getUserChannelsByPermissions(user, 'transaction-rerun-all', 'transaction-rerun-specified') 36 | } 37 | -------------------------------------------------------------------------------- /test/unit/appsTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | import should from 'should' 5 | 6 | import {getApps, updateApp} from '../../src/api/apps' 7 | import {AppModelAPI} from '../../src/model/apps' 8 | 9 | describe('Apps', () => { 10 | afterEach(async () => { 11 | await AppModelAPI.deleteMany({}) 12 | }) 13 | 14 | describe('getApps', () => { 15 | it('should pass when retrieving from mongo fails', async () => { 16 | const ctx = { 17 | request: { 18 | query: {} 19 | } 20 | } 21 | 22 | await getApps(ctx) 23 | 24 | ctx.status.should.equal(200) 25 | }) 26 | }) 27 | 28 | describe('updateApps', () => { 29 | it('should fail when updating in mongo fails', async () => { 30 | const app = AppModelAPI({ 31 | name: 'Test app1', 32 | description: 'An app for testing the app framework', 33 | icon: 'data:image/png;base64, ', 34 | type: 'external', 35 | category: 'Operations', 36 | access_roles: ['test-app-user'], 37 | url: 'http://test-app.org/app1', 38 | showInPortal: true, 39 | showInSideBar: true 40 | }) 41 | await app.save() 42 | 43 | const ctx = { 44 | request: { 45 | body: {} 46 | }, 47 | status: 200 48 | } 49 | 50 | await updateApp(ctx, app._id) 51 | 52 | should.exist(ctx.body.error) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /resources/certs/ihe/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDyjCCArICAgUSMA0GCSqGSIb3DQEBBQUAMIHBMQswCQYDVQQGEwJVUzERMA8G 3 | A1UECAwITWlzc291cmkxEjAQBgNVBAcMCVN0LiBMb3VpczEsMCoGA1UECgwjTWFs 4 | bGluY2tyb2R0IEluc3RpdHV0ZSBvZiBSYWRpb2xvZ3kxITAfBgNVBAsMGEVsZWN0 5 | cm9uaWMgUmFkaW9sb2d5IExhYjETMBEGA1UEAwwKTUlSMjAxNC0xNjElMCMGCSqG 6 | SIb3DQEJARYWbW91bHRvbnJAbWlyLnd1c3RsLmVkdTAeFw0xNDExMjUxMjUyMjFa 7 | Fw0xNjEwMzAxMjUyMjFaMIGSMQswCQYDVQQGEwJaQTEMMAoGA1UECAwDS1pOMQ8w 8 | DQYDVQQHDAZEdXJiYW4xITAfBgNVBAoMGEplbWJpIEhlYWx0aCBTeXN0ZW1zIE5Q 9 | QzEQMA4GA1UECwwHZUhlYWx0aDEQMA4GA1UEAwwHb3BlbmhpbTEdMBsGCSqGSIb3 10 | DQEJARYOcnlhbkBqZW1iaS5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 11 | AoIBAQCZqBw2xOE60yTogFtvf80JVeRcQlfkpxpmCPgKFcGNotBLJHCTPzW9z1w/ 12 | 6F3PsNBVKiD+LR3ALep7qcmPkA/9wVVun6ZpmzdVK8/Y0/HVCU+DLS0LpthS+w10 13 | H+RKWqEHceGkooGkVflFlUlfVu0EFNoob7jsljroYLnmdk7F6Kopy7SV2ylhtcqp 14 | Ucn+ePcArBCPGnqTYoskAEJtewkMUzELprIhA92n8wzICvQyhot5/Zmlb/Hg3hkf 15 | Y8+wxi7sy5jrFG2kWNLu20QCle6MCZsgMhpWl3thV5ysTxqNbRp6A2AzhhsIK441 16 | ZY9+J/XxytdVEGAkt4Pwcey1jZRvAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBADxp 17 | pP/hR5Vx0+FbucPD77GJGmyGUYRM4fselmPRDl6hE3ZKKAy6X/tanivPIf/xIbXj 18 | a1GrVnzz86N87sRNSXphj1egmyxF5fFuwANTUqoVcV6x+1ocSMlRhve/og4iH4+t 19 | UMb/PWv5B089fYPTS+U+IeBtRW4/aZNqn2ZuajGIXVAtQlTvpBatWKmwddZ5YBTt 20 | mcYj6QFUJgTHWyiPEnUjqXI9+kCRVpVWMr+/OvotuidWYog1y1y1hvBne5l/MyCl 21 | h6fkNfKRqir3pGPdyq7Kahjyv+vUbsra/swJoxGaf8R0NBhIiDllSHuLKbwlNrSU 22 | 698YfmRSmz1qHOmkLoY= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /src/model/events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | export const eventTypes = ['channel', 'primary', 'route', 'orchestration'] 8 | 9 | // Active transaction events 10 | // 11 | // A short term collection for functions that require 'live' analysis of transactions 12 | // e.g. alerting and the visualizer 13 | // 14 | // Events are more fine-grained than individual transactions 15 | // 16 | const EventsSchema = new Schema({ 17 | created: { 18 | type: Date, 19 | default: Date.now, 20 | expires: '1h' 21 | }, 22 | channelID: { 23 | type: Schema.Types.ObjectId, 24 | required: true 25 | }, 26 | transactionID: { 27 | type: Schema.Types.ObjectId, 28 | required: true 29 | }, 30 | type: { 31 | type: String, 32 | enum: exports.EventTypes 33 | }, 34 | event: { 35 | type: String, 36 | enum: ['start', 'end'] 37 | }, 38 | name: String, 39 | status: Number, 40 | statusType: { 41 | type: String, 42 | enum: ['success', 'error'] 43 | }, // status string supported by visualizer (e.g. 'error' is red) 44 | normalizedTimestamp: String, 45 | mediator: String, 46 | autoRetryAttempt: Number 47 | }) 48 | 49 | EventsSchema.index({created: 1}, {expireAfterSeconds: 3600}) 50 | 51 | export const EventModelAPI = connectionAPI.model('Event', EventsSchema) 52 | export const EventModel = connectionDefault.model('Event', EventsSchema) 53 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo": { 3 | "url": "mongodb://localhost/openhim-test", 4 | "atnaUrl": "mongodb://localhost/openhim-test" 5 | }, 6 | "logger": { 7 | "level": "fatal", 8 | "logToDB": true, 9 | "capDBLogs": false 10 | }, 11 | "authentication": { 12 | "enableCustomTokenAuthentication": true, 13 | "enableJWTAuthentication": true, 14 | "jwt": { 15 | "secretOrPublicKey": "test", 16 | "algorithms": "HS256", 17 | "issuer": "test" 18 | } 19 | }, 20 | "api": { 21 | "enabled": true, 22 | "protocol": "https", 23 | "port": 8080, 24 | "authWindowSeconds": 50, 25 | "maxBodiesSizeMB": 15, 26 | "maxPayloadSizeMB": 50, 27 | "truncateSize": 10, 28 | "truncateAppend": "\n[truncated ...]", 29 | "authenticationTypes": ["token", "basic", "local", "openid"], 30 | "openid": { 31 | "url": "http://localhost:10000/realms/realm", 32 | "callbackUrl": "http://localhost:10010", 33 | "clientId": "client-id", 34 | "clientSecret": "client-secret", 35 | "scope": "openid email profile offline_access roles" 36 | } 37 | }, 38 | "caching": { 39 | "enabled": false 40 | }, 41 | "agenda": { 42 | "startupDelay": 0 43 | }, 44 | "certificateManagement": { 45 | "watchFSForCert": false, 46 | "certPath": "resources/certs/default/cert.pem", 47 | "keyPath": "resources/certs/default/key.pem" 48 | }, 49 | "openhimConsoleBaseUrl": "http://localhost:9000" 50 | } 51 | -------------------------------------------------------------------------------- /test/resources/trust-tls/cert1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID5zCCAs+gAwIBAgIJAOAcCQL7ADuMMA0GCSqGSIb3DQEBCwUAMIGJMQswCQYD 3 | VQQGEwJaQTEMMAoGA1UECAwDS1pOMQ8wDQYDVQQHDAZEdXJiYW4xFTATBgNVBAoM 4 | DFRydXN0ZWQgSW5jLjEMMAoGA1UECwwDQUJDMRMwEQYDVQQDDAp0cnVzdDEub3Jn 5 | MSEwHwYJKoZIhvcNAQkBFhJ0cnVzdDFAdHJ1c3RlZC5vcmcwHhcNMTUwMjExMTAz 6 | MTU1WhcNNDIwNjI5MTAzMTU1WjCBiTELMAkGA1UEBhMCWkExDDAKBgNVBAgMA0ta 7 | TjEPMA0GA1UEBwwGRHVyYmFuMRUwEwYDVQQKDAxUcnVzdGVkIEluYy4xDDAKBgNV 8 | BAsMA0FCQzETMBEGA1UEAwwKdHJ1c3QxLm9yZzEhMB8GCSqGSIb3DQEJARYSdHJ1 9 | c3QxQHRydXN0ZWQub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 10 | 2dDFBEwnsVWrdDuP+D5LOgELcOHwAv7J2rEsIvaXI6gHcRuOAXIoFbezFrj8vyQJ 11 | lS0fu1aDbH8opzxuISOPuTvywZqGoIpp++fVKulTXa4cx8VimqdiS1JGb++yLaM+ 12 | owTcHV44ODxEk4+q9hWRJUJBBxcmrDgwcQOXr/bz6UI/G1703A/Bx3FzRuujnpy5 13 | La+aW+Ytad625L+5RKhl0vLCRDJw0QufSCK3+v1naKH9i6jzv8SHJZzZJHkge1n6 14 | s/UiIYkd2upAYJc+HeVIQQ51uqn/38WvGfgbgo0wGOr5DCNQDZJt4i05dzjoDqcK 15 | BHK0q9qt3nrbTWeJd52opwIDAQABo1AwTjAdBgNVHQ4EFgQUNI/P2We0k9nh9B70 16 | UwFwdGmpmaowHwYDVR0jBBgwFoAUNI/P2We0k9nh9B70UwFwdGmpmaowDAYDVR0T 17 | BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA00K9xIz71HODqeltX+kW/XcDeNyl 18 | V3rRj9xBfh/BrgkMiJ4t9p2U1eaEpaxpMdPboQlDfDV3XMhtT0DeoPLHASu4YJt9 19 | COr8A7dhdbK9c9c6U+9uak7hNwpq7n0QSDVpEjOqfdt45LOMMfWbZgh7qPCc3dlw 20 | NfsqR0PAK91H5SJVnoq350q4JWqbbDg2+su6UAqlp2ZuOM+Ub2Mso08wMzDaC9P2 21 | F1svLWCKXBatR9tUEqt3egqruwbvoN2OR0vVjGJgRxbOXfktv9SuQOXgklIfBQsO 22 | Cyk9vxzIPUrTN9UqGpOj8UauP0wD9aFXf8PAoN5blvSoVxDC92BTGbI/TQ== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /test/resources/client-tls/invalid-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID5zCCAs+gAwIBAgIJAMUOBt7nRnSAMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYD 3 | VQQGEwJaQTEMMAoGA1UECAwDS1pOMQ8wDQYDVQQHDAZEdXJiYW4xDzANBgNVBAoM 4 | Bk1hbGljZTEQMA4GA1UECwwHSGFja2VyczEZMBcGA1UEAwwQaGFja2Vycy5mYWtl 5 | Lm9yZzEcMBoGCSqGSIb3DQEJARYNbm9wZUBmYWtlLm9yZzAgFw0yNDA1MTQxMTM2 6 | MzZaGA8yMTI0MDQyMDExMzYzNlowgYgxCzAJBgNVBAYTAlpBMQwwCgYDVQQIDANL 7 | Wk4xDzANBgNVBAcMBkR1cmJhbjEPMA0GA1UECgwGTWFsaWNlMRAwDgYDVQQLDAdI 8 | YWNrZXJzMRkwFwYDVQQDDBBoYWNrZXJzLmZha2Uub3JnMRwwGgYJKoZIhvcNAQkB 9 | Fg1ub3BlQGZha2Uub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 10 | 1OOYSwhCvvdpb+Qj8mixGjqNapKCNoQiyIeki3GOq61kFVxgFhse3IO+TWemT6M0 11 | 66budSuJNNAZ/SOeLf/uMZoPWTHFF6S7L6jEYpy9nTJHevGhBmM+CYSq7ryzlkcF 12 | GUtrWXQAzRARelmhZeZ7jWpqgLD3Z4R9lc3IVpqFY8aIEMSVXc1re2o5H5Ps4HHU 13 | 7JqM8XKUspfJ77v0yzYyStlPe+ByOIld/o7AEv1EgAdiOu+r7j6/snwfB3LK7APu 14 | gqNgXIT1gK5cYIbzk0AXMzTP2E3zl4weSPM2WNTiMam5Lp+lgKBDeR/KRD+lWYID 15 | FPwZW8y6YiiW85WV2Ar5GQIDAQABo1AwTjAdBgNVHQ4EFgQUqNcGn+6Kif79mwAA 16 | C3lDMU9p1QYwHwYDVR0jBBgwFoAUqNcGn+6Kif79mwAAC3lDMU9p1QYwDAYDVR0T 17 | BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADm+0KFF3ZMhEuXAM9sQY/7t3LT97 18 | oQhe1g3PcNTmaHffrsYC0YgErAnslMwKtTOfKHDdXLMCMMKN8yi8S5jvwMU8ogUk 19 | pw34ACEvzRA/YisB6zHnpD9CRrj4cDtf5IAJ33GSGjOmgYEf3dVG/Th6XY7YC2M0 20 | 3NRdEs5DJVYCuXS9u/h/samzmvKudGf/VaU/lbhtVvrBHs8TsSweTX5YLsX+Y2Il 21 | MuA+8+Yp6XVHG2jllP4xLfr68dPfTesvSfKrTU9W/kG9oqYSfyHKjWXpmOIUElHF 22 | 2axWEbJzbDrFyBSnkiZKhkxJtRJF27TuJ6upk3c0T/mBOrUo0xQ4GqxMDg== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /test/resources/trust-tls/cert2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+zCCAuOgAwIBAgIJAMgppX+B19v1MA0GCSqGSIb3DQEBCwUAMIGTMQswCQYD 3 | VQQGEwJaQTEVMBMGA1UECAwMV2VzdGVybiBDYXBlMRIwEAYDVQQHDAlDYXBlIFRv 4 | d24xEjAQBgNVBAoMCUFCQyBUcnVzdDEMMAoGA1UECwwDQ0JBMRMwEQYDVQQDDAp0 5 | cnVzdDIub3JnMSIwIAYJKoZIhvcNAQkBFhN0cnVzdDJAYWJjdHJ1c3Qub3JnMB4X 6 | DTE1MDIxMTEwMzAyOFoXDTQyMDYyOTEwMzAyOFowgZMxCzAJBgNVBAYTAlpBMRUw 7 | EwYDVQQIDAxXZXN0ZXJuIENhcGUxEjAQBgNVBAcMCUNhcGUgVG93bjESMBAGA1UE 8 | CgwJQUJDIFRydXN0MQwwCgYDVQQLDANDQkExEzARBgNVBAMMCnRydXN0Mi5vcmcx 9 | IjAgBgkqhkiG9w0BCQEWE3RydXN0MkBhYmN0cnVzdC5vcmcwggEiMA0GCSqGSIb3 10 | DQEBAQUAA4IBDwAwggEKAoIBAQCyMfaA1RRvD1L37bXBG8pgnpRM4Nu1ri/CDHIu 11 | vNGmGHWQBqRnyI6X1TfvlsRJCEDDtVhgyd3tyl/KxIbI8ejSDedrgsi2yB4dTKEe 12 | xW2pf4QgMldAcD1of4oXjs2PJgyOiZERjh9jU+dZqju7Fdblt5JltYEwXJI9IIM5 13 | AJxy50SfgdE36/6jt7v8GhtNlw82n4sUMXvllp0qFQiZbVyYYCc4u1Xv823lVfDL 14 | QbDqL1TH90cNOOZnyHWXilB3d7RI4KExxghnmqtkVjeKcy1rOjzlMGcvrC/9qHHm 15 | 1yMhQ+vES3iS77qa9NZMtVkBDmNxNIHNHgoLTR90EhqtOVJxAgMBAAGjUDBOMB0G 16 | A1UdDgQWBBRZuYmhLVtE/u2xl4mCI1gW0GbCkTAfBgNVHSMEGDAWgBRZuYmhLVtE 17 | /u2xl4mCI1gW0GbCkTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAy 18 | f7gLxWrdifoq1YEQQKCFe9B4ADETx8CH2XNQBC6y8D0bu5zJrF5mXbVQT/PefxDb 19 | mMB6CF3+JZTHxJ0sYmA0jqJADGehWKMM2Ry9mEmM+ADy2sD2ApU8BotU7s2XOCth 20 | KlPEBOpJ9wSRLpQN+BhbAeY/NJ0AvawJpaocn8GAG/F3tDuchQpPQ1u1b5bw/jWM 21 | otfkbrk432R7OhmwFnkbWy6sraZvzO6bJJt4ubaXcrPrvAJ4WA/jE6vsZILoCDGh 22 | ncvt82dMvU8okmIyKG8mgY+EopGNXW/lfMkuRdicmWZ9lC4tFLQoTaMio+B9KmXf 23 | F9757AW/KNqu+HoMdFqx 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /resources/certs/default/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEADCCAuigAwIBAgIJAONzGh1FuqGCMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD 3 | VQQGEwJaQTEVMBMGA1UECAwMV2VzdGVybiBDYXBlMSQwIgYDVQQKDBtPcGVuSElN 4 | IERlZmF1bHQgQ2VydGlmaWNhdGUxEjAQBgNVBAMMCWxvY2FsaG9zdDE0MDIGCSqG 5 | SIb3DQEJARYlb3BlbmhpbS1pbXBsZW1lbnRlcnNAZ29vZ2xlZ3JvdXBzLmNvbTAe 6 | Fw0xNzEyMTMwNzA2NThaFw0zNzEyMDgwNzA2NThaMIGUMQswCQYDVQQGEwJaQTEV 7 | MBMGA1UECAwMV2VzdGVybiBDYXBlMSQwIgYDVQQKDBtPcGVuSElNIERlZmF1bHQg 8 | Q2VydGlmaWNhdGUxEjAQBgNVBAMMCWxvY2FsaG9zdDE0MDIGCSqGSIb3DQEJARYl 9 | b3BlbmhpbS1pbXBsZW1lbnRlcnNAZ29vZ2xlZ3JvdXBzLmNvbTCCASIwDQYJKoZI 10 | hvcNAQEBBQADggEPADCCAQoCggEBANATU5IKhCefWnS2exCZlCs6AuN0YnMH/9Bc 11 | 0Jf1InMgyFeFufR4wktQCCDp9BCB0CoYzM5To9UTDHsSA3VTTZhVm5cdHPl3Xjz3 12 | uUQQaRsFGlVXNXnkg0uMHwRAg3pzcXIE9s9k1v9mZHFZWLaZlypNjpBekz4nvtm0 13 | Vjxe9E+4JMunldIuCL7MNjSM+Eg6Mi/kcgH3m2clw2gVz1kQbshsp+Ou/iLv1Aao 14 | SCdu8sd/vWweTAT/9g2p1u2FASqn9kJ+oWuHvRNvRPNtByvS+eZKH6DhbIsz8r1a 15 | zfZMULCvz20Tk6BqJYYnk/KbjHyRiUJab+8Or3OFd6qk49RpkScCAwEAAaNTMFEw 16 | HQYDVR0OBBYEFEPrhGnwZAf3pnObFfKgQe5CSZzyMB8GA1UdIwQYMBaAFEPrhGnw 17 | ZAf3pnObFfKgQe5CSZzyMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD 18 | ggEBABz9lcSTasHRHwcka13MoVkjR9XuOelCq5wAYXaLh/1tJAKULwrDu9ryk5Uw 19 | Zn80f6YvYZG1EismMmAfayslAUX98aFme7qU1nQKdxtlLa5as9YmdWktt+DgarJQ 20 | eTkhlhgobrlcBVn6QGCU0clT8XRr8Hg1P/b1RY0kkUjHdxisoePgqHI/D185rZ5c 21 | Lg4qQUEZoo7OUs9g6EWD14LDbVahuS4cgtdwaW8BflBYTO2COpKXrp/rew4GQiw4 22 | yVg771t3oRcCt/xnZnDWKlNc9Q3bnax0s21PaHRwd2CVBVcd7TH/shDyQDUe6ush 23 | jTwrcEj33iR/tz9+NUeGKcOYcKI= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /src/model/mediators.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {ChannelDef, RouteDef} from './channels' 6 | import {connectionAPI, connectionDefault} from '../config' 7 | 8 | export const configParamTypes = [ 9 | 'string', 10 | 'bool', 11 | 'number', 12 | 'option', 13 | 'bigstring', 14 | 'map', 15 | 'struct', 16 | 'password' 17 | ] 18 | 19 | export const configDef = { 20 | param: String, 21 | displayName: String, 22 | description: String, 23 | type: { 24 | type: String, 25 | enum: exports.configParamTypes 26 | }, 27 | values: [{type: String}], 28 | template: {type: Array}, 29 | array: Boolean 30 | } 31 | 32 | // The properties prefixed with an '_' are internally used properties and shouldn't be set by the user 33 | const MediatorSchema = new Schema({ 34 | urn: { 35 | type: String, 36 | required: true, 37 | unique: true 38 | }, 39 | version: { 40 | type: String, 41 | required: true 42 | }, 43 | name: { 44 | type: String, 45 | required: true 46 | }, 47 | description: String, 48 | endpoints: [RouteDef], 49 | defaultChannelConfig: [ChannelDef], 50 | configDefs: [configDef], 51 | config: Object, 52 | _configModifiedTS: Date, 53 | _uptime: Number, 54 | _lastHeartbeat: Date 55 | }) 56 | 57 | // Model for describing a collection of mediators that have registered themselves with core 58 | export const MediatorModelAPI = connectionAPI.model('Mediator', MediatorSchema) 59 | export const MediatorModel = connectionDefault.model('Mediator', MediatorSchema) 60 | -------------------------------------------------------------------------------- /performance/load.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | import {check, sleep} from 'k6' 3 | 4 | const BASE_URL = __ENV.BASE_URL || 'http://localhost:5001/http' 5 | 6 | export const options = { 7 | stages: [ 8 | {duration: '30s', target: 100}, 9 | {duration: '1m'}, 10 | {duration: '30s', target: 0} 11 | ], 12 | thresholds: { 13 | http_req_duration: ['p(95)<600'] 14 | }, 15 | noVUConnectionReuse: true, 16 | discardResponseBodies: true 17 | } 18 | 19 | function makeGetRequest() { 20 | const response = http.get( 21 | `${BASE_URL}/mediator`, 22 | { 23 | headers: { 24 | Accept: 'application/json', 25 | Authorization: 'Basic cGVyZm9ybWFuY2U6cGVyZm9ybWFuY2U=' 26 | }, 27 | tags: { 28 | name: 'Get request' 29 | } 30 | } 31 | ) 32 | check(response, { 33 | 'status code is 200': r => r.status === 200 34 | }) 35 | } 36 | 37 | function makePostRequest() { 38 | const response = http.post( 39 | `${BASE_URL}/mediator`, 40 | '{"hello": "world"}', 41 | { 42 | headers: { 43 | Accept: 'application/json', 44 | Authorization: 'Basic cGVyZm9ybWFuY2U6cGVyZm9ybWFuY2U=', 45 | 'Content-Type': 'application/json' 46 | }, 47 | tags: { 48 | name: 'Post request' 49 | } 50 | } 51 | ) 52 | check(response, { 53 | 'status code is 200': r => r.status === 200 54 | }) 55 | } 56 | 57 | function think() { 58 | sleep(Math.random() * 0.5) 59 | } 60 | 61 | export default function() { 62 | makeGetRequest() 63 | think() 64 | makePostRequest() 65 | } 66 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import fs from 'fs' 4 | import logger from 'winston' 5 | import nconf from 'nconf' 6 | import path from 'path' 7 | 8 | export const appRoot = path.resolve(__dirname, '../..') 9 | 10 | /* 11 | * Define the default constructor 12 | */ 13 | function Config() { 14 | // Get the argument-value to use 15 | nconf.argv().env('_') 16 | 17 | // don't read NODE:ENV from nconf as it could be overwritten by any env var starting with 'NODE_' 18 | const environment = process.env.NODE_ENV 19 | 20 | const conf = nconf.get('conf') 21 | 22 | // Load the configuration-values 23 | // user specified config override 24 | if (conf) { 25 | if (!fs.existsSync(conf)) { 26 | logger.warn(`Invalid config path ${conf}`) 27 | } 28 | nconf.file('customConfigOverride', conf) 29 | } 30 | 31 | // environment override 32 | if (environment) { 33 | const envPath = `${appRoot}/config/${environment}.json` 34 | if (!fs.existsSync(envPath)) { 35 | logger.warn(`No config found for env ${environment} at path ${envPath}`) 36 | } 37 | nconf.file('environmentOverride', `${appRoot}/config/${environment}.json`) 38 | } 39 | 40 | // load the default config file 41 | nconf.file('default', `${appRoot}/config/default.json`) 42 | 43 | // Return the result 44 | } 45 | 46 | /* 47 | * This function return the value that was set in the key-value store 48 | */ 49 | Config.prototype.get = key => nconf.get(key) 50 | 51 | /* 52 | * This function constructs a new instanse of this class 53 | */ 54 | export const config = new Config() 55 | -------------------------------------------------------------------------------- /test/resources/client-tls/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEFzCCAv+gAwIBAgIJALAiF9OxCN0tMA0GCSqGSIb3DQEBCwUAMIGgMQswCQYD 3 | VQQGEwJaQTEMMAoGA1UECAwDS1pOMQ8wDQYDVQQHDAZEdXJiYW4xITAfBgNVBAoM 4 | GEplbWJpIEhlYWx0aCBTeXN0ZW1zIE5QQzEQMA4GA1UECwwHZUhlYWx0aDEeMBwG 5 | A1UEAwwVdGVzdC1jbGllbnQuamVtYmkub3JnMR0wGwYJKoZIhvcNAQkBFg5yeWFu 6 | QGplbWJpLm9yZzAgFw0yNDA1MTQxMjQ2MjhaGA8yMTI0MDQyMDEyNDYyOFowgaAx 7 | CzAJBgNVBAYTAlpBMQwwCgYDVQQIDANLWk4xDzANBgNVBAcMBkR1cmJhbjEhMB8G 8 | A1UECgwYSmVtYmkgSGVhbHRoIFN5c3RlbXMgTlBDMRAwDgYDVQQLDAdlSGVhbHRo 9 | MR4wHAYDVQQDDBV0ZXN0LWNsaWVudC5qZW1iaS5vcmcxHTAbBgkqhkiG9w0BCQEW 10 | DnJ5YW5AamVtYmkub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 11 | o8H15JT28BLDtr4OTv1P3Y+mR1Zw7LtO3gBBeh6kolHL+KndArE8ct6RKpFFDXaZ 12 | pmjtrGxy0HkSgjdsakP9r8a6RpLkETmu3mDvgbqIALbhHKB46/sfzpTTeXTpetPJ 13 | GCTYMbDktlVqONoVf3rfR6gGehM/EseY0cJX3gjhzVwQxmtxLqQ1cf/iO24skaNb 14 | L2mn0DQPst/96CgyZCoFDVFVzh/jc6ILKGsJUwxvZa4M9y2JdmuVeikHQ48ulA6/ 15 | dY4TN4qEDzwEtIfv4jKPoFQ9rMJ9VorJ+uprqJJSJdiPyUkUGE3j093mg8ctvphH 16 | YKJthlyAOEaZivKu6h9cFwIDAQABo1AwTjAdBgNVHQ4EFgQU+aZU1li6/YdrebVt 17 | G2TF92S4JowwHwYDVR0jBBgwFoAU+aZU1li6/YdrebVtG2TF92S4JowwDAYDVR0T 18 | BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAGFEJABaY2g7mgNylkQ9JETrcZEth 19 | pHuAwXaO15CWYgqz0JM5sZEz+RMlnjjZHGXea7GQXc9D7RlvGhIxlY+khtp5ua1P 20 | Bge4rfxLFZRdoNudzwzMbrCtBjth3ko9tai9YCqLupTLHKZcuVoU0RHnZ24eHY3p 21 | 42ftYqrNlKI1gLz2BLlY83I16gA9VuzDZ/VNmOA0SdR9ZlWIsES6Jth0ZN7oxJ9N 22 | zgV16J+FKwuauySlujHIqDp9yA6BQ59n8RYgPitzlQLL1VMTtGPigEwYxXt/h7GS 23 | HSLh91RLYJiZMGPHenh/ldvzj9VxWmmPAWPIeatgCf01TORJZiF+noWgbQ== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /src/model/tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI, connectionDefault} from '../config' 6 | 7 | const TaskSchema = new Schema({ 8 | status: { 9 | type: String, 10 | required: true, 11 | enum: ['Queued', 'Processing', 'Paused', 'Cancelled', 'Completed', "Pending Async"], 12 | default: 'Queued', 13 | index: true 14 | }, 15 | transactions: [ 16 | { 17 | tid: { 18 | type: String, 19 | required: true 20 | }, 21 | tstatus: { 22 | type: String, 23 | required: true, 24 | enum: ['Queued', 'Processing', 'Completed', 'Failed'], 25 | default: 'Queued' 26 | }, 27 | error: String, 28 | rerunID: String, 29 | rerunStatus: String 30 | } 31 | ], 32 | created: { 33 | type: Date, 34 | required: true, 35 | default: Date.now, 36 | index: true 37 | }, 38 | completedDate: Date, 39 | user: { 40 | type: String, 41 | required: true 42 | }, 43 | remainingTransactions: { 44 | type: Number, 45 | required: true 46 | }, 47 | totalTransactions: { 48 | type: Number, 49 | required: true 50 | }, 51 | batchSize: { 52 | type: Number, 53 | default: 1 54 | } 55 | }) 56 | 57 | /* 58 | * The task object that describes a specific task within the OpenHIM. 59 | * It provides some metadata describing a task and contains a number of transaction IDs. 60 | */ 61 | export const TaskModelAPI = connectionAPI.model('Task', TaskSchema) 62 | export const TaskModel = connectionDefault.model('Task', TaskSchema) 63 | -------------------------------------------------------------------------------- /src/api/logs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | import moment from 'moment' 5 | import {promisify} from 'util' 6 | 7 | import * as utils from '../utils' 8 | 9 | const levels = { 10 | debug: 1, 11 | info: 2, 12 | warn: 3, 13 | error: 4 14 | } 15 | 16 | export async function getLogs(ctx) { 17 | try { 18 | const authorised = await utils.checkUserPermission(ctx, 'getLogs', 'logs-view') 19 | 20 | if (!authorised) return 21 | 22 | let {query} = ctx.request || {} 23 | 24 | // default to info level logs 25 | if (query.level == null) { 26 | query.level = 'info' 27 | } 28 | 29 | const options = { 30 | from: 31 | query.from != null 32 | ? moment(query.from).toDate() 33 | : moment().subtract(5, 'minutes').toDate(), 34 | until: moment(query.until || undefined).toDate(), 35 | order: 'asc', 36 | start: parseInt(query.start, 10) || 0, 37 | limit: 100000 // limit: 0 doesn't work :/ 38 | } 39 | 40 | let results = await promisify(logger.query.bind(logger))(options) 41 | results = results.mongodb 42 | 43 | if (query.level != null) { 44 | results = results.filter(item => levels[item.level] >= levels[query.level]) 45 | } 46 | 47 | if (query.limit != null) { 48 | results.splice(query.limit, results.length - query.limit) 49 | } 50 | 51 | ctx.body = results 52 | ctx.status = 200 53 | } catch (err) { 54 | utils.logAndSetResponse( 55 | ctx, 56 | 500, 57 | `Could not get logs via the API: ${err}`, 58 | 'error' 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/middleware/sessionStore.js: -------------------------------------------------------------------------------- 1 | import {config, connectionAPI} from '../config' 2 | import {Schema} from 'mongoose' 3 | 4 | /** 5 | * Session Store 6 | * 7 | * To be able to store the session in a Mongo collection instead of just saving it in memory 8 | * 9 | */ 10 | 11 | const SessionSchema = new Schema({ 12 | _id: String, 13 | data: Object, 14 | updatedAt: { 15 | default: new Date(), 16 | type: Date 17 | } 18 | }) 19 | 20 | class MongooseStore { 21 | constructor() { 22 | this.session = connectionAPI.model('Session', SessionSchema) 23 | } 24 | 25 | /** 26 | * Override of the destroy, get and set functions used in session 27 | * 28 | */ 29 | 30 | async destroy(id) { 31 | const {session} = this 32 | return session.deleteOne({_id: id}) 33 | } 34 | 35 | async get(id) { 36 | const {session} = this 37 | const {data} = (await session.findById(id)) || {} 38 | return data 39 | } 40 | 41 | async set(id, data) { 42 | const {session} = this 43 | const record = {_id: id, data, updatedAt: new Date()} 44 | await session.findByIdAndUpdate(id, record, { 45 | upsert: true, 46 | writeConcern: {w: 'majority', wtimeout: 10000} 47 | }) 48 | return data 49 | } 50 | 51 | // This function is required by 'passport-openidconnect' 52 | verify = async function (req, handle, next) { 53 | var state = {handle} 54 | var ctx = { 55 | maxAge: config.api.maxAge, 56 | issued: '' 57 | } 58 | 59 | return next(null, ctx, state) 60 | } 61 | 62 | static create() { 63 | return new MongooseStore() 64 | } 65 | } 66 | 67 | export default MongooseStore 68 | -------------------------------------------------------------------------------- /test/unit/apiAuthenticationTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | import {_getEnabledAuthenticationTypesFromConfig} from '../../src/api/authentication' 6 | 7 | describe('API authentication', () => { 8 | describe('getEnabledAuthenticationTypesFromConfig', () => { 9 | it('returns authentication types if configured as an array', () => { 10 | const authenticationTypes = ['token'] 11 | const enabledTypes = _getEnabledAuthenticationTypesFromConfig({ 12 | api: { 13 | authenticationTypes 14 | } 15 | }) 16 | enabledTypes.should.deepEqual(authenticationTypes) 17 | }) 18 | 19 | it('returns authentication types if configured as a JSON array', () => { 20 | const authenticationTypes = ['token'] 21 | const enabledTypes = _getEnabledAuthenticationTypesFromConfig({ 22 | api: { 23 | authenticationTypes: JSON.stringify(authenticationTypes) 24 | } 25 | }) 26 | enabledTypes.should.deepEqual(authenticationTypes) 27 | }) 28 | 29 | it('returns an empty array if configured with JSON other than an array', () => { 30 | const enabledTypes = _getEnabledAuthenticationTypesFromConfig({ 31 | api: { 32 | authenticationTypes: '"basic"' 33 | } 34 | }) 35 | enabledTypes.should.deepEqual([]) 36 | }) 37 | 38 | it('returns an empty array if configured with invalid JSON', () => { 39 | const enabledTypes = _getEnabledAuthenticationTypesFromConfig({ 40 | api: { 41 | authenticationTypes: '[invalid, json]' 42 | } 43 | }) 44 | enabledTypes.should.deepEqual([]) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /resources/certs/ihe/cacert.MIR2014-16.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEVzCCAz+gAwIBAgIJAPgFhgCrjORNMA0GCSqGSIb3DQEBBQUAMIHBMQswCQYD 3 | VQQGEwJVUzERMA8GA1UECAwITWlzc291cmkxEjAQBgNVBAcMCVN0LiBMb3VpczEs 4 | MCoGA1UECgwjTWFsbGluY2tyb2R0IEluc3RpdHV0ZSBvZiBSYWRpb2xvZ3kxITAf 5 | BgNVBAsMGEVsZWN0cm9uaWMgUmFkaW9sb2d5IExhYjETMBEGA1UEAwwKTUlSMjAx 6 | NC0xNjElMCMGCSqGSIb3DQEJARYWbW91bHRvbnJAbWlyLnd1c3RsLmVkdTAeFw0x 7 | NDEwMDkxMzE1MjhaFw0xNjExMjkxMzE1MjhaMIHBMQswCQYDVQQGEwJVUzERMA8G 8 | A1UECAwITWlzc291cmkxEjAQBgNVBAcMCVN0LiBMb3VpczEsMCoGA1UECgwjTWFs 9 | bGluY2tyb2R0IEluc3RpdHV0ZSBvZiBSYWRpb2xvZ3kxITAfBgNVBAsMGEVsZWN0 10 | cm9uaWMgUmFkaW9sb2d5IExhYjETMBEGA1UEAwwKTUlSMjAxNC0xNjElMCMGCSqG 11 | SIb3DQEJARYWbW91bHRvbnJAbWlyLnd1c3RsLmVkdTCCASIwDQYJKoZIhvcNAQEB 12 | BQADggEPADCCAQoCggEBAN9p4XTyG+LCmQ77/DX1HDDIrrN42xEiS1XoQsmZEZa1 13 | P9zIhOqySdplRIdhliBx8Jw5WOtVh05Qt4kCxFMobRLi3BHgZ0JMTFqEA3y8D7ol 14 | Q6fL7uI8LgRaXI0pG8jg13kb2+evCZuvTEYsrbGnrgmSGm/xGDXx+lD1B9Q2/5cW 15 | 2ke4Cy31ZyFGcGlXjy5cEh/T0UGpVass9bmd3N6UYaby0vAkDm453qW3Wqbr6sI4 16 | NHradR0bs+3rTP6LcY+pOog1Hro9CX6zt6a5XnoZ5SDWjhKBEIWLrGPCre0yoXBZ 17 | av/HEpJmRTx0lFV1KeEfKsK8IAw76ysUctQ9Z1CPLgECAwEAAaNQME4wHQYDVR0O 18 | BBYEFE6qOZbWCJFFWHgClS7LOwT1uJlBMB8GA1UdIwQYMBaAFE6qOZbWCJFFWHgC 19 | lS7LOwT1uJlBMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKB5gWzB 20 | ZaS4wMsyJwtf6SkZO1jX9bMvsykZEHWBL9DvwckJRLZv89xxTOhOfDsciid+AFLG 21 | L5TzstyByn8+3k4I6OpOLwqHc04ocLggKZVJjKIw1O/J6kmblQKWXLDA7vQErnT2 22 | WH8WLXjpHzyWu57gPpCuF4nFosMDY0xf/v/K4GgpRDIBtShCkuNyLJd6FAVCxKVQ 23 | U7dR+lZmo+OyetOSq2SRmZPBjwqNeQLSMnMJhg49VsHNn93WEp4B/580YLL+2xae 24 | 8X1YgHPr8IjBDh8cMJrLjnYuR+lUO2P4hgws2hysXoLFNqIamYI+5jF2GbtzBjiC 25 | 85slwu0lnGA2c1U= 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /test/unit/tcpAdapterTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | /* eslint no-unused-expressions:0 */ 5 | 6 | import sinon from 'sinon' 7 | import {ObjectId} from 'mongodb' 8 | import {promisify} from 'util' 9 | 10 | import * as constants from '../constants' 11 | import * as tcpAdapter from '../../src/tcpAdapter' 12 | import {ChannelModel} from '../../src/model/channels' 13 | 14 | describe('TCP adapter tests', () => { 15 | const testChannel = new ChannelModel({ 16 | name: 'test', 17 | urlPattern: '/test', 18 | allow: '*', 19 | type: 'tcp', 20 | tcpPort: constants.PORT_START - 1, 21 | tcpHost: 'localhost', 22 | updatedBy: { 23 | id: new ObjectId(), 24 | name: 'Test' 25 | } 26 | }) 27 | 28 | const disabledChannel = new ChannelModel({ 29 | name: 'disabled', 30 | urlPattern: '/disabled', 31 | allow: '*', 32 | type: 'tcp', 33 | tcpPort: constants.PORT_START - 2, 34 | tcpHost: 'localhost', 35 | status: 'disabled', 36 | updatedBy: { 37 | id: new ObjectId(), 38 | name: 'Test' 39 | } 40 | }) 41 | 42 | before(async () => { 43 | await Promise.all([testChannel.save(), disabledChannel.save()]) 44 | }) 45 | 46 | after(async () => { 47 | await Promise.all([ 48 | promisify(tcpAdapter.stopServers)(), 49 | ChannelModel.deleteMany({}) 50 | ]) 51 | }) 52 | 53 | describe('.startupServers', () => 54 | it('should startup all enabled channels', async () => { 55 | const spy = sinon.spy(tcpAdapter, 'startupTCPServer') 56 | await promisify(tcpAdapter.startupServers)() 57 | spy.calledOnce.should.be.true 58 | spy.calledWith(testChannel._id) 59 | })) 60 | }) 61 | -------------------------------------------------------------------------------- /packaging/targets/trusty/debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # This is filled in when the packaging is built 5 | OPENHIM_VERSION= 6 | 7 | USERNAME=openhim 8 | HOME=/home/$USERNAME 9 | SH=/bin/bash 10 | CONFIGDIR=/etc/openhim 11 | CONFIGFILE=$CONFIGDIR/config.json 12 | 13 | . /usr/share/debconf/confmodule 14 | db_get openhim-core-js/watch-fs-for-cert 15 | WATCH=$RET 16 | if [ "$RET" = "true" ]; then 17 | db_get openhim-core-js/certPath 18 | CERT=$RET 19 | db_get openhim-core-js/keyPath 20 | KEY=$RET 21 | fi 22 | 23 | chown -R $USERNAME:$USERNAME $HOME/bin 24 | chown -R $USERNAME:$USERNAME $HOME/bin/install_node.sh 25 | chmod +x /usr/share/openhim-core/bin/openhim-core.js 26 | 27 | # Fetch OpenHIM config file 28 | mkdir /etc/openhim/ || true 29 | if [ ! -f $CONFIGFILE ]; then 30 | echo "Fetching shiny new config file from github ..." 31 | wget -O $CONFIGFILE https://raw.githubusercontent.com/jembi/openhim-core-js/v$OPENHIM_VERSION/config/default.json 32 | else 33 | echo "Config file "$CONFIGFILE" exits. Keeping old config, ensure you update this manually to support any new features in OpenHIM v"$OPENHIM_VERSION"!" 34 | fi 35 | 36 | if [ "$WATCH" = "true" ]; then 37 | sed -i -r '/watchFSForCert/s/false/true/' $CONFIGFILE 38 | sed -i -r '/certPath/s|/etc/letsencrypt/live/.*/cert.pem|'$CERT'|' $CONFIGFILE 39 | sed -i -r '/keyPath/s|/etc/letsencrypt/live/.*/privkey.pem|'$KEY'|' $CONFIGFILE 40 | fi 41 | 42 | cd $HOME 43 | 44 | # Install node using nvm as openhim user 45 | sudo -u $USERNAME $SH $HOME/bin/install_node.sh 46 | 47 | # Ensure service is started 48 | start openhim-core 49 | 50 | echo "To configure your installation edit the config file at $CONFIGFILE" 51 | 52 | exit 0 53 | -------------------------------------------------------------------------------- /src/model/metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionAPI} from '../config' 6 | 7 | export const METRIC_TYPE_MINUTE = 'm' 8 | export const METRIC_TYPE_HOUR = 'h' 9 | export const METRIC_TYPE_DAY = 'd' 10 | 11 | const MetricsSchema = new Schema({ 12 | startTime: { 13 | type: Date, 14 | required: true 15 | }, 16 | type: { 17 | type: String, 18 | enum: [METRIC_TYPE_MINUTE, METRIC_TYPE_HOUR, METRIC_TYPE_DAY], 19 | required: true 20 | }, 21 | channelID: { 22 | type: Schema.Types.ObjectId, 23 | required: true 24 | }, 25 | requests: { 26 | type: Number, 27 | default: 0 28 | }, 29 | // Sum of response time for all requests 30 | responseTime: { 31 | type: Number, 32 | default: 0 33 | }, 34 | minResponseTime: { 35 | type: Number, 36 | default: 0 37 | }, 38 | maxResponseTime: { 39 | type: Number, 40 | default: 0 41 | }, 42 | failed: { 43 | type: Number, 44 | default: 0 45 | }, 46 | successful: { 47 | type: Number, 48 | default: 0 49 | }, 50 | processing: { 51 | type: Number, 52 | default: 0 53 | }, 54 | completed: { 55 | type: Number, 56 | default: 0 57 | }, 58 | completedWithErrors: { 59 | type: Number, 60 | default: 0 61 | } 62 | }) 63 | 64 | // Expire minute buckets after an hour 65 | MetricsSchema.index('startTime', { 66 | expires: '1h', 67 | partialFilterExpression: { 68 | type: METRIC_TYPE_MINUTE 69 | } 70 | }) 71 | 72 | // Index for aggregation match stage 73 | MetricsSchema.index({ 74 | startTime: 1, 75 | channelID: 1, 76 | type: 1 77 | }) 78 | 79 | export const MetricModel = connectionAPI.model('Metric', MetricsSchema) 80 | -------------------------------------------------------------------------------- /src/bodyCull.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | import moment from 'moment' 5 | 6 | import {ChannelModel, TransactionModel} from './model' 7 | import {config} from './config' 8 | 9 | config.bodyCull = config.get('bodyCull') 10 | 11 | export function setupAgenda(agenda) { 12 | if (config.bodyCull == null) { 13 | return 14 | } 15 | agenda.define('transaction body culling', async (job, done) => { 16 | try { 17 | await cullBodies() 18 | done() 19 | } catch (err) { 20 | done(err) 21 | } 22 | }) 23 | agenda.every( 24 | `${config.bodyCull.pollPeriodMins} minutes`, 25 | `transaction body culling` 26 | ) 27 | } 28 | 29 | export async function cullBodies() { 30 | const channels = await ChannelModel.find({ 31 | maxBodyAgeDays: {$exists: true} 32 | }) 33 | await Promise.all(channels.map(channel => clearTransactions(channel))) 34 | } 35 | 36 | async function clearTransactions(channel) { 37 | const {maxBodyAgeDays, lastBodyCleared} = channel 38 | const maxAge = moment().subtract(maxBodyAgeDays, 'd').toDate() 39 | const query = { 40 | channelID: channel._id, 41 | 'request.timestamp': { 42 | $lte: maxAge 43 | } 44 | } 45 | 46 | if (lastBodyCleared != null) { 47 | query['request.timestamp'].$gte = lastBodyCleared 48 | } 49 | 50 | channel.lastBodyCleared = Date.now() 51 | channel.updatedBy = {name: 'Cron'} 52 | await channel.save() 53 | const updateResp = await TransactionModel.updateMany(query, { 54 | $unset: {'request.body': '', 'response.body': ''} 55 | }) 56 | if (updateResp.modifiedCount > 0) { 57 | logger.info( 58 | `Culled ${updateResp.modifiedCount} transactions for channel ${channel.name}` 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/integration/aboutAPITests.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | import request from 'supertest' 6 | import {promisify} from 'util' 7 | 8 | import * as server from '../../src/server' 9 | import * as testUtils from '../utils' 10 | import {BASE_URL, SERVER_PORTS} from '../constants' 11 | 12 | describe('API Integration Tests', () => 13 | describe('About Information REST Api Testing', () => { 14 | before(async () => { 15 | await promisify(server.start)({apiPort: SERVER_PORTS.apiPort}) 16 | await testUtils.setupTestUsers() 17 | }) 18 | 19 | after(async () => { 20 | await Promise.all([ 21 | promisify(server.stop)(), 22 | testUtils.cleanupTestUsers() 23 | ]) 24 | }) 25 | 26 | describe('*getAboutInformation', () => { 27 | it('should return status 401 when being unauthenticated', async () => { 28 | await request(BASE_URL).get('/about').expect(401) 29 | }) 30 | 31 | it('should fetch core version and return status 200', async () => { 32 | const user = testUtils.rootUser 33 | const cookie = await testUtils.authenticate(request, BASE_URL, user) 34 | 35 | const res = await request(BASE_URL) 36 | .get('/about') 37 | .set('Cookie', cookie) 38 | .expect(200) 39 | 40 | res.body.should.have.property('currentCoreVersion') 41 | }) 42 | 43 | it('should return 404 if not found', async () => { 44 | const user = testUtils.rootUser 45 | const cookie = await testUtils.authenticate(request, BASE_URL, user) 46 | 47 | await request(BASE_URL) 48 | .get('/about/bleh') 49 | .set('Cookie', cookie) 50 | .expect(404) 51 | }) 52 | }) 53 | })) 54 | -------------------------------------------------------------------------------- /infrastructure/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | networks: 3 | openhim: 4 | 5 | services: 6 | mongo-db: 7 | container_name: mongo-db 8 | image: mongo:4.0 9 | networks: 10 | - openhim 11 | volumes: 12 | - "mongo-data:/data/db" 13 | restart: unless-stopped 14 | 15 | openhim-core: 16 | container_name: openhim-core 17 | image: jembi/openhim-core:latest 18 | restart: unless-stopped 19 | environment: 20 | - mongo_url=mongodb://mongo-db/openhim-development 21 | - mongo_atnaUrl=mongodb://mongo-db/openhim-development 22 | - NODE_ENV=development 23 | - api_authenticationTypes=["token", "basic", "openid", "local"] 24 | - authentication_enableCustomTokenAuthentication=true 25 | - authentication_enableJWTAuthentication=true 26 | - authentication_jwt_secretOrPublicKey=secret 27 | - authentication_jwt_algorithms=HS256 28 | - authentication_jwt_issuer=openhim 29 | - openhimConsoleBaseUrl=http://localhost:9000 30 | ports: 31 | - "8080:8080" 32 | - "5000:5000" 33 | - "5001:5001" 34 | networks: 35 | - openhim 36 | healthcheck: 37 | test: "curl -sSk https://openhim-core:8080/heartbeat || exit 1" 38 | interval: 30s 39 | timeout: 30s 40 | retries: 3 41 | 42 | openhim-console: 43 | container_name: openhim-console 44 | image: jembi/openhim-console:latest 45 | restart: unless-stopped 46 | networks: 47 | - openhim 48 | ports: 49 | - "9000:80" 50 | healthcheck: 51 | test: "curl -sS http://openhim-console || exit 1" 52 | interval: 30s 53 | timeout: 30s 54 | retries: 3 55 | 56 | volumes: 57 | mongo-data: 58 | -------------------------------------------------------------------------------- /resources/certs/ihe/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAmagcNsThOtMk6IBbb3/NCVXkXEJX5KcaZgj4ChXBjaLQSyRw 3 | kz81vc9cP+hdz7DQVSog/i0dwC3qe6nJj5AP/cFVbp+maZs3VSvP2NPx1QlPgy0t 4 | C6bYUvsNdB/kSlqhB3HhpKKBpFX5RZVJX1btBBTaKG+47JY66GC55nZOxeiqKcu0 5 | ldspYbXKqVHJ/nj3AKwQjxp6k2KLJABCbXsJDFMxC6ayIQPdp/MMyAr0MoaLef2Z 6 | pW/x4N4ZH2PPsMYu7MuY6xRtpFjS7ttEApXujAmbIDIaVpd7YVecrE8ajW0aegNg 7 | M4YbCCuONWWPfif18crXVRBgJLeD8HHstY2UbwIDAQABAoIBAGNmmTnlCz8esFWo 8 | E36B0oYjhmMYl/tXDoDLdIoxoNCbbtvtatYWohF0P10oWOeunlc5tbY4QXK9QV9W 9 | awnwTCgAFeBGb0UKIDV87ix6j4sdRs65Pta2zGKB/iYNK4XeMyW5yhrS9cL1yxf0 10 | h06h0Ro+0xItlBeY5+rEB/7KshLce/vs+RZ5h+z1fgdM8tbeRHbnlbJHn516ZY6W 11 | CDYhp75EfecTSTxK+Ep5PaC4mpQ/WvcYj/0PskwVtd7/gKEe12fYRAdEqv57SAf1 12 | 5JVzrlEbDcMZUpHdWAxiLA1Ikk/a2B04FRaZMffIz4tdYyfkavlpfSWDpxkD6y5y 13 | w0iHfEECgYEAxxyXpxQCQm2PKxZc1cUt1CII9gy7Cbs9QinrYnaFpOJxjAo5+NXr 14 | G8uvI+xNCpRcny5OTw5FfjdvJmmZzvjRLa0FvxDRtvOvyUcGa6ADwFHTXRe/LCpT 15 | 1qsegmIv4Qs9Ck+bEF+w6aYGVxhsrHDdiw3ec/08bc3zc5vtgkO8JFcCgYEAxY7a 16 | H8tm9PjBbW1bQQ5qM4dAHrmOEbjnQ/JznG5ovFj0YuzKaYq9PLgZQSBlXv9SC9le 17 | 0YAmU1Ju9d6yQy86lk578WFxJYjLMwjeEYkYA9VvtazRMLhOHUY2Doe7quGWA5sI 18 | YqzJkmB2Rw2hGVEE1sWPW8oE/4XGI8iJn1gswakCgYEAtT3CkXQ2Jk9h/b+KW4/z 19 | imA8tfOaoV4Ngdd0ipkGR7NT1jxMzsOyEggS8WkcK6Amu0LER1PfT+eXxmhjeDGm 20 | /qO7UlM2AgnJnjaHkpoCMCKLefaurr4MuJ/k0pwbpwa61lLl/D6OY/AbX1Y9Pa4z 21 | C2thbUU6p1zL2RU7WdLcRFsCgYAtKZkHx7IDspJVd7hyyN7SZMrtuzGVL5X+3IA9 22 | dKgQ1q0XqFYlhof4R4XOc5949hlFiC266vbX+XJQVtadsUBNeCexaLs6/2ikhBRw 23 | Ic0Ro0tkVtuDrb59xr042SK3z9ZBgMGL/UAfbFndqGn5paHbj1JUg3xdBbHkIEYF 24 | 3Vxt8QKBgF1+XCzJgQIRLWIwqm6T/YXCR1eqeWmIsQzgli50YkLPH8vYKu6Jww0H 25 | A8JlJmm7ZfgmRbn/EUXRdlAtfETvZDYOT7WGOXb3T2ficMgDXjX/UD+8zG+RkQWs 26 | +B2oVa2ZCm4EPajxsIIAiYE4NhWM1nyO9Qh9dUfwKpkkjXwMi7OL 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/resources/server-tls/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAvjzjg5W0f7znVALhvJnGdQSqOjGP+4PFUQoSJ+UNnLYGkL3H 3 | IE29cn/sDxlxTG5V395INHtI88SppcFjCLgEWufDZVLQZ+B1MhaVNueHEZnVZAxb 4 | Jqtxbp1mmageuUIpcWZq4PH9O20Do3wUi1qCfZdfgO1NDIbZl7XOH0RWETdk74yx 5 | IHHtEKWqjbGHdchVaktD3Ty6Ch9nDCZio38df2pudp3XcTtRsankq+FxCzLHRvs/ 6 | sb1BeT3jkLypuWeM5B1xkQuVolpzvtvW/PkGSnrqoeZfC6pRg3zVG1rxNs/SZUlg 7 | +BNPu6akxjcxGq84F460P+FGbjmnNzYhzNHhWwIDAQABAoIBADF4viXTAaySYIod 8 | cbzNNKnru6UEjI/41ZBckRok2+RcBdsQLz98V/71u1q14BvjOZvNP3O+d9tE0mVT 9 | pEB5MlC9orev0Jii63dKHiW4Eg0Gt1KcT/v/a7E7Z5of+VVuGj7uRm8XHyacE+0o 10 | 8GLI99RXUDPEAlx2mRF16xOAfu1cycf8ym4I7AcC/rtckUD16a4HZWuitqEnj5AU 11 | yKvU5i9+KO0p/BRHcApUHvSQUq1Lcp9kOx1BQpwx+CY8DcBf7IAQCEiEy8o5M+Bh 12 | jaSAQvn7AYfHvaFbnr5JfjHg+eu2axBuwPfCEBiuyq0bXdpizyofcMw23cjEkKIH 13 | rMc1/QkCgYEA+jyjOYatIGrgOLxRNFML+ztc2zfmPjjmA+0ZyZdvbkKfjOQQ519O 14 | GqDy4r3P9KugXpCf6bWit6ClZ9DfsnNCl4Djfhmal8Ey92vFra3UJtYMCKwN0GXk 15 | QNRWpW89jg65DY40z14q6FUi5RkdaTRR0kAlVJGBVVUSrFp70w5JyU8CgYEAwp6B 16 | TnYS79PLdO/Vgg2GGjm66R5X93xahwLszW42vQklTNyDskFeofEnWBRDGJSnVZQF 17 | eB6tDby1UXZ35BYB6rU/Q2JV/qFYisu+EFf+JFoKs3ldPrw5387LZKFTx+A9PZhH 18 | 7xkiQFXIeols86jE/PtoTvfYwRy403+rSiZMjDUCgYEAhKJ5Xbdtsl4TxN7EJ/jQ 19 | w/Q+Do/9gQOFn5Uf8wNKc+XqTbJg51JTwreGpfo37Ja/pQABUs4MlkoXAgTrXNnM 20 | 9SLC8ga+MyubSRudpqYZahBQRpRzqf8n0dj6qCtjNBXHrlCu2y8tjQH7QfVi6m1p 21 | 5QhNARsyszkpcaNwOHk9UO8CgYANBlKdD6XgywnYv5xIYeNRqf9qTo+Qjo/4bSev 22 | 7+pV6GDIrhFaQqm0qND7Qg/jez9fqNJc4aCvRKvV0dngN+DU9mBPgCtFKUMlttD3 23 | gwwNHsGMlVi6NBqjcdGG4I0c2VL/5ksaI1J5f8Q0/zF9dRQJ4kFu14N4GRX9gZPT 24 | GJES6QKBgDAz6qJyqBIo+/PMugHCYl/fTB15BLs1eAK2qGc+XC69Laynptzg8gpQ 25 | rt7kH0tYccutDt9IQGRr2JPoHNjuo1UMlJnWm+U+twsxzahPRrN2zx8rEESzeeVH 26 | OgxlgV0XefX2ZBfOozDItdeSdcn7o0m+HVaLL3wCUNdNNFtuoX5B 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /resources/certs/default/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA0BNTkgqEJ59adLZ7EJmUKzoC43Ricwf/0FzQl/UicyDIV4W5 3 | 9HjCS1AIIOn0EIHQKhjMzlOj1RMMexIDdVNNmFWblx0c+XdePPe5RBBpGwUaVVc1 4 | eeSDS4wfBECDenNxcgT2z2TW/2ZkcVlYtpmXKk2OkF6TPie+2bRWPF70T7gky6eV 5 | 0i4Ivsw2NIz4SDoyL+RyAfebZyXDaBXPWRBuyGyn467+Iu/UBqhIJ27yx3+9bB5M 6 | BP/2DanW7YUBKqf2Qn6ha4e9E29E820HK9L55kofoOFsizPyvVrN9kxQsK/PbROT 7 | oGolhieT8puMfJGJQlpv7w6vc4V3qqTj1GmRJwIDAQABAoIBADp6pAswf3HvXGne 8 | +3lv4G/BrxwkEqNHxULXQxziHVuEBCptABVQdzLZfF9bwWd+RHYAYFdAtJPm9aQF 9 | LQmlbvJ4ZPE0kncecFLsQJp7jj600XDYwMPQUIJEEFeOliUtGt4zvPYO3koLigE9 10 | Xix7wDt4bq/ulK1sWDwxbecrvLhGg71Q1fW24tqGJUznYtynwhfE5U+qCTSLjye/ 11 | fELTCLArK4olHcxjwPY9gdeeBTqP0IryPad7TroK6xK+j+966xf8aiCfys711ZPq 12 | 2wPO7WtNTZd1iOXWA6cCN2yjggziYSDwbRby8OCcxlr+yNSbV/isL9dHzdkEcCQ8 13 | OdxNVFECgYEA9ZqyI1UVpnix8CC8iIGk6UlmuY0JVKS7oPMqyIH5jSSAKG6y6aP0 14 | FLhvk+ccgEhzU8QHGyIshggtHoLj1STxZcv/RQ/RE7/5Dmv+PqBe25zi8jZEa631 15 | qm/x70alvSYIDqDkPN3gl2Mw2EPuNntVhFHUY5Ru1v8n+Lf2/a9QKYkCgYEA2OH6 16 | eYcTn2UDaa9miCdIGE2emo2KX9o2YUnWTbutU97wi+RU9oX+mwQijGboH9i45N8w 17 | IXnrTHpE2JUxQ58oH17c14AIK9aStjSzo3CdszVA+KyaPxjwfBMez8ZcsflsAKE+ 18 | gIGhSF8RCza+S3bUMCLulkrBzFntChQSnxnzKS8CgYEAqAxjGv3axH8M8UrU9WoP 19 | lnfcy9UOr1XP8/6uJWeZgtOYxRGJZYgDODJCVibqP3RTRNt6KTymzkbF6HySu4J9 20 | kbOVRIHgAu110kJeQielAAtG1YgAJNVwpF/YYGoseVGObcP0pxkB1VPPVgqU8b5N 21 | 8y2ybzhe5sjLZskpPMc5yFECgYAh7+Jwx8kzROwuymqB3Zuzadnql0FS1GNz/8jP 22 | 6xEpMZZ0XWtKFBeODCyXybaC3Te5YI84fcpq8ir5tQa7bRkrJ33pHriom2/joQWH 23 | CqUymJ+tCd0tCGsBD0DgBEsW8k+LHbAZDa++ElopZgyK5HI5gg3RaKtAncWwsQlk 24 | 6fuZswKBgQCzWwwaNlLZIIeDSpbxND6zLZigOY2eoLRWbX98PiyIMrJPmoXotGKD 25 | lfz7xXDskspilKMmtlxkqDcqF+TqnSsdDcSEF8kLAorY6m1k9o/IU2X0oM/ywfPo 26 | ltj5CTFgdJyYbDEkXe/69fNVO9LV0/Zgy0E8IB/An8llKmLJzcbruQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/resources/trust-tls/key1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA2dDFBEwnsVWrdDuP+D5LOgELcOHwAv7J2rEsIvaXI6gHcRuO 3 | AXIoFbezFrj8vyQJlS0fu1aDbH8opzxuISOPuTvywZqGoIpp++fVKulTXa4cx8Vi 4 | mqdiS1JGb++yLaM+owTcHV44ODxEk4+q9hWRJUJBBxcmrDgwcQOXr/bz6UI/G170 5 | 3A/Bx3FzRuujnpy5La+aW+Ytad625L+5RKhl0vLCRDJw0QufSCK3+v1naKH9i6jz 6 | v8SHJZzZJHkge1n6s/UiIYkd2upAYJc+HeVIQQ51uqn/38WvGfgbgo0wGOr5DCNQ 7 | DZJt4i05dzjoDqcKBHK0q9qt3nrbTWeJd52opwIDAQABAoIBACIVP+73N7LRE/4h 8 | H9sZbE4MQc0NHbBqTA2zcZObAHVs4pU5WZQyV8FmOFjS44HJUtKL+A4pcHE2yJa6 9 | pwT6eo/iqI7Q7kYKwqDPWxTW49uh1hEQeuh2TrQ4kJsNZ9UqWS/LH+JcjvGFxLlO 10 | XtMY9thcsK46WivSswnMJR08yFf0YuH5ML8UvhtXeGFVGP+ncXm/xLhZRT+lEsl8 11 | bjsWIvMMbCkqMeK/u/4oKFEs/tW+CH5CKCxjrFO2qHFQFbxpM9/jIzF+EjG0mnrC 12 | 5RvDtjXj8+9WfltR3GO5lfQXNGIasC1aBC6Rf+r9ZGN/MATXjaw16ITfT+v5Gv9B 13 | UYojPfECgYEA7omj/8jLjds0tALl6i97zYReP0hrR7e1vJ49tOxdLrFCvpSFCIx4 14 | fal518ss6aFqWwA6IIJ/JS/253NobdU+FCUZPxS01+Y0IjbA2loax3B0wGpJuNN1 15 | 7uaUtxIPqUWL59r3M/CbwBc0/Je1p5s0NhmqS8MZRYdlPiAXZbPIL/kCgYEA6cLI 16 | GFeBFX2Ck6jWddbdl0e6yiKu2zA+CTbs3haH7O+G2C9ubFz/mphj3W62504ffUq8 17 | hom848J/nxA81Px1gyBe19NoHVIt9nAAFPa4jhr2YHsgtAsIcADprb15Pv+HppJ/ 18 | +T/lN6Dyz6+Riv206woFxtO9kX9stk5huBYYBZ8CgYEAhPw1cy/F1ANGoakP9vyb 19 | /9rxFETSknwoFMz/x/MSjCqE9G0msbpmnmAyroz2MBYujidVQLSgH+Jau3SK3tjI 20 | sEByj8mCfO85V23BHLqpPVFDtdZL3X/+ur7QYDPwiUVdl1hrAwj8PB50CvKlcpjD 21 | hfnPCEfvueSfHACWKrwNOjkCgYAjWPbU53Z3f3EdTuyPCRyc4ZXXNwNl1Zer4QhU 22 | VOfc5iNn0VRLQozMHbsMIs+5jlCoq4JyfaV4c1LFBLmOI1bJqz1sOWWG+YRFvY3l 23 | naEuq9kRoczCLODrvteXFA2ffxhOEd0UHZtVnBrbWNsTP2X6HJvfAumpdLFFp/iS 24 | 05RyVQKBgQCIkLFPKUC/UTxCkvNdAlavxwJY5ioo8gvo5r1DVpsiMsxX5Dsdgsj/ 25 | 7BvsLBcdWtc1tkOfDMmHYYQE1F0LrUYJrrKgxJQ2TCP4ZcFvVzI3Q7zxh43qfzU9 26 | X7u2Xr9SOaAnNJ4ZC9e2l7BFgzfBX8L07vv/N/z0NEqesvr+CF5wmA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/resources/trust-tls/key2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAsjH2gNUUbw9S9+21wRvKYJ6UTODbta4vwgxyLrzRphh1kAak 3 | Z8iOl9U375bESQhAw7VYYMnd7cpfysSGyPHo0g3na4LItsgeHUyhHsVtqX+EIDJX 4 | QHA9aH+KF47NjyYMjomREY4fY1PnWao7uxXW5beSZbWBMFySPSCDOQCccudEn4HR 5 | N+v+o7e7/BobTZcPNp+LFDF75ZadKhUImW1cmGAnOLtV7/Nt5VXwy0Gw6i9Ux/dH 6 | DTjmZ8h1l4pQd3e0SOChMcYIZ5qrZFY3inMtazo85TBnL6wv/ahx5tcjIUPrxEt4 7 | ku+6mvTWTLVZAQ5jcTSBzR4KC00fdBIarTlScQIDAQABAoIBAQCfxiCp/2Qk+G6C 8 | Esem8E4TN9zs2ajmS9pSeWpf8blg3gl7LBl5XOBNz0YqMtqoSRgY4p7tmdK9hXlC 9 | i9f2kMEappDbWxawnNozlyPYPVlIYyeK3thQ1NKawdCFsuso8QrNOxexz+bg3rhP 10 | M03/P2PB8Uj4nTfKrxYIbQB7ucqNUsPYL9Do5Ng/m/WNno+I66JbJaAu4Q0r/ATa 11 | 8IYCTaBKrK5nwXXROp4p+9WGQ8rvdQifMrfhEwLp/ObEly8O8Oyl5c6WY1sS/Pdz 12 | jYmtMtCzRB3YsQB5wl2v4lwJb5cvd3ZQ5n8YR0c1GewSFsBm//jGzNTbql0j32py 13 | /wvEXO2JAoGBAOR84BFkeA99sM+nI5W3YPl8q5sRJLMdWoom+++Z+KfzSV+ypMWB 14 | 1CyU13Ah9B5M+g8AsquN8KaWSJIDIOJFb80o3fBFpSf83MfnLWrZZRfz183D5soP 15 | duS4aYm8+/kXS8BimFhmCxsb+0kki9j7EbjOJhLSYH98dqy0gvw0NfkjAoGBAMem 16 | 0ekfgILMit6X2CbOygTuzEl+Txl+9f20URe7GmRBoh1jvduamcOgCULVKLp61/1Y 17 | Y0XbXgPDYpeahRrWJrJfTPNrheqOugtSr/X0Jw5bKL+Zz/cEBIDElnKoylYj3uTH 18 | FzaXQJ5KvBn19BzaEAK5IN84OThrHIs4vApfxuFbAoGAX+oHyWVKfP4/VbwM1RFo 19 | tPJfuzWEGWhuFGZvBdK1rhMTrwd34a0xWxGUKm8mYf3gTqH1Xsyzw8p8ZkbH7BRH 20 | 7XCFUhJ6VQHx0Dy8NQlTRa3H3omo/RNmznaXXwmlIJhl5Tm+SAV2Dgg+ASNFTW1/ 21 | r/thwi7r8ThXPQJ9pc9pBM8CgYAgFar+L9XTL1PR2c0ez3SfvlCOLwI/MqwNjYDK 22 | s4LWQcXvuyuzsvCrA3j5iI/3B+7WA/JQM6S0Lh4WBvsFCQ1G9RHEvK7UtwMm9q7Y 23 | c9gi8ntl7y4AoGCo4O7clkuN9E3JrkUIvN3hklkQQvpr+I9CIMYrmTl4253GseNm 24 | N2YaqQKBgQCAqdakYs/N3QHbDK2FFQ4Gfj19g7jMXb7Q+HMg3knY7hwNSuemVY4z 25 | g/xpUJ7IlhebrFZD1rALq85T62Q5fBrBQyea91XAffvb1AXn2BlGrEi/il8QJuWJ 26 | Tnr7DTa2YVZDR02IcMO7cO0v2PisFxFfXARl6wiWRa57uuOFqCF45w== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/api/restart.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | import {promisify} from 'util' 5 | 6 | import * as KeystoreAPI from '../api/keystore' 7 | import * as authorisation from '../api/authorisation' 8 | import * as server from '../server' 9 | import * as utils from '../utils' 10 | import {config} from '../config' 11 | 12 | config.router = config.get('router') 13 | config.api = config.get('api') 14 | config.rerun = config.get('rerun') 15 | config.polling = config.get('polling') 16 | config.tcpAdapter = config.get('tcpAdapter') 17 | 18 | /* 19 | * restart the server 20 | */ 21 | export async function restart(ctx) { 22 | const authorised = await utils.checkUserPermission(ctx, 'restartService', 'certificates-manage') 23 | 24 | if (!authorised) return 25 | 26 | try { 27 | const emailAddr = ctx.authenticated.email 28 | 29 | const result = await promisify(KeystoreAPI.getCertKeyStatus)() 30 | 31 | // valid certificate/key 32 | if (result) { 33 | server.startRestartServerTimeout(() => 34 | logger.info( 35 | `User ${emailAddr} has requested a Server Restart. Proceeding to restart servers...` 36 | ) 37 | ) 38 | 39 | // All ok! So set the result 40 | ctx.body = 'Server being restarted' 41 | ctx.status = 200 42 | } else { 43 | // Not valid 44 | logger.info( 45 | `User ${emailAddr} has requested a Server Restart with invalid certificate details. Cancelling restart...` 46 | ) 47 | ctx.body = 'Certificates and Key did not match. Cancelling restart...' 48 | ctx.status = 400 49 | } 50 | } catch (e) { 51 | utils.logAndSetResponse( 52 | ctx, 53 | 400, 54 | `Could not restart the servers via the API: ${e}`, 55 | 'error' 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/resources/trust-tls/chain/ca.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA3myWNdneQE//sBGMqlY5N/G+VIP41/FjfhL6V/xuFWRMHVaL 3 | p7/7811h0834zbqJzARIwGeTBoBCVW9WXiZb74rNhZV8rSh96r/kyVr+VEZImQJg 4 | OXcNiJtVgb2NLuhRkTZp80gzSQvvtdFkbv790MO52LuHZSnAepv/M6OXdlsTipoh 5 | mMqB7jMNO9dvhfvrPM/DDlaMBlLBkS6S9xo9lPlQX6RmcRqfOmdW2OzWRz7xBshd 6 | Xt+Jwxj6riNww8Lr3GmAvqdtV4fgAsua870nqWhzej4PmAMkorAQbgat36+NoKHM 7 | AUXQc8it887qzxeSkN3yMCSmySjwmSp1gdwjDwIDAQABAoIBAQDdY5ff4gumV/pY 8 | dh9dLJWnaafY18NIrawqQRmNmmS4Riy8xvga8K0Hf1twRwPvQk0GFP2VBYMCZYeh 9 | IkEFVYkM1UvcOv7uG++5SWegREICADMimYrNqAKAhpyFlvaASh1mPJMY5DSn8ImY 10 | n+Mn2AcrVKYXZBFQUE6Vu9A4xgxscD/oiCdb/fiAVST1I/Xc+pp6iz358LSIzWN1 11 | vCP0XCfTV8zINQd4PbTzqlso/dk1WYU9LGAnWoGWOunO5g9A/uETE0+HJR3tZIS3 12 | fS5fKysDeg58N3m+GrZuZnaMCMaFSM1c5FX8zVTktUDsueyPMBMgvHLTxBNbacfV 13 | Q605kKABAoGBAPA65BhDC8JInGBvVV2hK8T6RWKBJJDzRRQkKrkSA/c0uM9KGPWf 14 | tgGT24dRFRRWdJkq1JoT2ddTuxHBbZq9N88rhQHiUbVqY9th0Njb3qN7vI8v78n2 15 | nrPUstaL2nfZuvJ6RBG9dVanALpSWcqsSqaWpfEx0TJzdhbUojejyUoPAoGBAO0G 16 | dv6U1FTIYllfJ0sXzDolAZWW/u/8YNPnlc2F5R/S+RwttlQR/CRlLuJa2VGYLmQT 17 | xG3Bt/03bv0q79zHgh/yzmH/mvUTPtJZQ7LZatCZxzLBNYJD0N0FYhy9ENLyPd+B 18 | 2KlenPr0HIJpB0RkvpimdPXQ1ojemEb3KNoaXJcBAoGAd2MtHImlfGaNoLdz21ZP 19 | 5FLkzLGVtJvRxtK6kMNNSTvgZNRUVy2KTHpo7XT0XukghUZt5izbZFHudEKG8BH9 20 | fG4KagdJyDkZa1vPaRbMLL8/1IFkUCDKemDIiCt05X9Fk2AEGDTp7aiNiguHKFw7 21 | tVLFIXs6ljIfi/UcsT4quS0CgYEArlSU7MX2La18eJM9yUNlmFZjheya8VPEu9aX 22 | HaSfUvrO8eTFN6xKsVIiW6XKNPMjG8RWMwvKEVnWLiqIZO/wSyNAnF9tT1eqWvgA 23 | jOjQYUDZfAETJA5Q7l7pPI3qc9xrmZpdCqVl/EXTOi+Zgro+XPQBDlNdlylxArEC 24 | wPempgECgYBbZWnFy9k8yhL983rirec4O+tOkbmZYNqBcsfsZLkCPZRAvTzv7PL4 25 | wDkAK2fXQSHX3TPksctNNPc5Wizd9n4kgLiJBM1T9DiVq9YlvuEs7OBaDTsQ5vOr 26 | 8JXreN7F1cTlRc4jh36u8XexamOZMZebAzP6ktSjaVlAvXGOo8roBg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/resources/trust-tls/chain/intermediate.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA0CjnC/Tz/CGBQybGzRObGFPK8CiiVoZ/Hpls92DjmSXJdZmK 3 | WMAYN2B08MkxicDzCIzNt6nOerLisEqEujQ48Wg7G7T1PpRMJyqEWJrb627eavsP 4 | BPxOUqb91rTdiA14qP6pSEYb+Juzysv6PG/Y1W9Bbi2mokG6CIaLmeCRQkxJ3Ft1 5 | hVA8rgdTaI6cgu5JG2wR5Z7G9w/rWUSMimkqdoq6OpK2HYwwGARPEDTXVeWDTW/g 6 | MlTUGX4gVPYlCbMTE9Jdq9oJxyiJmDsBQz5u0ycYTOefT0Tya7fyq2jcMaa07HF/ 7 | fxp5ARIZ5k1pHHIpZNY/CHfrNpX43DTPpn0DLwIDAQABAoIBAFK3DiiIK7OStMS0 8 | UecH1WdzVH7eBrQPOdA080HRtF8Uztr2Ki1eF8FZiN5tdEvi6jAJk21aAnxrPP0E 9 | f31mZwI2gtZdr94wG5qsoG59CsqhAazNGojGoXDifpekwyre/4VXvHjFszO021rI 10 | dyUw5J7bWMqpRT/QklX08OAyhkYrRm36rGkN+GeE9xLk4FBwyVVUuH+iMObD3LLK 11 | ri9mRJxAD6HzbVo8GNG6u40Dn8inyZoqx8HLN9Z1W63qVsX/dE8XCoJqHCZJrHwR 12 | /Lf5rfx1IzAKnGstnSDMapmYoa8wbOzkTsVaWW94eQ6+wm7FNuPx2Ci3+6vH89Qp 13 | 30A/rTECgYEA+LOR55ZPrE0nKtWyxtMEC1/bPjxHCI45TzGUY0KrQBbUN9shP4m4 14 | cvJb8bWizO7aRgS0MkDGpuOhlHp6EPY7nnSq1PaujE4r65lEgAANzjrIaMxIaAFn 15 | Jx8G55GJYfH1G1j2OTvMh56CWON74EDx5plZpfJJhc6jhnnDUrMBdE0CgYEA1kTA 16 | 4VshqHKss3UIv0fmXsfpelcThDR0glWU+xPBQfnnbQV3VDHJDYVosPPzVTVJiofm 17 | EX+7GsWgmZVqwC5hMmFE+ERaAF5ePm20xZ9f1SqI9Y+f+m/rCtaia/z1uH7wzrSj 18 | AwHsEKCccxCLGVPn2w3uoM7F0V1LdDY6zFbWg2sCgYEA+K29t/8Rva0L/AXglHSN 19 | d74qoMlvEbv8zBY8VSBl0ljBbPSdA0Z3e+ChVUiU+Cvhb9/mOtWQjXHRsIDjK7C7 20 | Ho9z/iyHjl3gBt2jpAXpyAv5NukFgZHdjj21Drn00E47E1RaKBKGShmSJsWYqTzn 21 | ZPhzrr5dDAH6e02npDq/uoUCgYBVnP8cjFc0gS10Pw0RsN7hfCtUELrhljv/X8a5 22 | U7libUIFE6wMXiAhYO7l1XOi1jneOlK5JzpRhimXdFZFF8MKaEcwk88KWqp5c0hJ 23 | tlaKTAaRZ6PaYyGdIQmg8GAWKdJ6JT8FEp2KP0KDz1Zk9lur8TMp2bS85PQjKQ/x 24 | Wbk4pQKBgGFnhxCnbJsQJWCSQSQHcV+dLAQUyt7MDFmTIf9RLJ1PuTg+snUGyKbs 25 | Xhob2Sxz+AbVG15m0O7t3BwFR9mFIW433R9hbxqYDYKVyoRC1YrEc4Jy2HxZwxZT 26 | V/ua3Aifu2VvX2rxfp8kbFHumgfDPc4pwvNzZAEBu2h/Qh8ZJTkd 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/resources/trust-tls/chain/test.openhim.org.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAvhGGY1z1tDO5M6fwqL+JxEnfHawQwID6PjgqBqBFQguH51aS 3 | 12NWrcSRJl0S8WYbTExgSYlEl68JTJeIn6pni8pYA73lp8uR0LIRIJkHGfGs4ha+ 4 | yfxbMLWm0AW1jeBmPeUrzqrord3NpftPs+0pwazPDcEe8RuudnrxPwiY9oTkwHd8 5 | G8HoeQXNcNVQVcSEBdLjKicA5cVRjxijOrWELM+NUxJFOUIAxyK3m52pFkYzQAyk 6 | ULaDmjf2/sh243cvE2btTOtWr621k77ZXw2cd8NStA9AxfR5mHncbtKW1Sb3jQll 7 | dDF0kX+K1iDvu1Pm4815vOQBNJ33T9CNxzcP8QIDAQABAoIBAQCESdJNajAhQjbO 8 | +z6HVPjgQ8Ik9tOaCKsjjxfBEN/jff8TRrBhdWSpAT09hF5g4f5x9l62D/JL2KoS 9 | Qw32gzYal7T2tb8YMJnGtYkyNRA6QHlhyw7zqs8VcfkupCqfGuZOgikhaNcV3GD7 10 | +5SX2KPNhyd9nUKZ8fhVwnASvTspqFYZFwUgwcci2Ciof7Z4GilgMOwFy77wa/uE 11 | VkJRX/JR1ywYFI1dg6XcUCKvBeFj5F1qOf1+A6+wWxs5TOZjR0qrGYzoJvaeuqYm 12 | 1pqgcX+fYuBCw+jOgNaAN4fUDol1raPhGPvSvKRHgOEo9Lv/4KnDahpQ+jmOc0T8 13 | 2lPLz6GBAoGBAOLHU26PUFJ4OUzWFKpy0NJoZCiYA7SxzvkA5u4/R1MHKQX6nCgU 14 | YHryER2jMbDR2P+bMZQe51+7f/MAsYQ4Ca9cDTV0+GiJtSQp+8wyyz8Ylkvjcluj 15 | FDP7y+M5ROBTjLHJ07bcg/3+yTqNKarCbFP+a8Fe7kFU18VR4E1ZDvxJAoGBANaP 16 | QK+LK5QdJ/xf7vSSeqOgikL37I/xUQeuJAU9giDtvlYkGEpSo1eFDGYbK4bjffmn 17 | fFFafeXgTAl9hah8L2HSpcPSZ5scO2q8Z5LizfnqqVdgQ8Bibo+TjcUP2RbHVcVn 18 | omYG4JjL0Qkk73ljSbC1S9KMSsulHBP6A4JuKOZpAoGABBqbVereB0AJuWw+X39I 19 | bCZbeaiWmJSHLFZp13HptoTwpx0L6mXEATWUPbcQVSTXCnkigo20njTkcd9XVw80 20 | janay1bcj/e+AXOJDP72UqP9RE5PaX0gBnjODcShjHW2nBkM7cg700LjHV4knOR7 21 | 1WFLTeqL4T7J0NZUetHbxZECgYAJSAshTODmuFgpd7DrjYaWm3Q/r1/hXiwGLAdM 22 | 3rtJV197dHZEiMyeFZhStmwsmpyxdwhOsJ5vmkNPyclFX5HhJm6bSPfIYvFDDcFh 23 | PkRwwjDvIa/sm5VyyoxAcQ4QCTFxp6kDxtpiYba6MYnWc3AVn9IW2pDFvG+5gYAz 24 | XTaRuQKBgC3Nrx262wW0M4pw+64QotNovo7jr0DItZ+uAykjtm7rl+cpe+6QnkL3 25 | TgwfTMqJg9a655yaVOUtLGXv5iuo2Cb0JSucRw5rQN+ZQ2wksSSHI7JwSyVWLfij 26 | V0JBFEERl/0Qj2Gsa3PyM1Wo8wBJ6WrRBdNCdRpYzE0ollpiTCgR 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/resources/client-tls/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjwfXklPbwEsO2 3 | vg5O/U/dj6ZHVnDsu07eAEF6HqSiUcv4qd0CsTxy3pEqkUUNdpmmaO2sbHLQeRKC 4 | N2xqQ/2vxrpGkuQROa7eYO+BuogAtuEcoHjr+x/OlNN5dOl608kYJNgxsOS2VWo4 5 | 2hV/et9HqAZ6Ez8Sx5jRwlfeCOHNXBDGa3EupDVx/+I7biyRo1svaafQNA+y3/3o 6 | KDJkKgUNUVXOH+NzogsoawlTDG9lrgz3LYl2a5V6KQdDjy6UDr91jhM3ioQPPAS0 7 | h+/iMo+gVD2swn1Wisn66muoklIl2I/JSRQYTePT3eaDxy2+mEdgom2GXIA4RpmK 8 | 8q7qH1wXAgMBAAECggEBAI6Nu5vYHbtR6nP7sQMMOuk6MtrpIfbC+94ARP+lC1aO 9 | dEysukustF2e6KowufcFZq5Zq4UtByn6K6WVz9gWBhJtohzrpIRmPZcytWyuT9Sn 10 | XwIHpBxxiPfR0OJrZxDYqrdM47MmNYOQiZ6iIpLOCO8dEz5T4NVdUehQESOlgcO9 11 | W9yzQGhxKr3Er/mfgcBIEiN1LFgMuDuLxA03x47M4zAfsYRmiBSQl1U5GUZGY0AO 12 | yrGdT9SADEhkjPZ/QDiJZSEM+PGx0vetwgtTli6uAy+xwg7IjgPuOYACItJGao+F 13 | oSF+ufW+GKRT0A4P22fz9cDeyDv8AlqwUlH1gZXApkECgYEA1Cj3KxD45ov16NOS 14 | o0PCgK9qtihYnFjkyY+cupvh7V25YX6SJwOql1WiyjmcTvg/MRjTPJtzJsqNQRzc 15 | tH6NBFAodsTQCD8N98L1Ywi0WQTpKE0r4qLH26VeHBDvgvjpEuizbgy0E9L/olfU 16 | 7CLwO/vlUNE6Bjwx3FNGJU8JQCECgYEAxZiQCAOlLq0blnPITEiWtZu1HwisTVto 17 | gFdlln2ADXfuIypy3ClaUdVkTzz6bd/xKgZn08Fafg6jsmTMm3FiWVYXea2g71op 18 | tUtmLs4s5q/JN6TwnmN1C2RvDhczryaLe5taHDfs3n2eo/A2glmzpApQgfVE8hz8 19 | uTj8Kkhj9TcCgYBznluMgbRS16VMm7hG9w9RV8dcu8IGej88ROJRo8/Mro3vPq3h 20 | SxM1Y7rSX/D57DlrxS6g0cpAMpQmDteMGYc5+YEDsJ/6TnqLMhjOPpkiBhYWG3A2 21 | EyW9YnJV8zjDdGh4II5gA9ZDE3KygxELGsM2vUCr+UmYykcV3d/VRywwYQKBgDwY 22 | dlW5phdYFrffZt+shAmf3aoenSi50EKYUhE/Ah8WhpqzYlOLUCjrWaKvzQp1vYcm 23 | g5VnjPohwa+T8JamGV9KS8F6CSRCq8SztdQuaIGQnQYLaBMD9KkOVqhWbiWq92kU 24 | iodiiuvwELuTZ0Clv3j4mhusWNe58TL0Hd1OPvhpAoGADOljmh91Aux+F0VdJ8RC 25 | 9MCfWx/KZpbeoI5th3s8hvE92qncZRjKH8Y12Yq/4VFwRm5YODoZHF6d2r0YNSiq 26 | 6vokpWnoLiHc2gM7KbNu8TYjD/jJpq5Na99Y+dH0VbgRuuxi8RSoQbCIx/ocJs6H 27 | CzfoPK4awbxKvxxQ7BrPnds= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/resources/client-tls/invalid-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDU45hLCEK+92lv 3 | 5CPyaLEaOo1qkoI2hCLIh6SLcY6rrWQVXGAWGx7cg75NZ6ZPozTrpu51K4k00Bn9 4 | I54t/+4xmg9ZMcUXpLsvqMRinL2dMkd68aEGYz4JhKruvLOWRwUZS2tZdADNEBF6 5 | WaFl5nuNamqAsPdnhH2VzchWmoVjxogQxJVdzWt7ajkfk+zgcdTsmozxcpSyl8nv 6 | u/TLNjJK2U974HI4iV3+jsAS/USAB2I676vuPr+yfB8HcsrsA+6Co2BchPWArlxg 7 | hvOTQBczNM/YTfOXjB5I8zZY1OIxqbkun6WAoEN5H8pEP6VZggMU/BlbzLpiKJbz 8 | lZXYCvkZAgMBAAECggEBAIxXzdoJBBdoezWsLJZttfMYjomnM/hEe7m+0harMeaz 9 | U7tRPnbUQjAVGatlnRn6+bQwRBsyLC2I0tkyVeD1S02fxmaCjO/dRRlSJMTtl4K9 10 | 1qmSCRlw60DTGOxxseJrx0y5j3dVJgIJibwiwmeu3dyIPtW/1BmGGlRbaKrPCwiO 11 | b+bV/s4OyceXxHmlSiTY9wMN3mygwtRHKjkIzOmPuivEtQxOokNhMiyueW9NKgvh 12 | XuMwEWaOQVsc95DzaU4dyjTO+fK5GWA5gDT9PK9Haj3pfzJ0PDlS4j1laURyA+Oa 13 | JNRvucsuZlF/YqCiI/X1SZyXNZDwzMa/xKLQMIhyIdECgYEA/WNrsk7iyAKm1mNP 14 | CBi/r2UpFKGejox3q57GRq+w+6CGnSy+KecpO4tQYjQZUVYbyUmnNWdvmeuuS8yG 15 | TudZw/Vzm2g1CiaFIKxqhIW1yT4lVbtBid4uJYOti/qIa6syZMX64qdhorP7I/qt 16 | NeTMYe/X/kuEH56+92oeAocVnJcCgYEA1xVQgrYY+dmA7R7DGv8Xcps0gDyupmOA 17 | JGH1nwTz5rMNRRw5luyBTralmREHBSWiP6HFS5x/gtISePalUbO0qgC5dXMMv6mX 18 | a0H60K0lR5jvKk+PTPDz0LagbS8KyLx4RnT9IX7GW4/yZtCMLLY8nLOnQdCPV+dv 19 | h0x6bGsV3c8CgYAhgHinzahMW5VleSHk5yjI7u4cjTXikQ3tggOjKu2Sh2nk9Bp8 20 | fdTEy6moIk1KpMDtvzA9blyiFDgqS3NikVIcB6LuZDvHCMrHRCSdOvSLFA1ppWWH 21 | 7flZ+mwCuvA4lB0Il+iQ+SJ+mZ9V5XnrS0H+nPCI7cEdUSbcnYo0OVoRJwKBgGHV 22 | DiQGlGHBb4VsAq8a7R1yP3U9JOwGQllKPaExbYe4VgbjicZ+mWqmZbi0KA9NSPnM 23 | qaN08gMdbs2a0yPQrBLP9YvY4ymjCH7/KgkVWOmyRMdoHPSQfTaoe1xuk2cvYz4Z 24 | JLLBqZQoa8gcgEYuNm/IwAGNzkXbvb07Kkx6gR29AoGBAN3xS22WThM0EZ5DFkiN 25 | ZqI9F9vQ8HZF7JXx/PCCf61+1Cqqkcivg1CWOPUgHm/36q0YNit2pEaDeMm3d+EX 26 | oIlV5vD5aiQWsFucdYJ8RsXnxObCuFAPmNtm+iaQKe9pwc0+Hb2GHDb+21Wsj8Ip 27 | k2FfqiC47H8RFt+8Ahx1lBtO 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/resources/trust-tls/chain/test.openhim.org.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEzjCCAragAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwTDELMAkGA1UEBhMCWkEx 3 | CzAJBgNVBAgMAldDMQ4wDAYDVQQKDAVKZW1iaTEgMB4GA1UEAwwXSmVtYmkgVGVz 4 | dCBJbnRlcm1lZGlhdGUwHhcNMTgxMjA1MDk1NTQ1WhcNMjkwMzEyMDk1NTQ1WjBA 5 | MQswCQYDVQQGEwJaQTELMAkGA1UECAwCV0MxDjAMBgNVBAoMBUplbWJpMRQwEgYD 6 | VQQDDAtUZXN0IENsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 7 | AL4RhmNc9bQzuTOn8Ki/icRJ3x2sEMCA+j44KgagRUILh+dWktdjVq3EkSZdEvFm 8 | G0xMYEmJRJevCUyXiJ+qZ4vKWAO95afLkdCyESCZBxnxrOIWvsn8WzC1ptAFtY3g 9 | Zj3lK86q6K3dzaX7T7PtKcGszw3BHvEbrnZ68T8ImPaE5MB3fBvB6HkFzXDVUFXE 10 | hAXS4yonAOXFUY8Yozq1hCzPjVMSRTlCAMcit5udqRZGM0AMpFC2g5o39v7IduN3 11 | LxNm7UzrVq+ttZO+2V8NnHfDUrQPQMX0eZh53G7SltUm940JZXQxdJF/itYg77tT 12 | 5uPNebzkATSd90/Qjcc3D/ECAwEAAaOBxTCBwjAJBgNVHRMEAjAAMBEGCWCGSAGG 13 | +EIBAQQEAwIFoDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xp 14 | ZW50IENlcnRpZmljYXRlMB0GA1UdDgQWBBTDoukTNGKpY/wL0EAi8iQtdB9aLTAf 15 | BgNVHSMEGDAWgBQ6kvplsoi8I5XlxGUsUSSYz0/00TAOBgNVHQ8BAf8EBAMCBeAw 16 | HQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA0GCSqGSIb3DQEBCwUAA4IC 17 | AQCFks5EM+ypBEHr5tVhx95qn5LkR8Nhwwbe0hshBQtb44J7XNqse0qB2SHHqJJY 18 | aPp1E6TPVloPcfFPQeCGMGJ1uMqOGFOxQ9UCKp1zIV+lou69YF9pMuC7X4d2Fq52 19 | hpt/4R9CxCS3iRK+xN5Bsm2M8POBYIt+tNDUWAb79wkAdTYiqayBok6phL6IvjyP 20 | 85jKJCv+wnRPaYUAHajbfz6mIZM1eKO1OCaY3Oy8NyKhqWOYJe7CxMBkW/LAhww8 21 | Ql2mlE4sOx506DqSOTzAcjOMkMgQNqXNNcvbuD2Az2U5KcnjxrLHhbybUt5DtjVj 22 | 6cu6ypAb6Hg9TIWprFKcCgBdYbsFKTJ+3IQKEy1DHLDpJZv9Lp7CqFhawKVhwhgx 23 | HPiRqqjjklld3U8YRiWeEgl2KueJECeV5Eh95zBffqDmAa6RYRVN2IFehKFG1PYZ 24 | IabBwZg6TX9+/naaZHMc4euot6Eo+d1ogi0/lj4kPzZAdWl5zW4cCH905bQSMt03 25 | M/Vt8a1O+a87roYzg51Y7QJQ1LuUMrYIi3FH6LSWV9ywIlFbv0Qmvv6K+6Q4/6VQ 26 | xjlxyXZYKqC5+T5IumXuV7MNN1Jh7/NFL6HpIAWbUeiwsJrSlGE5ZVjQ7rhcilJy 27 | 0DohQRTf8ZmGbhtzdE7SZOOBhTi1FMahC8NKjwnIr6Nx2A== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/unit/kafkaProducerTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | import sinon from 'sinon' 6 | import logger from 'winston' 7 | import {KafkaProducer} from '../../src/kafkaProducer' 8 | 9 | describe('Kafka Producer Test', () => { 10 | describe('connect', () => { 11 | it('should call connect on the kafka producer and set isConnected to true', async () => { 12 | const producer = new KafkaProducer('test', 60000) 13 | producer._producer.connect = sinon.stub() 14 | 15 | await producer.connect() 16 | 17 | producer.isConnected.should.be.true 18 | producer.producer.connect.called.should.be.true 19 | }) 20 | }) 21 | 22 | describe('disconnect', () => { 23 | it('should call disconnect on the kafka producer and set isConnected to false', async () => { 24 | const producer = new KafkaProducer('test', 60000) 25 | producer._producer.connect = sinon.stub() 26 | producer._producer.disconnect = sinon.stub() 27 | await producer.connect() 28 | producer.isConnected.should.be.true 29 | 30 | await producer.disconnect() 31 | 32 | producer.isConnected.should.be.false 33 | producer.producer.connect.called.should.be.false 34 | }) 35 | 36 | it('should log when disconnect throws and not update connected', async () => { 37 | const loggerSpy = sinon.spy(logger, 'error') 38 | const producer = new KafkaProducer('test', 60000) 39 | producer._producer.disconnect = sinon 40 | .stub() 41 | .returns(Promise.reject(new Error('bad test'))) 42 | producer._producer.connect = sinon.stub() 43 | await producer.connect() 44 | producer.isConnected.should.be.true 45 | 46 | await producer.disconnect() 47 | 48 | producer.isConnected.should.be.true 49 | loggerSpy.calledOnce.should.be.true 50 | loggerSpy.restore() 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/unit/utilsTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | /* eslint no-unused-expressions:0 */ 5 | 6 | import should from 'should' 7 | 8 | import * as utils from '../../src/utils' 9 | import {config} from '../../src/config' 10 | 11 | describe('Utils', () => { 12 | describe('.statusCodePatternMatch()', () => { 13 | it('should return true when pattern value match status code (2xx)', () => { 14 | const result = utils.statusCodePatternMatch('2xx') 15 | result.should.true() 16 | }) 17 | 18 | it('should return true when pattern value match status code (2)', () => { 19 | const result = utils.statusCodePatternMatch('2xx') 20 | result.should.true() 21 | }) 22 | 23 | it('should return false when pattern value does NOT match status code (200)', () => { 24 | const result = utils.statusCodePatternMatch('200') 25 | result.should.false() 26 | }) 27 | 28 | it('should return server timezone', () => { 29 | const result = utils.serverTimezone() 30 | should.exist(result) 31 | }) 32 | }) 33 | 34 | describe('.hashPassword()', () => { 35 | after(() => { 36 | config.api.salt = 10 37 | }) 38 | 39 | it('should return a non empty hashed password', async () => { 40 | const result = await utils.hashPassword('password') 41 | result.should.not.be.empty() 42 | }) 43 | 44 | it('should return an error when password is not provided', async () => { 45 | try { 46 | await utils.hashPassword() 47 | } catch (err) { 48 | err.message.should.be.equal("Password wasn't provided") 49 | } 50 | }) 51 | 52 | it('should return an error if salt provided is invalid', async () => { 53 | config.api.salt = '2xld' 54 | try { 55 | await utils.hashPassword('password') 56 | } catch (err) { 57 | err.message.should.match(/Invalid salt version/) 58 | } 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/model/visualizer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import * as events from './events' 6 | import {connectionAPI, connectionDefault} from '../config' 7 | 8 | const EventLinkDef = { 9 | eventType: { 10 | type: String, 11 | enum: events.eventTypes 12 | }, 13 | eventName: String, 14 | display: String 15 | } 16 | 17 | const MediatorLinkDef = { 18 | mediator: String, // mediator URN 19 | name: String, 20 | display: String 21 | } 22 | 23 | const VisualizerSchema = new Schema({ 24 | name: { 25 | type: String, 26 | required: true, 27 | unique: true 28 | }, 29 | components: [EventLinkDef], 30 | channels: [EventLinkDef], 31 | mediators: [MediatorLinkDef], 32 | color: { 33 | inactive: { 34 | type: String, 35 | default: '#cccccc' 36 | }, 37 | active: { 38 | type: String, 39 | default: '#4cae4c' 40 | }, 41 | error: { 42 | type: String, 43 | default: '#d43f3a' 44 | }, 45 | text: { 46 | type: String, 47 | default: '#000000' 48 | } 49 | }, 50 | size: { 51 | responsive: { 52 | type: Boolean, 53 | default: true 54 | }, 55 | width: { 56 | type: Number, 57 | default: 1000 58 | }, 59 | height: { 60 | type: Number, 61 | default: 400 62 | }, 63 | padding: { 64 | type: Number, 65 | default: 20 66 | } 67 | }, 68 | time: { 69 | updatePeriod: { 70 | type: Number, 71 | default: 200 72 | }, 73 | minDisplayPeriod: { 74 | type: Number, 75 | default: 500 76 | }, 77 | maxSpeed: { 78 | type: Number, 79 | default: 5 80 | }, 81 | maxTimeout: { 82 | type: Number, 83 | default: 5000 84 | } 85 | } 86 | }) 87 | 88 | export const VisualizerModelAPI = connectionAPI.model( 89 | 'Visualizer', 90 | VisualizerSchema 91 | ) 92 | export const VisualizerModel = connectionDefault.model( 93 | 'Visualizer', 94 | VisualizerSchema 95 | ) 96 | -------------------------------------------------------------------------------- /src/kafkaProducer.js: -------------------------------------------------------------------------------- 1 | import logger from 'winston' 2 | import {Kafka, logLevel} from 'kafkajs' 3 | import {config} from './config' 4 | 5 | config.router = config.get('router') 6 | 7 | // Customize Kafka logs 8 | function kafkaLogger() { 9 | const toWinstonLogLevel = level => { 10 | switch (level) { 11 | case logLevel.ERROR: 12 | case logLevel.NOTHING: 13 | return 'error' 14 | case logLevel.WARN: 15 | return 'warn' 16 | case logLevel.INFO: 17 | return 'info' 18 | case logLevel.DEBUG: 19 | return 'debug' 20 | } 21 | } 22 | return ({level, log}) => { 23 | const {message, ...extra} = log 24 | logger[toWinstonLogLevel(level)]({ 25 | message, 26 | extra 27 | }) 28 | } 29 | } 30 | 31 | export class KafkaProducer { 32 | _producer = null 33 | _isConnected = false 34 | 35 | constructor(clientId, timeout) { 36 | if (clientId) { 37 | let brokers = config.router.kafkaBrokers 38 | brokers = brokers.replace(/"/g, '').split(',') 39 | 40 | const kafka = new Kafka({ 41 | brokers: brokers, 42 | clientId: clientId, 43 | requestTimeout: timeout, 44 | connectionTimeout: timeout, 45 | logLevel: logLevel.DEBUG, 46 | logCreator: kafkaLogger 47 | }) 48 | 49 | this._producer = kafka.producer() 50 | 51 | this._producer.on(this._producer.events.DISCONNECT, () => { 52 | this._isConnected = false 53 | }) 54 | } 55 | } 56 | 57 | get isConnected() { 58 | return this._isConnected 59 | } 60 | 61 | get producer() { 62 | return this._producer 63 | } 64 | 65 | async connect() { 66 | // Not catching the error to throw the original error message 67 | await this._producer.connect() 68 | this._isConnected = true 69 | } 70 | 71 | async disconnect() { 72 | try { 73 | await this._producer.disconnect() 74 | this._isConnected = false 75 | } catch (err) { 76 | logger.error(err.message) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/resources/trust-tls/chain/ca.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | NxBrMSSlQd8+xhsnCmzbSzM= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/resources/trust-tls/chain/intermediate.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | vA2chlJIclk= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /performance/mediator/tls/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFljCCA36gAwIBAgIJALcO+F0qxegqMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV 3 | BAYTAlpBMRkwFwYDVQQIDBBXZXN0ZXJuIFByb3ZpbmNlMRIwEAYDVQQHDAlDYXBl 4 | IFRvd24xDjAMBgNVBAoMBUplbWJpMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTkw 5 | MTA4MTIwNzM4WhcNMjkwMTA1MTIwNzM4WjBgMQswCQYDVQQGEwJaQTEZMBcGA1UE 6 | CAwQV2VzdGVybiBQcm92aW5jZTESMBAGA1UEBwwJQ2FwZSBUb3duMQ4wDAYDVQQK 7 | DAVKZW1iaTESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOC 8 | Ag8AMIICCgKCAgEA8c5eaKs/PRNqKCcQG5Xerw9aP2GaEaHRzt+QE6cQQ9Qe1P1n 9 | F1AvSmOP/6crwUCsRkUXOqG8tIdsieG5CjJnkaqBHRjP0pL8AWTWDRVAdKCI/ngI 10 | rvWmT+NnolNKuwK47CwoDcQI6P2xoccgXzBBSBC0jcWLE6LDAo7sd1injEEjwYw2 11 | ABMloJYmtwgsXOVFxgOJC7hdR2BYpSrbHiY7OXVg5piMRGZaaN2ZoyJv78hoXili 12 | NuvmGju7XtFObKRtnPEdFGoLX3DZivtSi+mBePfl/LJcoH4HX+6aJSb99wADA6pk 13 | hrfrsxNFVHoUFxucB7nkZ7w8L4GkawAJ2rY/DePTP6TG/MHvpENv2SErh9leoN5A 14 | 6M/Ru5oDre67VBi061L3AXnAO19AMrcKFQchuBDFGNWETb1Fpikl3w1lDnTTCHF/ 15 | WGMxC4PJ4qTBIMNDN2gY/ck4IGDAqvoVNX26UT6HLCRGNmn9oXks3/+jLyezb369 16 | rXnlroKxh5hWh1PV/DjpFlUCTFFnPQaGS2OMkEYVH7C+BN6fHMDQw3hfliugQwvA 17 | k0b5tDrkVHKQ3CEycpf6JAUf1C22snSCVJnlzrcLnZ5WEQMG3zzb6shSLCjZEjmi 18 | Sr/Gt/dlNr07wA3JkjZfmbDoFKZAhN81gbkOcBRrlR4qfrpfWrSEe5DPGkECAwEA 19 | AaNTMFEwHQYDVR0OBBYEFJs0ONTQhVwmDzzQ/MxbenbJmLyIMB8GA1UdIwQYMBaA 20 | FJs0ONTQhVwmDzzQ/MxbenbJmLyIMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN 21 | AQELBQADggIBAMVtSEZDCfo+2GLOB844qP0Ah9X/GQT9PuGbpid27MyFe0q41cBN 22 | jq5Au9yiPS6sCJgNjb+p+8YOMlg918tJf0zokBOepfDOiNOGr+87WVa2hLOcEVzb 23 | s3yvo5Rf4uuQcUJlxR507kZ1nqmUuSFSY28RiiRZrUIh1Ng6NbdA1pG8ONq2BVgw 24 | pwtoH3yM1YYUUfC/8p8m4o1BH/KztPQSwtMbHhgCO64Q14Xzwo9rlnzvTaU6ocUC 25 | u5fJnwLp8FnUyda4H3bp4fUBlQ2a67CLh7jOvErdCyQEguUJccfYPJ7WieNiYYln 26 | JAG22D3FoD+g1xbad+W4jBhOp139Phw/zZ+BPbIueZQG9NOjknHw1vM2k7YeGYFu 27 | hueCeg8pL+OZaWs7lCfdC2uUrkk9MRnVoBLwqW0Hd2qResu8PAnm4NbDmJM5Aqaa 28 | ajnmNwOHdsb5TTJGls4D8g5ml+RVlWlERmyUGR7NnQS0SCYLGgPJ4//ajdEVPRGE 29 | Dl67/7rE+c8mPVzrO6FqU8jzGoIJjNToXMPZO7m9+PbCMpm+jkpRfRBDmnzVllBm 30 | S10pGQjzMJCIJt2oBWAiJYTrtgZfCSBSg36atEEr0MgfpmMuDRaVamoCYqddI7Bd 31 | sAr9F8EUeYBI4fOG4/z9FGkj7lRUtHjHG1Yj97+v7+9peZKHjm64Y5ZQ 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/unit/passportTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | /* eslint no-unused-expressions:0 */ 5 | 6 | import should from 'should' 7 | 8 | import * as model from '../../src/model' 9 | 10 | describe('PassportModel tests', () => { 11 | const email = 'bfm@crazy.net' 12 | 13 | const user = new model.UserModelAPI({ 14 | firstname: 'Bill', 15 | surname: 'Murray', 16 | email, 17 | groups: ['HISP', 'group2'] 18 | }) 19 | 20 | before(async () => { 21 | await user.save() 22 | }) 23 | 24 | after(async () => { 25 | await model.UserModelAPI.deleteMany({}) 26 | }) 27 | 28 | describe('.createPassport()', () => { 29 | it('should create a new password', async () => { 30 | const {error, user} = await model.createPassport({email}, 'password') 31 | 32 | should.equal(error, null) 33 | user.should.have.property('email', email) 34 | 35 | const passportResult = await model.PassportModelAPI.findOne({ 36 | email 37 | }) 38 | 39 | passportResult.should.have.property('password') 40 | }) 41 | }) 42 | 43 | describe('.updatePassport()', () => { 44 | it('should update passport of a user', async () => { 45 | const passportResult = await model.PassportModelAPI.findOne({ 46 | email 47 | }) 48 | 49 | const {error, user} = await model.updatePassport( 50 | {email}, 51 | {id: passportResult.id, password: 'new_password'} 52 | ) 53 | 54 | should.equal(error, null) 55 | user.should.have.property('email', email) 56 | 57 | const newPassportResult = await model.PassportModelAPI.findOne({ 58 | email 59 | }) 60 | 61 | newPassportResult.should.have.property('password') 62 | newPassportResult.password.should.not.equal(passportResult.password) 63 | }) 64 | 65 | it('should return error for non existent passport', async () => { 66 | const {error, user} = await model.updatePassport( 67 | {email}, 68 | {id: 'non_existent_id'} 69 | ) 70 | 71 | should.equal(user, null) 72 | error.should.have.property('message') 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /packaging/README.md: -------------------------------------------------------------------------------- 1 | Packaging OpenHIM Core 2 | ===================================== 3 | 4 | # Debian Packaging 5 | 6 | The package is dependant on the system architecture that you build it on (amd64, i386 etc). So, please use an amd64 Ubuntu environment to build this on. 7 | 8 | To create a debian package execute `./create-deb.sh`. This will run you through the process of creating a deb file. It can even upload a package to launchpad for inclusion in the ubuntu reposiotries if you so choose. 9 | 10 | To upload to launchpad you will have to have a public key create in gpg and registered with launchpad. You must link you .gnupg folder to the /packaging folder of this project for the upload to use it. From inside the /packaging folder execute `ln -s ~/.gnupg`. 11 | 12 | You must also have an environment variable set with the id of the key to use. View your keys with `gpg --list-keys` and export the id (the part after the '/') with `export DEB_SIGN_KEYID=xxx`. Now you should be all set to upload to launchpad. Use the following details when running the ./create-deb script. 13 | 14 | Login: openhie 15 | PPA: release 16 | 17 | # Bundled Release 18 | 19 | A bundled release will ensure all the relevant dependencies are downloaded and bundled into a built version of the OpenHIM core. Only the relevant scripts needed to run the OpenHIM core are added to the bundled release. 20 | 21 | To create a new build release execute the below command. This does assume that your Linux distribution has the `zip` module installed 22 | 23 | `./build-release-zip.sh ` 24 | 25 | E.g 26 | 27 | `./build-release-zip.sh v7.0.0` 28 | 29 | # CentOS RPM Packaging 30 | 31 | Building the CentOS package makes uses of a CentOS docker container which runs various commands to build the package. 32 | 33 | Execute the `build-docker-centos-rpm.sh` bash script with a specific release version as an argument to build the RPM package on a specific release version. 34 | 35 | `build-docker-centos-rpm.sh 7.0.0` will build and RPM package for the 7.0.0 release of the OpenHIM 36 | 37 | Once the bash script has completed and cleaned up after itself, you will see the built rpm package in the directory of this script. The package will look something like: 38 | `openhim-core-7.0.0-1.x86_64.rpm` 39 | -------------------------------------------------------------------------------- /test/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {config} from '../src/config' 4 | 5 | export const PORT_START = parseInt(process.env.TEST_PORT, 10) || 32000 6 | export const UDP_PORT = PORT_START + 1 7 | export const TCP_PORT = PORT_START + 2 8 | export const TLS_PORT = PORT_START + 3 9 | export const HTTP_PORT = PORT_START + 4 10 | export const AUDIT_PORT = PORT_START + 5 11 | export const STATIC_PORT = PORT_START + 6 12 | export const HTTPS_PORT = PORT_START + 7 13 | export const MEDIATOR_PORT = PORT_START + 8 14 | 15 | export const DEFAULT_STATUS = 201 16 | 17 | const SERVER_PORT_START = PORT_START + 20 18 | export const SERVER_PORTS = Object.freeze({ 19 | httpPort: SERVER_PORT_START, 20 | httpsPort: SERVER_PORT_START + 1, 21 | apiPort: SERVER_PORT_START + 2, 22 | rerunPort: SERVER_PORT_START + 3, 23 | tcpHttpReceiverPort: SERVER_PORT_START + 4, 24 | pollingPort: SERVER_PORT_START + 5, 25 | auditUDPPort: SERVER_PORT_START + 6, 26 | auditTlsPort: SERVER_PORT_START + 7, 27 | auditTcpPort: SERVER_PORT_START + 8 28 | }) 29 | 30 | export const BASE_URL = `${config.get('api').protocol}://localhost:${ 31 | SERVER_PORTS.apiPort 32 | }` 33 | export const HTTP_BASE_URL = `http://localhost:${SERVER_PORTS.httpPort}` 34 | 35 | export const UPD_SOCKET_TYPE = 'udp4' 36 | export const DEFAULT_HTTP_RESP = 'Mock response body\n' 37 | export const DEFAULT_HTTPS_RESP = 'Secured Mock response body\n' 38 | export const DEFAULT_STATIC_PATH = 'test/resources' 39 | 40 | export const MEDIATOR_HEADERS = { 41 | 'Content-Type': 'application/json+openhim; charset=utf-8' 42 | } 43 | export const DEFAULT_HEADERS = {'Content-Type': 'text/plain'} 44 | export const MEDIATOR_REPONSE = Object.freeze({ 45 | status: 'Successful', 46 | response: { 47 | status: 201, 48 | headers: {}, 49 | body: 'Mock response body\n' 50 | }, 51 | orchestrations: { 52 | name: 'Mock mediator orchestration', 53 | request: { 54 | path: '/some/path', 55 | method: 'GET', 56 | timestamp: new Date().toString() 57 | }, 58 | response: { 59 | status: 200, 60 | body: 'Orchestrated response', 61 | timestamp: new Date().toString() 62 | } 63 | }, 64 | properties: { 65 | prop1: 'val1', 66 | prop2: 'val2' 67 | }, 68 | metrics: { 69 | prop1: 'val1' 70 | } 71 | }) 72 | -------------------------------------------------------------------------------- /src/protocols/local.js: -------------------------------------------------------------------------------- 1 | import {UserModelAPI} from '../model' 2 | import {validatePassword} from '../utils' 3 | import {PassportModelAPI} from '../model/passport' 4 | 5 | /** 6 | * Local Authentication Protocol 7 | * 8 | * The most widely used way for websites to authenticate users is via a username 9 | * and/or email as well as a password. This module provides functions both for 10 | * registering entirely new users, assigning passwords to already registered 11 | * users and validating login requesting. 12 | * 13 | * For more information on local authentication in Passport.js, check out: 14 | * http://passportjs.org/guide/username-password/ 15 | */ 16 | 17 | /** 18 | * Validate a login request 19 | * 20 | * Looks up a user using the supplied identifier (email or username) and then 21 | * attempts to find a local Passport associated with the user. If a Passport is 22 | * found, its password is checked against the password supplied in the form. 23 | * 24 | */ 25 | export const login = async function (email, password, next) { 26 | return await UserModelAPI.findOne({email}) 27 | .then(function (user) { 28 | if (!user) { 29 | return next( 30 | new Error(`No user exists for ${email}, denying access to API`), 31 | false 32 | ) 33 | } 34 | return PassportModelAPI.findOne({ 35 | protocol: user.provider === 'token' ? 'token' : 'local', 36 | email: user.email 37 | }) 38 | .then(function (passport) { 39 | if (passport) { 40 | return validatePassword(passport, password, function (err, res) { 41 | if (err || !res) { 42 | return next( 43 | new Error( 44 | `Wrong password entered by ${email}, denying access to API ${ 45 | err ? err : '' 46 | }` 47 | ), 48 | false 49 | ) 50 | } else { 51 | return next(null, user) 52 | } 53 | }) 54 | } else { 55 | return next( 56 | new Error(`Password not set for ${email}, denying access to API`), 57 | false 58 | ) 59 | } 60 | }) 61 | .catch(next) 62 | }) 63 | .catch(next) 64 | } 65 | -------------------------------------------------------------------------------- /src/model/audits.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {Schema} from 'mongoose' 4 | 5 | import {connectionATNA} from '../config' 6 | 7 | const codeTypeDef = { 8 | code: String, 9 | displayName: String, 10 | codeSystemName: String 11 | } 12 | 13 | const syslogDef = { 14 | prival: Number, 15 | facilityID: Number, 16 | severityID: Number, 17 | facility: String, 18 | severity: String, 19 | type: {type: String}, 20 | time: Date, 21 | host: String, 22 | appName: String, 23 | pid: String, 24 | msgID: String 25 | } 26 | 27 | const ActiveParticipantDef = { 28 | userID: String, 29 | alternativeUserID: String, 30 | userIsRequestor: String, 31 | networkAccessPointID: String, 32 | networkAccessPointTypeCode: String, 33 | roleIDCode: codeTypeDef 34 | } 35 | 36 | const ParticipantObjectIdentificationDef = { 37 | participantObjectID: String, 38 | participantObjectTypeCode: String, 39 | participantObjectTypeCodeRole: String, 40 | participantObjectIDTypeCode: codeTypeDef, 41 | participantObjectQuery: String, 42 | participantObjectDetail: { 43 | type: {type: String}, 44 | value: String 45 | } 46 | } 47 | 48 | const AuditRecordSchema = new Schema({ 49 | rawMessage: String, 50 | syslog: syslogDef, 51 | eventIdentification: { 52 | eventDateTime: { 53 | type: Date, 54 | required: true, 55 | default: Date.now, 56 | index: true 57 | }, 58 | eventOutcomeIndicator: String, 59 | eventActionCode: String, 60 | eventID: codeTypeDef, 61 | eventTypeCode: codeTypeDef 62 | }, 63 | activeParticipant: [ActiveParticipantDef], 64 | auditSourceIdentification: { 65 | auditSourceID: String, 66 | auditEnterpriseSiteID: String, 67 | auditSourceTypeCode: codeTypeDef 68 | }, 69 | participantObjectIdentification: [ParticipantObjectIdentificationDef] 70 | }) 71 | 72 | // keeps track of unique codes for various fields found in the audits collection 73 | const AuditMetaRecordSchema = new Schema( 74 | { 75 | eventType: [codeTypeDef], 76 | eventID: [codeTypeDef], 77 | activeParticipantRoleID: [codeTypeDef], 78 | participantObjectIDTypeCode: [codeTypeDef], 79 | auditSourceID: [String] 80 | }, 81 | { 82 | collection: 'auditMeta' 83 | } 84 | ) 85 | 86 | export const AuditModel = connectionATNA.model('Audit', AuditRecordSchema) 87 | export const AuditMetaModel = connectionATNA.model( 88 | 'AuditMeta', 89 | AuditMetaRecordSchema 90 | ) 91 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {config} from './config' 4 | 5 | export const DEFAULT_API_PORT = 8080 6 | 7 | export const MUTUAL_TLS_AUTH_TYPE = 'mutual-tls-auth' 8 | 9 | export const BASIC_AUTH_TYPE = 'basic-auth' 10 | 11 | export const CUSTOM_TOKEN_AUTH_TYPE = 'custom-token-auth' 12 | 13 | export const JWT_AUTH_TYPE = 'jwt-auth' 14 | 15 | export const JWT_PATTERN = 16 | /^ *(?:[Bb][Ee][Aa][Rr][Ee][Rr]) +([A-Za-z0-9\-._~+/]+=*) *$/ 17 | 18 | export const CUSTOM_TOKEN_PATTERN = 19 | /^ *(?:[Cc][Uu][Ss][Tt][Oo][Mm]) +([A-Za-z0-9\-._~+/]+=*) *$/ 20 | 21 | const OPENHIM_CONSOLE_BASE_URL = config.get('openhimConsoleBaseUrl') 22 | 23 | export const DEFAULT_IMPORT_MAP_PATHS = { 24 | '@jembi/openhim-header': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/openhim-header/dist/jembi-openhim-header.js`, 25 | '@jembi/legacy-console': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/legacy-console/dist/bundle.js`, 26 | '@jembi/openhim-core-api': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/openhim-core-api/dist/jembi-openhim-core-api.js`, 27 | '@jembi/openhim-theme': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/openhim-theme/dist/jembi-openhim-theme.js`, 28 | '@jembi/portal-admin': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/portal-admin/dist/jembi-portal-admin.js`, 29 | '@jembi/openhim-portal': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/openhim-portal/dist/jembi-openhim-portal.js`, 30 | '@jembi/root-config': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/root-config/dist/jembi-root-config.js`, 31 | '@jembi/openhim-sidebar': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/openhim-sidebar/dist/jembi-openhim-sidebar.js`, 32 | '@jembi/clients-app': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/clients-app/dist/jembi-clients-app.js`, 33 | '@jembi/dashboard-app': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/dashboard-app/dist/jembi-dashboard-app.js`, 34 | '@jembi/rbac-app': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/rbac-app/dist/jembi-rbac-app.js`, 35 | '@jembi/footer-app': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/footer-app/dist/jembi-footer-app.js`, 36 | '@jembi/transaction-log': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/transaction-log/dist/jembi-transaction-log.js`, 37 | '@jembi/channels-app': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/channels-app/dist/jembi-channels-app.js`, 38 | '@jembi/users-app': `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/users-app/dist/jembi-users-app.js`, 39 | "@jembi/client-roles-app": `${OPENHIM_CONSOLE_BASE_URL}/libs/@jembi/client-roles-app/dist/jembi-client-roles-app.js` 40 | } 41 | -------------------------------------------------------------------------------- /src/polling.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | import axios from 'axios' 5 | 6 | import * as Channels from './model/channels' 7 | import * as utils from './utils' 8 | import {config} from './config' 9 | import {promisify} from 'util' 10 | 11 | const {ChannelModel} = Channels 12 | config.polling = config.get('polling') 13 | 14 | export let agendaGlobal = null 15 | 16 | export async function registerPollingChannel(channel, callback) { 17 | logger.info(`Registering polling channel: ${channel._id}`) 18 | if (!channel.pollingSchedule) { 19 | return callback(new Error('no polling schedule set on this channel')) 20 | } 21 | 22 | try { 23 | await exports.agendaGlobal.cancel({name: `polling-job-${channel._id}`}) 24 | 25 | exports.agendaGlobal.define(`polling-job-${channel._id}`, (job, done) => { 26 | logger.info(`Polling channel ${channel._id}`) 27 | 28 | const options = { 29 | url: `http://${config.polling.host}:${config.polling.pollingPort}/trigger`, 30 | headers: { 31 | 'channel-id': channel._id, 32 | 'X-OpenHIM-LastRunAt': job.attrs.lastRunAt 33 | } 34 | } 35 | return axios(options).then(() => done()) 36 | }) 37 | 38 | exports.agendaGlobal.every( 39 | channel.pollingSchedule, 40 | `polling-job-${channel._id}`, 41 | null, 42 | {timezone: utils.serverTimezone()} 43 | ) 44 | 45 | return callback(null) 46 | } catch (err) { 47 | return callback(err) 48 | } 49 | } 50 | 51 | export async function removePollingChannel(channel, callback) { 52 | logger.info(`Removing polling schedule for channel: ${channel._id}`) 53 | 54 | try { 55 | await exports.agendaGlobal.cancel({name: `polling-job-${channel._id}`}) 56 | return callback(null) 57 | } catch (err) { 58 | return callback(err) 59 | } 60 | } 61 | 62 | export function setupAgenda(agenda, callback) { 63 | logger.info('Starting polling server...') 64 | const registerPollingChannelPromise = promisify(registerPollingChannel) 65 | agendaGlobal = agenda 66 | return ChannelModel.find({type: 'polling'}, (err, channels) => { 67 | if (err) { 68 | return err 69 | } 70 | 71 | const promises = [] 72 | for (const channel of Array.from(channels)) { 73 | if (Channels.isChannelEnabled(channel)) { 74 | promises.push(registerPollingChannelPromise(channel)) 75 | } 76 | } 77 | 78 | return Promise.all(promises).then(callback) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /test/unit/jwksCacheTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | // Import the necessary modules 6 | import should from 'should' 7 | import sinon from 'sinon' 8 | import proxyquire from 'proxyquire' 9 | 10 | // Create a Sinon stub for JwksRsa 11 | const JwksRsaStub = sinon.stub() 12 | 13 | // Stub the 'getSigningKeys' method 14 | const getSigningKeysStub = sinon.stub() 15 | JwksRsaStub.returns({getSigningKeys: getSigningKeysStub}) 16 | 17 | // Stub the 'config' module 18 | const configStub = { 19 | config: { 20 | get: sinon.stub().returns('http://example.com/jwks') 21 | } 22 | } 23 | 24 | // Use proxyquire to require the module under test, replacing the default export with your stub 25 | const {populateCache, getKey} = proxyquire('../../src/jwksCache', { 26 | 'jwks-rsa': JwksRsaStub, 27 | './config': configStub 28 | }) 29 | 30 | describe('JWKS Cache Test', () => { 31 | beforeEach(() => { 32 | // Reset the stubs before each test 33 | JwksRsaStub.resetHistory() 34 | getSigningKeysStub.resetHistory() 35 | configStub.config.get.resetHistory() 36 | }) 37 | 38 | describe('populateCache', () => { 39 | it('should populate the cache', async () => { 40 | const keys = [{kid: 'key1'}, {kid: 'key2'}] 41 | getSigningKeysStub.resolves(keys) 42 | await populateCache() 43 | 44 | JwksRsaStub.calledOnce.should.be.true() 45 | getSigningKeysStub.calledOnce.should.be.true() 46 | configStub.config.get 47 | .calledWith('authentication:jwt:jwksUri') 48 | .should.be.true() 49 | }) 50 | }) 51 | 52 | describe('getKey', () => { 53 | it('should return the key if it exists in the cache', async () => { 54 | const keys = [{kid: 'key1'}, {kid: 'key2'}] 55 | getSigningKeysStub.resolves(keys) 56 | 57 | await populateCache() 58 | const key = await getKey('key1') 59 | 60 | key.should.deepEqual({kid: 'key1'}) 61 | }) 62 | 63 | it('should populate the cache and return the key if it does not exist in the cache', async () => { 64 | const keys = [{kid: 'key1'}, {kid: 'key2'}] 65 | getSigningKeysStub.onCall(0).resolves(keys) 66 | getSigningKeysStub.onCall(1).resolves(keys.concat({kid: 'key3'})) 67 | 68 | await populateCache() 69 | const key = await getKey('key3') 70 | 71 | JwksRsaStub.calledTwice.should.be.true() 72 | getSigningKeysStub.calledTwice.should.be.true() 73 | configStub.config.get.calledTwice.should.be.true() 74 | should(key).deepEqual({kid: 'key3'}) 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/api/metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | import moment from 'moment' 5 | import mongoose from 'mongoose' 6 | import {register} from 'prom-client' 7 | 8 | import * as authorisation from './authorisation' 9 | import * as metrics from '../metrics' 10 | 11 | // all in one getMetrics generator function for metrics API 12 | export async function getMetrics(ctx, groupChannels, timeSeries, channelID) { 13 | logger.debug( 14 | `Called getMetrics(${groupChannels}, ${timeSeries}, ${channelID})` 15 | ) 16 | const channels = await authorisation.getUserViewableChannels( 17 | ctx.authenticated 18 | ) 19 | let channelIDs = channels.map(c => c._id) 20 | if (typeof channelID === 'string') { 21 | if (channelIDs.map(id => id.toString()).includes(channelID)) { 22 | channelIDs = [mongoose.Types.ObjectId(channelID)] 23 | } else { 24 | ctx.status = 401 25 | return 26 | } 27 | } 28 | 29 | const query = ctx.request.query 30 | 31 | if (!query.startDate || !query.endDate) { 32 | ctx.status = 400 33 | ctx.body = 'Both start and end date are required' 34 | return 35 | } 36 | 37 | const filters = { 38 | startDate: new Date(query.startDate), 39 | endDate: new Date(query.endDate), 40 | timeSeries, 41 | channels: channelIDs 42 | } 43 | 44 | const results = await metrics.calculateMetrics(filters, groupChannels) 45 | ctx.body = results.map(convertMetric) 46 | } 47 | 48 | /** 49 | * Convert metrics to the format expected to be returned by the API to prevent 50 | * breakage. 51 | */ 52 | function convertMetric(metric) { 53 | const timestamp = moment(metric.startTime) 54 | return { 55 | total: metric.requests, 56 | avgResp: calculateAverage(metric.responseTime, metric.requests), 57 | minResp: metric.minResponseTime, 58 | maxResp: metric.maxResponseTime, 59 | failed: metric.failed, 60 | successful: metric.successful, 61 | processing: metric.processing, 62 | completed: metric.completed, 63 | completedWErrors: metric.completedWithErrors, 64 | timestamp: metric.startTime, 65 | _id: { 66 | channelID: metric.channelID, 67 | minute: timestamp.minute(), 68 | hour: timestamp.hour(), 69 | day: timestamp.day(), 70 | week: timestamp.week(), 71 | month: timestamp.month(), 72 | year: timestamp.year() 73 | } 74 | } 75 | } 76 | 77 | function calculateAverage(total, count) { 78 | if (count === 0) { 79 | return 0 80 | } 81 | return total / count 82 | } 83 | 84 | export async function getPrometheusMetrics(ctx) { 85 | ctx.body = await register.metrics() 86 | } 87 | -------------------------------------------------------------------------------- /infrastructure/centos/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CORE_VERSION="master" 4 | CONSOLE_VERSION="master" 5 | 6 | if [ $# -eq 0 ] 7 | then 8 | echo "No arguments supplied. Defaults will be used" 9 | fi 10 | 11 | usage() 12 | { 13 | echo "if this was a real script you would see something useful here" 14 | echo "" 15 | echo "Parameters:" 16 | echo " -h --help" 17 | echo " --core-version=$CORE_VERSION" 18 | echo " --console-version=$CONSOLE_VERSION" 19 | echo "" 20 | } 21 | 22 | # https://gist.github.com/jehiah/855086 23 | while [ "$1" != "" ]; do 24 | PARAM=`echo $1 | awk -F= '{print $1}'` 25 | VALUE=`echo $1 | awk -F= '{print $2}'` 26 | case $PARAM in 27 | -h | --help) 28 | usage 29 | exit 30 | ;; 31 | --core-version) 32 | CORE_VERSION=$VALUE 33 | ;; 34 | --console-version) 35 | CONSOLE_VERSION=$VALUE 36 | ;; 37 | *) 38 | echo "ERROR: unknown parameter \"$PARAM\"" 39 | usage 40 | exit 1 41 | ;; 42 | esac 43 | shift 44 | done 45 | 46 | echo "Core version: $CORE_VERSION" 47 | echo "Console version: $CONSOLE_VERSION" 48 | 49 | yum -y update 50 | yum install -y git rpm-build redhat-rpm-config gcc-c++ make 51 | 52 | # reason for this instead of nvm is that nvm only installs node 53 | curl --silent --location https://rpm.nodesource.com/setup_8.x | bash - 54 | yum install -y nodejs 55 | 56 | # Install nvm and nodejs 57 | npm --version && node -v 58 | 59 | # Generate openhim-core rpm package 60 | git clone https://github.com/jembi/openhim-core-js.git 61 | cd openhim-core-js/ && git checkout $CORE_VERSION 62 | npm install && npm install speculate && npm run build 63 | # generate SPEC file for rpmbuild 64 | npm run spec 65 | # Link source folder with default rpmbuild 66 | ln -s /openhim-core-js ~/rpmbuild 67 | # Build rpm package 68 | node -v && rpmbuild -bb ~/rpmbuild/SPECS/openhim-core.spec 69 | # copy rpm package to user folder for extraction 70 | cp RPMS/x86_64/*.rpm /usr/packages 71 | 72 | 73 | # Generate openhim-console rpm package 74 | cd / && rm -rf ~/rpmbuild && git clone https://github.com/jembi/openhim-console.git 75 | cd openhim-console/ && git checkout $CONSOLE_VERSION 76 | npm install && npm install speculate 77 | # generate SPEC file for rpmbuild 78 | npm run spec 79 | # Link source folder with default rpmbuild 80 | ln -s /openhim-console ~/rpmbuild 81 | # Build rpm package 82 | rpmbuild -bb ~/rpmbuild/SPECS/openhim-console.spec 83 | # copy rpm package to user folder for extraction 84 | cp RPMS/x86_64/*.rpm /usr/packages -------------------------------------------------------------------------------- /performance/mediator/http-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const qs = require('querystring') 4 | const url = require('url') 5 | const BodyStream = require('./body-stream') 6 | 7 | function buildMediatorResponse () { 8 | const now = Date.now() 9 | return `{ 10 | "x-mediator-urn": "urn:uuid:5411f30d-3416-44dc-83f9-406ec5c6a259", 11 | "status": "Successful", 12 | "response": { 13 | "status": 200, 14 | "headers": { 15 | "Content-Type": "application/json" 16 | }, 17 | "body": "{\\"message\\":\\"Hello world\\\\n\\"}", 18 | "timestamp": ${now} 19 | }, 20 | "orchestrations": [ 21 | { 22 | "name": "Test", 23 | "request": { 24 | "method": "POST", 25 | "path": "/", 26 | "headers": { 27 | "Content-Type": "application/x-www-form-urlencoded" 28 | }, 29 | "body": "name=world", 30 | "timestamp": ${now} 31 | }, 32 | "response": { 33 | "status": 200, 34 | "headers": { 35 | "Content-Type": "text/plain" 36 | }, 37 | "body": "Hello world\\n", 38 | "timestamp": ${now} 39 | } 40 | } 41 | ]\n}` 42 | } 43 | 44 | function respondImmediately (req, res) { 45 | res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}) 46 | res.end('Hello world\n') 47 | } 48 | 49 | function respondWithBody (req, res, length) { 50 | if (!Number.isInteger(length)) { 51 | length = 2 * 1024 * 1024 52 | } 53 | res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}) 54 | new BodyStream(length).pipe(res) 55 | } 56 | 57 | function respondAsMediator (req, res, delay) { 58 | if (!Number.isInteger(delay)) { 59 | delay = 500 60 | } 61 | setTimeout(() => { 62 | res.writeHead(200, {'Content-Type': 'application/json+openhim; charset=utf-8'}) 63 | res.end(buildMediatorResponse()) 64 | }, delay) 65 | } 66 | 67 | function handleRequest (req, res) { 68 | const parsed = url.parse(req.url) 69 | if (parsed.pathname === '/immediate') { 70 | return respondImmediately(req, res) 71 | } 72 | if (parsed.pathname === '/body') { 73 | const query = qs.parse(parsed.query) 74 | return respondWithBody(req, res, +query.length) 75 | } 76 | if (parsed.pathname === '/mediator') { 77 | const query = qs.parse(parsed.query) 78 | return respondAsMediator(req, res, +query.delay) 79 | } 80 | res.writeHead(404) 81 | res.end('Not found\n') 82 | } 83 | 84 | exports.handleRequest = handleRequest 85 | -------------------------------------------------------------------------------- /resources/sampleRecords/audit.json: -------------------------------------------------------------------------------- 1 | { 2 | "rawMessage": "This will be the raw ATNA message that gets received to be used as a backup reference", 3 | "eventIdentification": { 4 | "eventDateTime": "2015-02-17T15:38:25.282+02:00", 5 | "eventOutcomeIndicator": "0", 6 | "eventActionCode": "R", 7 | "eventID": { 8 | "code": "222", 9 | "displayName": "Read", 10 | "codeSystemName": "DCM" 11 | }, 12 | "eventTypeCode": { 13 | "code": "ITI-9", 14 | "displayName": "PIX Read", 15 | "codeSystemName": "IHE Transactions" 16 | } 17 | }, 18 | "activeParticipant": [ 19 | { 20 | "userID": "pix|pix", 21 | "alternativeUserID": "2100", 22 | "userIsRequestor": "false", 23 | "networkAccessPointID": "localhost", 24 | "networkAccessPointTypeCode": "1", 25 | "roleIDCode": { 26 | "code": "110152", 27 | "displayName": "Destination", 28 | "codeSystemName": "DCM" 29 | } 30 | }, { 31 | "userID": "pix|pix", 32 | "alternativeUserID": "2100", 33 | "userIsRequestor": "false", 34 | "networkAccessPointID": "localhost", 35 | "networkAccessPointTypeCode": "1", 36 | "roleIDCode": { 37 | "code": "110152", 38 | "displayName": "Destination", 39 | "codeSystemName": "DCM" 40 | } 41 | } 42 | ], 43 | "auditSourceIdentification": { 44 | "auditSourceID": "openhim" 45 | }, 46 | "participantObjectIdentification": [ 47 | { 48 | "participantObjectID": "975cac30-68e5-11e4-bf2a-04012ce65b02^^^ECID&ECID&ISO", 49 | "participantObjectTypeCode": "1", 50 | "participantObjectTypeCodeRole": "1", 51 | "participantObjectIDTypeCode": { 52 | "code": "2", 53 | "displayName": "PatientNumber", 54 | "codeSystemName": "RFC-3881" 55 | } 56 | }, { 57 | "participantObjectID": "dca6c09e-cc92-4bc5-8741-47bd938fa405", 58 | "participantObjectTypeCode": "2", 59 | "participantObjectTypeCodeRole": "24", 60 | "participantObjectIDTypeCode": { 61 | "code": "ITI-9", 62 | "displayName": "PIX Query", 63 | "codeSystemName": "IHE Transactions" 64 | }, 65 | "participantObjectQuery": "TVNIfF5+XCZ8b3BlbmhpbXxvcGVuaGltLW1lZGlhdG9yLW9oaWUteGRzfHBpeHxwaXh8MjAxNTAyMjAxNTM4MjUrMDIwMHx8UUJQXlEyM15RQlBfUTIxfDEwMDQxYWQ5LTkyNDAtNDEyNS04ZDMwLWZiYzczNGEwOTMwMXxQfDIuNQ1RUER8SUhFIFBJWCBRdWVyeXw1OTRhNDVkYS0zOTY5LTQzOTAtODE2Ni01MjhkZDFmNWU0ZTF8NzZjYzc2NWE0NDJmNDEwXl5eJjEuMy42LjEuNC4xLjIxMzY3LjIwMDUuMy43JklTT15QSXxeXl5FQ0lEJkVDSUQmSVNPXlBJDVJDUHxJDQ==", 66 | "participantObjectDetail": { 67 | "type": "MSH-10", 68 | "value": "MTAwNDFhZDktOTI0MC00MTI1LThkMzAtZmJjNzM0YTA5MzAx" 69 | } 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /test/integration/restartAPITests.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | /* eslint no-unused-expressions:0 */ 5 | 6 | import request from 'supertest' 7 | import sinon from 'sinon' 8 | import {ObjectId} from 'mongodb' 9 | import {promisify} from 'util' 10 | 11 | import * as constants from '../constants' 12 | import * as server from '../../src/server' 13 | import * as testUtils from '../utils' 14 | import {ChannelModelAPI} from '../../src/model/channels' 15 | 16 | describe('API Integration Tests', () => { 17 | const {SERVER_PORTS, BASE_URL} = constants 18 | 19 | describe('Restart REST Api testing', () => { 20 | const channel = new ChannelModelAPI({ 21 | name: 'TestChannel1', 22 | urlPattern: 'test/sample', 23 | allow: ['PoC', 'Test1', 'Test2'], 24 | routes: [ 25 | { 26 | name: 'test route', 27 | host: 'localhost', 28 | port: 9876, 29 | primary: true 30 | } 31 | ], 32 | txViewAcl: ['group1'], 33 | txViewFullAcl: [], 34 | updatedBy: { 35 | id: new ObjectId(), 36 | name: 'Test' 37 | } 38 | }) 39 | 40 | before(async () => { 41 | await testUtils.cleanupTestUsers() 42 | await Promise.all([ 43 | channel.save(), 44 | promisify(server.start)({apiPort: SERVER_PORTS.apiPort}), 45 | testUtils.setupTestUsers() 46 | ]) 47 | }) 48 | 49 | after(async () => { 50 | await Promise.all([ 51 | testUtils.cleanupTestUsers(), 52 | ChannelModelAPI.deleteMany({}), 53 | promisify(server.stop)() 54 | ]) 55 | }) 56 | 57 | describe('*restart()', () => { 58 | it('should successfully send API request to restart the server', async () => { 59 | const user = testUtils.rootUser 60 | const cookie = await testUtils.authenticate(request, BASE_URL, user) 61 | 62 | const stub = await sinon.stub(server, 'startRestartServerTimeout') 63 | 64 | await request(BASE_URL) 65 | .post('/restart') 66 | .set('Cookie', cookie) 67 | .send() 68 | .expect(200) 69 | 70 | stub.calledOnce.should.be.true() 71 | }) 72 | 73 | it('should not allow non admin user to restart the server', async () => { 74 | const user = testUtils.nonRootUser 75 | const cookie = await testUtils.authenticate(request, BASE_URL, user) 76 | 77 | await request(BASE_URL) 78 | .post('/restart') 79 | .set('Cookie', cookie) 80 | .send() 81 | .expect(403) 82 | }) 83 | 84 | it('should return 401 for unauthenticated restarting the server', async () => { 85 | await request(BASE_URL).post('/restart').expect(401) 86 | }) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /src/middleware/authorisation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import atna from 'atna-audit' 4 | import logger from 'winston' 5 | import os from 'os' 6 | 7 | import * as auditing from '../auditing' 8 | import {config} from '../config' 9 | import {promisify} from 'util' 10 | 11 | config.authentication = config.get('authentication') 12 | const himSourceID = config.get('auditing').auditEvents.auditSourceID 13 | 14 | function genAuthAudit(remoteAddress) { 15 | let audit = atna.construct.nodeAuthentication( 16 | remoteAddress, 17 | himSourceID, 18 | os.hostname(), 19 | atna.constants.OUTCOME_MINOR_FAILURE 20 | ) 21 | audit = atna.construct.wrapInSyslog(audit) 22 | return audit 23 | } 24 | 25 | function authoriseClient(channel, ctx) { 26 | if (ctx.authenticated != null && channel.allow != null) { 27 | if (ctx.authenticated.roles != null) { 28 | for (const role of Array.from(channel.allow)) { 29 | if (Array.from(ctx.authenticated.roles).includes(role)) { 30 | return true 31 | } 32 | } 33 | } 34 | if (Array.from(channel.allow).includes(ctx.authenticated.clientID)) { 35 | return true 36 | } 37 | } 38 | 39 | return false 40 | } 41 | 42 | function authoriseIP(channel, ctx) { 43 | if ((channel.whitelist != null ? channel.whitelist.length : undefined) > 0) { 44 | return Array.from(channel.whitelist).includes(ctx.ip) 45 | } else { 46 | return true // whitelist auth not required 47 | } 48 | } 49 | 50 | export async function authorise(ctx, done) { 51 | const channel = ctx.matchingChannel 52 | 53 | if ( 54 | channel != null && 55 | authoriseIP(channel, ctx) && 56 | (channel.authType === 'public' || authoriseClient(channel, ctx)) 57 | ) { 58 | // authorisation succeeded 59 | ctx.authorisedChannel = channel 60 | logger.info( 61 | `The request, '${ctx.request.path}' is authorised to access ${ctx.authorisedChannel.name}` 62 | ) 63 | } else if (!channel) { 64 | // Channel not found 65 | ctx.response.status = 404 66 | } else { 67 | // authorisation failed 68 | ctx.response.status = 401 69 | if (config.authentication.enableBasicAuthentication) { 70 | ctx.set('WWW-Authenticate', 'Basic') 71 | } 72 | logger.info( 73 | `The request, '${ctx.request.path}', is not authorised to access any channels.` 74 | ) 75 | auditing.sendAuditEvent(genAuthAudit(ctx.ip), () => 76 | logger.debug('Processed nodeAuthentication audit') 77 | ) 78 | } 79 | 80 | return done() 81 | } 82 | 83 | export async function koaMiddleware(ctx, next) { 84 | const _authorise = promisify(authorise) 85 | await _authorise(ctx) 86 | if (ctx.authorisedChannel != null) { 87 | await next() 88 | } 89 | } 90 | 91 | // export private functions for unit testing 92 | // note: you cant spy on these method because of this :( 93 | if (process.env.NODE_ENV === 'test') { 94 | exports.genAuthAudit = genAuthAudit 95 | } 96 | -------------------------------------------------------------------------------- /performance/mediator/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const http = require('http') 5 | const https = require('https') 6 | const path = require('path') 7 | const tcp = require('net') 8 | const tls = require('tls') 9 | 10 | const httpHandler = require('./http-handler') 11 | const tcpHandler = require('./tcp-handler') 12 | 13 | const config = { 14 | httpPort: +(process.env.HTTP_PORT || 8082), 15 | httpsPort: +(process.env.HTTPS_PORT || 8443), 16 | tcpBodyPort: +(process.env.TCP_BODY_PORT || 9000), 17 | tlsBodyPort: +(process.env.TLS_BODY_PORT || 9001), 18 | tcpDelayPort: +(process.env.TCP_DELAY_PORT || 9002), 19 | tlsDelayPort: +(process.env.TLS_DELAY_PORT || 9003), 20 | tcpImmediatePort: +(process.env.TCP_IMMEDIATE_PORT || 9004), 21 | tlsImmediatePort: +(process.env.TLS_IMMEDIATE_PORT || 9005) 22 | } 23 | 24 | const tlsOptions = { 25 | key: fs.readFileSync(path.join(__dirname, 'tls', 'key.pem')), 26 | cert: fs.readFileSync(path.join(__dirname, 'tls', 'cert.pem')) 27 | } 28 | 29 | const tcpOptions = { 30 | allowHalfOpen: true 31 | } 32 | 33 | const httpServer = http.createServer(httpHandler.handleRequest) 34 | httpServer.listen(config.httpPort, () => { 35 | console.log(`HTTP server started on ${config.httpPort}`) 36 | }) 37 | 38 | const httpsServer = https.createServer(tlsOptions, httpHandler.handleRequest) 39 | httpsServer.listen(config.httpsPort, () => { 40 | console.log(`HTTPS server started on ${config.httpsPort}`) 41 | }) 42 | 43 | const tcpBodyServer = tcp.createServer(tcpOptions, tcpHandler.handleBodyRequest) 44 | tcpBodyServer.listen(config.tcpBodyPort, () => { 45 | console.log(`TCP body server started on ${config.tcpBodyPort}`) 46 | }) 47 | 48 | const tlsBodyServer = tls.createServer(Object.assign({}, tcpOptions, tlsOptions), tcpHandler.handleBodyRequest) 49 | tlsBodyServer.listen(config.tlsBodyPort, () => { 50 | console.log(`TLS body server started on ${config.tlsBodyPort}`) 51 | }) 52 | 53 | const tcpDelayServer = tcp.createServer(tcpOptions, tcpHandler.handleDelayRequest) 54 | tcpDelayServer.listen(config.tcpDelayPort, () => { 55 | console.log(`TCP delay server started on ${config.tcpDelayPort}`) 56 | }) 57 | 58 | const tlsDelayServer = tls.createServer(Object.assign({}, tcpOptions, tlsOptions), tcpHandler.handleDelayRequest) 59 | tlsDelayServer.listen(config.tlsDelayPort, () => { 60 | console.log(`TLS delay server started on ${config.tlsDelayPort}`) 61 | }) 62 | 63 | const tcpImmediateServer = tcp.createServer(tcpOptions, tcpHandler.handleImmediateRequest) 64 | tcpImmediateServer.listen(config.tcpImmediatePort, () => { 65 | console.log(`TCP immediate server started on ${config.tcpImmediatePort}`) 66 | }) 67 | 68 | const tlsImmediateServer = tls.createServer(Object.assign({}, tcpOptions, tlsOptions), tcpHandler.handleImmediateRequest) 69 | tlsImmediateServer.listen(config.tlsImmediatePort, () => { 70 | console.log(`TLS immediate server started on ${config.tlsImmediatePort}`) 71 | }) 72 | -------------------------------------------------------------------------------- /resources/certs/ihe/README.TXT: -------------------------------------------------------------------------------- 1 | #******************************************************************************* 2 | # README.TXT R Moulton 3 | # ReadMe file for IHE Pre-Connectathon and Connectathon Certificate Generation. 4 | # 2015 and 2016 Connectathons 5 | #******************************************************************************* 6 | 7 | The contents of this (cert.zip) file were created by the Certificate generation 8 | software using the parameter values you entered on the web application form. 9 | Commands are in Linux format. You will not need all of these files, and you may 10 | need other files than these, depending on your software. These files are 11 | provided for the convenience of various different types of developers. If you 12 | have suggestions on how to improve this distribution, email them to Ralph 13 | Moulton at moultonr@mir.wustl.edu. 14 | 15 | Contents of the cert.zip file: 16 | 17 | dn.txt - Contains the entries you gave in the Certificate Generation web 18 | application, and which were used to generate your certificates. 19 | 20 | password.txt - A text file containing your password. In the commands below, the 21 | appearance of pw is understood to be replaced with the password in this file. 22 | 23 | key.pem - Your private key, in PEM format. Generated using: 24 | 25 | openssl genrsa -out key.pem 1024 26 | 27 | cert.pem - Your certificate, in PEM format. Generated using: 28 | 29 | openssl req -new -key key.pem -out csr -in dn.txt 30 | openssl x509 -req -inform PEM -in csr -days 435 -CA cacert.pem -keyform PEM \ 31 | -CAkey cakey.pem -set_serial sn -out cert.pem 32 | 33 | where cacert.pem is the CA certificate you selected on the web application 34 | form, from those listed below cacert.MIR2014-16.pem, cakey.pem is the private 35 | key of that certificate, and sn is an incremental serial number. The 36 | certificate request file (csr) is deleted afterwards. 37 | 38 | cacert.MIR2014-16.pem - The CA certificate for 2015 and 2016 connectathons. 39 | Software for these connectathons will have to accept certificates signed by 40 | this certificate. 41 | 42 | cert.p12 - A PKCS12 file containing your private key and certificate, in PEM 43 | format. Generated using: 44 | 45 | openssl pkcs12 -export -in cert.pem -inkey key.pem -out cert.p12 \ 46 | -passout pass:pw 47 | 48 | keystore.jks - A java keystore file in JKS format containing your certificate 49 | and key and the CA certificates. Generated using: 50 | 51 | keytool -import -trustcacerts -file cacert.MIR2014-16.pem \ 52 | -keystore keystore.jks -storepass pw -alias MIR2014-16 53 | keytool -importkeystore -srckeystore cert.p12 -destkeystore keystore.jks \ 54 | -srcstoretype PKCS12 -deststoreType JKS -srcstorepass pw \ 55 | -deststorepass pw 56 | -------------------------------------------------------------------------------- /src/middleware/basicAuthentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import auth from 'basic-auth' 4 | import bcrypt from 'bcryptjs' 5 | import crypto from 'crypto' 6 | import logger from 'winston' 7 | import {promisify} from 'util' 8 | 9 | import {ClientModel} from '../model/clients' 10 | 11 | const bcryptCompare = (pass, client, callback) => 12 | bcrypt.compare(pass, client.passwordHash, callback) 13 | 14 | function cryptoCompare(pass, client, callback) { 15 | const hash = crypto.createHash(client.passwordAlgorithm) 16 | hash.update(pass) 17 | hash.update(client.passwordSalt) 18 | if (hash.digest('hex') === client.passwordHash) { 19 | return callback(null, true) 20 | } else { 21 | return callback(null, false) 22 | } 23 | } 24 | 25 | function comparePasswordWithClientHash(pass, client, callback) { 26 | if (Array.from(crypto.getHashes()).includes(client.passwordAlgorithm)) { 27 | return cryptoCompare(pass, client, callback) 28 | } else { 29 | return bcryptCompare(pass, client, callback) 30 | } 31 | } 32 | 33 | export function authenticateUser(ctx, done) { 34 | const user = auth(ctx.req) 35 | 36 | if (user) { 37 | return ClientModel.findOne({clientID: user.name}, (err, client) => { 38 | if (err) { 39 | return done(err) 40 | } 41 | 42 | if (client) { 43 | if (!(client.passwordAlgorithm && client.passwordHash)) { 44 | logger.warn(`${user.name} does not have a basic auth password set`) 45 | return done(null, null) 46 | } 47 | 48 | return comparePasswordWithClientHash(user.pass, client, (err, res) => { 49 | if (err) { 50 | return done(err) 51 | } 52 | 53 | if (res) { 54 | logger.info(`Client (${client.name}) is Authenticated.`) 55 | ctx.authenticated = client 56 | ctx.authenticationType = 'basic' 57 | return done(null, client) 58 | } else { 59 | logger.info( 60 | `${user.name} could NOT be authenticated, trying next auth mechanism if any...` 61 | ) 62 | return done(null, null) 63 | } 64 | }) 65 | } else { 66 | logger.info( 67 | `${user.name} not found, trying next auth mechanism if any...` 68 | ) 69 | return done(null, null) 70 | } 71 | }) 72 | } else { 73 | logger.info( 74 | 'No basic auth details supplied, trying next auth mechanism if any...' 75 | ) 76 | ctx.authenticated = null // Set to empty object rather than null 77 | return done(null, null) 78 | } 79 | } 80 | 81 | /* 82 | * Koa middleware for authentication by basic auth 83 | */ 84 | export async function koaMiddleware(ctx, next) { 85 | if (ctx.authenticated != null) { 86 | await next() 87 | } else { 88 | const _authenticateUser = promisify(authenticateUser) 89 | await _authenticateUser(ctx) 90 | if ( 91 | (ctx.authenticated != null ? ctx.authenticated.clientID : undefined) != 92 | null 93 | ) { 94 | ctx.header['X-OpenHIM-ClientID'] = ctx.authenticated.clientID 95 | } 96 | await next() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/middleware/rerunUpdateTransactionTask.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | import {promisify} from 'util' 5 | 6 | import {TaskModel} from '../model/tasks' 7 | import {TransactionModel} from '../model/transactions' 8 | 9 | export function setAttemptNumber(ctx, done) { 10 | return TransactionModel.findOne({_id: ctx.parentID}, (err, transaction) => { 11 | if (err) { 12 | return done(err) 13 | } 14 | if (transaction.autoRetry) { 15 | if (transaction.autoRetryAttempt != null) { 16 | ctx.currentAttempt = transaction.autoRetryAttempt + 1 17 | } else { 18 | ctx.currentAttempt = 1 19 | } 20 | } 21 | return transaction.save((err, tx) => { 22 | if (err) { 23 | logger.error( 24 | `Original transaction ${transaction._id} could not be updated: ${err}` 25 | ) 26 | } else { 27 | logger.debug( 28 | `Original transaction #${tx._id} Updated successfully with attempt number` 29 | ) 30 | } 31 | 32 | return done(null) 33 | }) 34 | }) 35 | } 36 | 37 | export function updateOriginalTransaction(ctx, done) { 38 | return TransactionModel.findOne({_id: ctx.parentID}, (err, transaction) => { 39 | if (err) { 40 | return done(err) 41 | } 42 | transaction.childIDs.push(ctx.transactionId) 43 | transaction.wasRerun = true 44 | 45 | return transaction.save((err, tx) => { 46 | if (err) { 47 | logger.error( 48 | `Original transaction ${transaction._id} could not be updated: ${err}` 49 | ) 50 | } else { 51 | logger.debug( 52 | `Original transaction ${tx._id} - Updated successfully with childID` 53 | ) 54 | } 55 | 56 | return done(null, transaction) 57 | }) 58 | }) 59 | } 60 | 61 | export function updateTask(ctx, done) { 62 | return TaskModel.findOne({_id: ctx.taskID}, (err, task) => { 63 | if (err) { 64 | return done(err) 65 | } 66 | task.transactions.forEach(tx => { 67 | if (tx.tid === ctx.parentID) { 68 | tx.rerunID = ctx.transactionId 69 | tx.rerunStatus = ctx.transactionStatus 70 | } 71 | }) 72 | 73 | return task.save((err, task) => { 74 | if (err) { 75 | logger.info(`Rerun Task ${ctx.taskID} could not be updated: ${err}`) 76 | } else { 77 | logger.info( 78 | `Rerun Task ${ctx.taskID} - Updated successfully with rerun transaction details.` 79 | ) 80 | } 81 | 82 | return done(null, task) 83 | }) 84 | }) 85 | } 86 | 87 | /* 88 | * Koa middleware for updating original transaction with childID 89 | */ 90 | export async function koaMiddleware(ctx, next) { 91 | const setAttempt = promisify(setAttemptNumber) 92 | await setAttempt(ctx) 93 | 94 | // do intial yield for koa to come back to ctx function with updated ctx object 95 | await next() 96 | const _updateOriginalTransaction = promisify(updateOriginalTransaction) 97 | await _updateOriginalTransaction(ctx) 98 | 99 | const _updateTask = promisify(updateTask) 100 | await _updateTask(ctx) 101 | } 102 | -------------------------------------------------------------------------------- /packaging/build-docker-centos-rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RELEASE_VERSION=$1 4 | if [ -z ${RELEASE_VERSION} ] 5 | then 6 | echo "You need so specify the release version you wish to build: e.g './build-docker-centos-rpm.sh 4.0.0'" 7 | echo "https://github.com/jembi/openhim-core-js/releases" 8 | exit 1 9 | fi 10 | 11 | set -eu 12 | 13 | # Set docker container name to build RPM package 14 | containerName=openhim-core-centos-rpm 15 | 16 | # Define the CentOS version to build in the docker container 17 | docker pull centos:7 18 | 19 | docker run -t -d --rm --name $containerName centos:7 /bin/bash 20 | 21 | echo "Update packages: " 22 | docker exec -it $containerName sh -c "yum -y update" 23 | 24 | echo "Install needed packages: " 25 | docker exec -it $containerName sh -c "yum install -y git rpm-build redhat-rpm-config gcc-c++ make" 26 | 27 | echo "Install needed packages: " 28 | docker exec -it $containerName sh -c "curl -sL https://rpm.nodesource.com/setup_12.x | bash -" 29 | 30 | echo "Install needed packages: " 31 | docker exec -it $containerName sh -c "yum -y install nodejs-12.18.1" 32 | 33 | echo "https://github.com/jembi/openhim-core-js/archive/v$RELEASE_VERSION.tar.gz" 34 | echo "Fetch release version from Github" 35 | docker exec -it $containerName sh -c "mkdir /openhim-core-js && curl -sL 'https://github.com/jembi/openhim-core-js/archive/v$RELEASE_VERSION.tar.gz' | tar --strip-components=1 -zxv -C /openhim-core-js" 36 | 37 | echo "npm install && npm install speculate && npm run build" 38 | docker exec -it $containerName sh -c "cd /openhim-core-js && npm install && npm install speculate && npm run build && npm run spec" 39 | 40 | echo "Symlink the openhim-core folder with the rpmbuild folder" 41 | docker exec -it $containerName sh -c "ln -s /openhim-core-js ~/rpmbuild" 42 | 43 | # if the Release Version incluldes a dash, apply workaround for rpmbuild to not break on dashes 44 | if [[ "${RELEASE_VERSION}" == *"-"* ]] 45 | then 46 | RELEASE_VERSION_TEMP=${RELEASE_VERSION//-/_} 47 | echo "Release Version contains unsupported dash (-) for building rpm package. Replacing with underscore (_) temporarily" 48 | docker exec -it $containerName sh -c "sed -i 's/$RELEASE_VERSION/$RELEASE_VERSION_TEMP/g' ~/rpmbuild/SPECS/openhim-core.spec" 49 | fi 50 | 51 | echo "Build RPM package from spec" 52 | docker exec -it $containerName sh -c "rpmbuild -bb ~/rpmbuild/SPECS/openhim-core.spec" 53 | 54 | # if the Release Version incluldes a dash, apply workaround for rpmbuild to not break on dashes 55 | if [[ "${RELEASE_VERSION}" == *"-"* ]] 56 | then 57 | RELEASE_VERSION_TEMP=${RELEASE_VERSION//-/_} 58 | echo "Rename the generated RPM package to the expected release version name (revert the changes from underscore to dashes)" 59 | docker exec -it $containerName sh -c "mv /openhim-core-js/RPMS/x86_64/openhim-core-$RELEASE_VERSION_TEMP-1.x86_64.rpm /openhim-core-js/RPMS/x86_64/openhim-core-$RELEASE_VERSION-1.x86_64.rpm" 60 | fi 61 | 62 | echo "Extract RPM package from container" 63 | docker cp $containerName:/openhim-core-js/RPMS/x86_64/openhim-core-$RELEASE_VERSION-1.x86_64.rpm . 64 | 65 | # Stop the container to ensure it gets cleaned up after running to commands 66 | echo "Removing the container" 67 | docker stop $containerName 68 | -------------------------------------------------------------------------------- /performance/metrics.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | import {check, group} from 'k6' 3 | import {getTestAuthHeaders} from './auth.js' 4 | 5 | const BASE_URL = __ENV.BASE_URL || 'https://localhost:8080' 6 | 7 | export const options = { 8 | vus: 1, 9 | iterations: 100, 10 | thresholds: { 11 | http_req_duration: ['p(95)<50'] 12 | }, 13 | insecureSkipTLSVerify: true 14 | } 15 | 16 | function getMetricsByMinute() { 17 | const res = http.get( 18 | `${BASE_URL}/metrics/timeseries/minute?startDate=2017-12-01T10:00:00.000Z&endDate=2017-12-01T11:00:00.000Z`, 19 | { 20 | headers: Object.assign(getTestAuthHeaders(), { 21 | Accept: 'application/json' 22 | }), 23 | tags: { 24 | name: 'All metrics by minute' 25 | } 26 | } 27 | ) 28 | check(res, { 29 | 'status code is 200': r => r.status === 200 30 | }) 31 | } 32 | 33 | function getMetricsByHour() { 34 | const res = http.get( 35 | `${BASE_URL}/metrics/timeseries/hour?startDate=2017-12-01T00:00:00.000Z&endDate=2017-12-01T23:59:59.999Z`, 36 | { 37 | headers: Object.assign(getTestAuthHeaders(), { 38 | Accept: 'application/json' 39 | }), 40 | tags: { 41 | name: 'All metrics by hour' 42 | } 43 | } 44 | ) 45 | check(res, { 46 | 'status code is 200': r => r.status === 200 47 | }) 48 | } 49 | 50 | function getMetricsByDay() { 51 | const res = http.get( 52 | `${BASE_URL}/metrics/timeseries/day?startDate=2017-12-01&endDate=2017-12-08`, 53 | { 54 | headers: Object.assign(getTestAuthHeaders(), { 55 | Accept: 'application/json' 56 | }), 57 | tags: { 58 | name: 'All metrics by day' 59 | } 60 | } 61 | ) 62 | check(res, { 63 | 'status code is 200': r => r.status === 200 64 | }) 65 | } 66 | 67 | function getMetricsByMonth() { 68 | const res = http.get( 69 | `${BASE_URL}/metrics/timeseries/month?startDate=2017-01-01&endDate=2017-12-31`, 70 | { 71 | headers: Object.assign(getTestAuthHeaders(), { 72 | Accept: 'application/json' 73 | }), 74 | tags: { 75 | name: 'All metrics by month' 76 | } 77 | } 78 | ) 79 | check(res, { 80 | 'status code is 200': r => r.status === 200 81 | }) 82 | } 83 | 84 | function getMetricsByChannel() { 85 | const res = http.get( 86 | `${BASE_URL}/metrics/channels/303030303030303030303030?startDate=2017-01-01T00:00:00.000Z&endDate=2017-01-01T23:59:59.999Z`, 87 | { 88 | headers: Object.assign(getTestAuthHeaders(), { 89 | Accept: 'application/json' 90 | }), 91 | tags: { 92 | name: 'Metrics by channel' 93 | } 94 | } 95 | ) 96 | check(res, { 97 | 'status code is 200': r => r.status === 200 98 | }) 99 | } 100 | 101 | export default function execute() { 102 | group('Metrics', () => { 103 | group('By time range', () => { 104 | group('By minute', getMetricsByMinute) 105 | group('By hour', getMetricsByHour) 106 | group('By day', getMetricsByDay) 107 | group('By month', getMetricsByMonth) 108 | }) 109 | group('By channel', getMetricsByChannel) 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /src/api/certificateAuthority.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import logger from 'winston' 4 | import pem from 'pem' 5 | 6 | import * as utils from '../utils' 7 | import {KeystoreModelAPI} from '../model/keystore' 8 | import {promisify} from 'util' 9 | 10 | const readCertificateInfo = promisify(pem.readCertificateInfo) 11 | const getFingerprint = promisify(pem.getFingerprint) 12 | 13 | export async function generateCert(ctx) { 14 | const authorised = await utils.checkUserPermission(ctx, 'generateCert', 'certificates-manage') 15 | 16 | if (!authorised) return 17 | 18 | let result 19 | 20 | const { 21 | request: {body: options} 22 | } = ctx 23 | if (options.type === 'server') { 24 | logger.info('Generating server cert') 25 | result = await generateServerCert(options, ctx) 26 | } else { 27 | logger.info('Generating client cert') 28 | result = await generateClientCert(options, ctx) 29 | } 30 | ctx.status = 201 31 | ctx.body = result 32 | } 33 | 34 | async function generateClientCert(options, ctx) { 35 | const keystoreDoc = await KeystoreModelAPI.findOne() 36 | 37 | // Set additional options 38 | options.selfSigned = true 39 | 40 | // Attempt to create the certificate 41 | try { 42 | ctx.body = await createCertificate(options) 43 | const certInfo = await extractCertMetadata(ctx.body.certificate, ctx) 44 | keystoreDoc.ca.push(certInfo) 45 | await keystoreDoc.save() 46 | // Add the new certificate to the keystore 47 | ctx.status = 201 48 | logger.info('Client certificate created') 49 | } catch (err) { 50 | utils.logAndSetResponse( 51 | ctx, 52 | 'internal server error', 53 | `Could not create a client cert via the API: ${err}`, 54 | 'error' 55 | ) 56 | } 57 | return ctx.body 58 | } 59 | 60 | async function generateServerCert(options, ctx) { 61 | const keystoreDoc = await KeystoreModelAPI.findOne() 62 | options.selfSigned = true 63 | try { 64 | ctx.body = await createCertificate(options) 65 | keystoreDoc.cert = await extractCertMetadata(ctx.body.certificate, ctx) 66 | keystoreDoc.key = ctx.body.key 67 | await keystoreDoc.save() 68 | // Add the new certificate to the keystore 69 | ctx.status = 201 70 | logger.info('Server certificate created') 71 | } catch (err) { 72 | utils.logAndSetResponse( 73 | ctx, 74 | 'internal server error', 75 | `Could not create a client cert via the API: ${err}`, 76 | 'error' 77 | ) 78 | } 79 | return ctx.body 80 | } 81 | 82 | function createCertificate(options) { 83 | return new Promise((resolve, reject) => { 84 | pem.createCertificate(options, (err, cert) => { 85 | if (err) { 86 | return reject(err) 87 | } 88 | resolve({ 89 | certificate: cert.certificate, 90 | key: cert.clientKey 91 | }) 92 | }) 93 | }) 94 | } 95 | 96 | async function extractCertMetadata(cert, ctx) { 97 | const certInfo = await readCertificateInfo(cert) 98 | const fingerprint = await getFingerprint(cert) 99 | certInfo.data = ctx.body.certificate 100 | certInfo.fingerprint = fingerprint.fingerprint 101 | return certInfo 102 | } 103 | -------------------------------------------------------------------------------- /test/unit/metadataTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | import * as metadata from '../../src/api/metadata' 6 | 7 | describe('Metadata Functions', () => { 8 | describe('.removeProperties', () => 9 | it('should return an object with _id and __v removed from all objects in the object', done => { 10 | const object = { 11 | _id: '11111', 12 | __v: 'test', 13 | someProp: 'hello', 14 | innerObj: { 15 | _id: '11111', 16 | __v: 'test', 17 | someOtherProp: 'hello' 18 | } 19 | } 20 | const result = metadata.removeProperties(object) 21 | result.should.have.property('someProp', 'hello') 22 | result.should.have.property('innerObj', {someOtherProp: 'hello'}) 23 | result.should.not.have.property('_id', '11111') 24 | result.should.not.have.property('__v', 'test') 25 | return done() 26 | })) 27 | 28 | describe('.getUniqueIdentifierForCollection', () => 29 | it("should return objects with the collection's unique attribute and the respective value", done => { 30 | let result = metadata.getUniqueIdentifierForCollection('Channels', { 31 | name: 'channelUID' 32 | }) 33 | result.should.have.property('name', 'channelUID') 34 | 35 | result = metadata.getUniqueIdentifierForCollection('Clients', { 36 | clientID: 'clientUID' 37 | }) 38 | result.should.have.property('clientID', 'clientUID') 39 | 40 | result = metadata.getUniqueIdentifierForCollection('Mediators', { 41 | urn: 'mediatorUID' 42 | }) 43 | result.should.have.property('urn', 'mediatorUID') 44 | 45 | result = metadata.getUniqueIdentifierForCollection('Passports', { 46 | email: 'userEmail', 47 | protocol: 'local' 48 | }) 49 | result.should.have.property('email', 'userEmail') 50 | result.should.have.property('protocol', 'local') 51 | 52 | result = metadata.getUniqueIdentifierForCollection('Users', { 53 | email: 'userUID' 54 | }) 55 | result.should.have.property('email', 'userUID') 56 | 57 | result = metadata.getUniqueIdentifierForCollection('ContactGroups', { 58 | groups: 'cgUID' 59 | }) 60 | result.should.have.property('groups', 'cgUID') 61 | 62 | result = metadata.getUniqueIdentifierForCollection('Invalid-collection', { 63 | id: '123333' 64 | }) 65 | result.should.deepEqual({}) 66 | return done() 67 | })) 68 | 69 | describe('.buildResponseObject', () => 70 | it('build a response object', done => { 71 | const model = 'Channels' 72 | const doc = { 73 | name: 'Channel1', 74 | urlPattern: 'test/sample' 75 | } 76 | const status = 'Valid' 77 | const message = '' 78 | const uid = 'Channel1' 79 | 80 | const result = metadata.buildResponseObject( 81 | model, 82 | doc, 83 | status, 84 | message, 85 | uid 86 | ) 87 | result.should.have.property('model', 'Channels') 88 | result.should.have.property('record', { 89 | name: 'Channel1', 90 | urlPattern: 'test/sample' 91 | }) 92 | result.should.have.property('status', 'Valid') 93 | result.should.have.property('message', '') 94 | result.should.have.property('uid', 'Channel1') 95 | return done() 96 | })) 97 | }) 98 | -------------------------------------------------------------------------------- /test/unit/customTokenAuthenticationTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | import logger from 'winston' 6 | import should from 'should' 7 | import sinon from 'sinon' 8 | 9 | import * as customTokenAuthentication from '../../src/middleware/customTokenAuthentication' 10 | import * as client from '../../src/model/clients' 11 | 12 | describe('Custom Token Authorization Test', () => { 13 | describe('koa middleware', () => { 14 | let sandbox = sinon.createSandbox() 15 | 16 | afterEach(() => { 17 | sandbox.restore() 18 | }) 19 | 20 | it('should skip middleware if ctx is authenticated', async () => { 21 | const ctx = { 22 | authenticated: { 23 | clientID: 'test' 24 | }, 25 | header: {} 26 | } 27 | const next = sandbox.spy() 28 | 29 | await customTokenAuthentication.koaMiddleware(ctx, next) 30 | should(ctx.header['X-OpenHIM-ClientID']).eql('test') 31 | next.callCount.should.eql(1) 32 | }) 33 | 34 | it('should succeed when Custom Token ID correlates to a client', async () => { 35 | const ctx = { 36 | header: {}, 37 | request: { 38 | header: { 39 | authorization: 'Custom test1' 40 | } 41 | } 42 | } 43 | const next = sandbox.spy() 44 | 45 | const loggerStub = sandbox.stub(logger, 'info') 46 | 47 | const clientStub = sandbox 48 | .stub(client.ClientModel, 'findOne') 49 | .withArgs({customTokenID: 'test1'}) 50 | .resolves({name: 'Test', clientID: 'test'}) 51 | 52 | await customTokenAuthentication.koaMiddleware(ctx, next) 53 | 54 | next.callCount.should.eql(1) 55 | clientStub.callCount.should.eql(1) 56 | loggerStub.callCount.should.eql(1) 57 | should(ctx.authenticated).be.ok() 58 | should(ctx.authenticationType).eql('token') 59 | should(ctx.header['X-OpenHIM-ClientID']).eql('test') 60 | }) 61 | 62 | it('should fail when authentication header is missing', async () => { 63 | const ctx = { 64 | header: {}, 65 | request: { 66 | header: {} 67 | } 68 | } 69 | const next = sandbox.spy() 70 | 71 | const loggerStub = sandbox.stub(logger, 'debug') 72 | 73 | await customTokenAuthentication.koaMiddleware(ctx, next) 74 | 75 | next.callCount.should.eql(1) 76 | loggerStub.callCount.should.eql(1) 77 | should(ctx.authenticated).be.undefined() 78 | }) 79 | 80 | it('should fail when no client matches the custom token', async () => { 81 | const ctx = { 82 | header: {}, 83 | request: { 84 | header: { 85 | authorization: 'Custom test1' 86 | } 87 | } 88 | } 89 | const next = sandbox.spy() 90 | 91 | const loggerStub = sandbox.stub(logger, 'error') 92 | 93 | const clientStub = sandbox 94 | .stub(client.ClientModel, 'findOne') 95 | .withArgs({customTokenID: 'test1'}) 96 | .resolves(null) 97 | 98 | await customTokenAuthentication.koaMiddleware(ctx, next) 99 | 100 | next.callCount.should.eql(1) 101 | clientStub.callCount.should.eql(1) 102 | loggerStub.callCount.should.eql(1) 103 | should(ctx.authenticated).be.undefined() 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /performance/mediator/tls/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | TTyLi4zsa/I4JapGFdJmS0xZ6gwNqS8= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/integration/urlRewriting.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | import nconf from 'nconf' 6 | import request from 'supertest' 7 | import {ObjectId} from 'mongodb' 8 | import {promisify} from 'util' 9 | 10 | import * as constants from '../constants' 11 | import * as server from '../../src/server' 12 | import * as testUtils from '../utils' 13 | import {ChannelModelAPI} from '../../src/model/channels' 14 | import {ClientModelAPI} from '../../src/model/clients' 15 | import {config} from '../../src/config' 16 | 17 | const {SERVER_PORTS} = constants 18 | describe('URL rewriting test', () => { 19 | const ORIGINAL_CONFIG_ROUTER = config.router 20 | config.authentication = config.get('authentication') 21 | config.tlsClientLookup = config.get('tlsClientLookup') 22 | config.router = config.get('router') 23 | let mockServer = null 24 | const jsonResponse = { 25 | href: `http://localhost:${constants.MEDIATOR_PORT}/test/mock` 26 | } 27 | 28 | before(async () => { 29 | const overrideConfig = Object.assign({}, config.router, { 30 | httpPort: SERVER_PORTS.httpPort 31 | }) 32 | nconf.set('router', overrideConfig) 33 | config.authentication.enableMutualTLSAuthentication = false 34 | config.authentication.enableBasicAuthentication = true 35 | 36 | // Setup some test data 37 | await new ChannelModelAPI({ 38 | name: 'TEST DATA - Mock endpoint', 39 | urlPattern: 'test/mock', 40 | allow: ['PoC'], 41 | methods: ['GET'], 42 | routes: [ 43 | { 44 | name: 'test route', 45 | host: 'localhost', 46 | port: constants.MEDIATOR_PORT, 47 | primary: true 48 | } 49 | ], 50 | rewriteUrls: true, 51 | updatedBy: { 52 | id: new ObjectId(), 53 | name: 'Test' 54 | } 55 | }).save() 56 | 57 | const testAppDoc = { 58 | clientID: 'testApp', 59 | clientDomain: 'test-client.jembi.org', 60 | name: 'TEST Client', 61 | roles: ['OpenMRS_PoC', 'PoC'], 62 | passwordAlgorithm: 'sha512', 63 | passwordHash: 64 | '28dce3506eca8bb3d9d5a9390135236e8746f15ca2d8c86b8d8e653da954e9e3632bf9d85484ee6e9b28a3ada30eec89add42012b185bd9a4a36a07ce08ce2ea', 65 | passwordSalt: '1234567890', 66 | cert: '' 67 | } 68 | 69 | await new ClientModelAPI(testAppDoc).save() 70 | 71 | // Create mock endpoint to forward requests to 72 | mockServer = await testUtils.createMockHttpServer( 73 | JSON.stringify(jsonResponse), 74 | constants.MEDIATOR_PORT, 75 | 201 76 | ) 77 | }) 78 | 79 | after(async () => { 80 | await Promise.all([ 81 | ChannelModelAPI.deleteOne({name: 'TEST DATA - Mock endpoint'}), 82 | ClientModelAPI.deleteOne({clientID: 'testApp'}), 83 | mockServer.close() 84 | ]) 85 | config.router = ORIGINAL_CONFIG_ROUTER 86 | }) 87 | 88 | afterEach(async () => { 89 | await promisify(server.stop)() 90 | }) 91 | 92 | it('should rewrite response urls', async () => { 93 | await promisify(server.start)({httpPort: SERVER_PORTS.httpPort}) 94 | 95 | const res = await request(constants.HTTP_BASE_URL) 96 | .get('/test/mock') 97 | .auth('testApp', 'password') 98 | .expect(201) 99 | 100 | const response = await JSON.parse(res.text) 101 | response.href.should.be.exactly(`${constants.HTTP_BASE_URL}/test/mock`) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /src/middleware/jwtAuthentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import jwt from 'jsonwebtoken' 4 | import logger from 'winston' 5 | 6 | import * as client from '../model/clients' 7 | import * as configIndex from '../config' 8 | import * as jwtSecretOrPublicKeyCache from '../jwtSecretOrPublicKeyCache' 9 | import * as jwksCache from '../jwksCache' 10 | import {JWT_PATTERN} from '../constants' 11 | import {promisify} from 'util' 12 | 13 | const verify = promisify(jwt.verify) 14 | 15 | async function authenticateClient(clientID) { 16 | return client.ClientModel.findOne({clientID}).then(client => { 17 | if (!client) { 18 | throw new Error('Client does not exist') 19 | } 20 | return client 21 | }) 22 | } 23 | 24 | function getJwtOptions() { 25 | const jwtConfig = configIndex.config.get('authentication:jwt') 26 | const jwtOptions = {} 27 | 28 | if (jwtConfig.algorithms) { 29 | // Algorithms can be input as an environment variable therefore the string needs to be split 30 | jwtOptions.algorithms = jwtConfig.algorithms.split(' ') 31 | } else { 32 | // The jsonwebtoken package does not require this field, but allowing any algorithm to be used opens a security risk 33 | throw new Error('JWT Algorithm not specified') 34 | } 35 | 36 | if (jwtConfig.audience) { 37 | // Audience can be input as an environment variable therefore the string needs to be split 38 | jwtOptions.audience = jwtConfig.audience.split(' ') 39 | } 40 | 41 | jwtOptions.issuer = jwtConfig.issuer 42 | 43 | return jwtOptions 44 | } 45 | 46 | async function authenticateToken(ctx) { 47 | if (ctx.authenticated) { 48 | return 49 | } 50 | 51 | const authHeader = ctx.request.header.authorization || '' 52 | const token = JWT_PATTERN.exec(authHeader) 53 | 54 | if (!token) { 55 | logger.debug(`Missing or invalid JWT 'Authorization' header`) 56 | return 57 | } 58 | 59 | try { 60 | let secretOrPublicKey = jwtSecretOrPublicKeyCache.getSecretOrPublicKey() 61 | 62 | if ( 63 | !secretOrPublicKey && 64 | configIndex.config.get('authentication:jwt:jwksUri') 65 | ) { 66 | // If the secretOrPublicKey is not set, then set this to a function that checks the JWKS cache 67 | logger.info('Using JWKS URI to verify JWT') 68 | secretOrPublicKey = async (header, callback) => { 69 | const key = await jwksCache.getKey(header.kid) 70 | if (!key) { 71 | return callback(new Error('Public key not found in JWKS cache')) 72 | } 73 | callback(null, key.publicKey || key.rsaPublicKey) 74 | } 75 | } 76 | 77 | const decodedToken = await verify( 78 | token[1], 79 | secretOrPublicKey, 80 | getJwtOptions() 81 | ) 82 | 83 | if (!decodedToken.sub) { 84 | logger.error('Invalid JWT Payload') 85 | return 86 | } 87 | 88 | const client = await authenticateClient(decodedToken.sub) 89 | logger.info(`Client (${client.name}) is Authenticated`) 90 | ctx.authenticated = client 91 | ctx.authenticationType = 'token' 92 | } catch (error) { 93 | logger.error(`JWT could not be verified: ${error.message}`) 94 | return 95 | } 96 | } 97 | 98 | export async function koaMiddleware(ctx, next) { 99 | await authenticateToken(ctx) 100 | if (ctx.authenticated && ctx.authenticated.clientID) { 101 | ctx.header['X-OpenHIM-ClientID'] = ctx.authenticated.clientID 102 | } 103 | await next() 104 | } 105 | --------------------------------------------------------------------------------