@brickert/kerio-connect-api 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+
2
+ # kerio-connect-api
3
+
4
+ An unofficial API wrapper in NodeJS for Kerio Connect mail server.
5
+
6
+ ## Notes
7
+
8
+ Tested on Kerio Connect Linux (using Debian 12 `.deb` file install) version `10.0.8 Patch 2`. You can download it here: [http://download.kerio.com/archive/](http://download.kerio.com/archive/)
9
+
10
+ Under the hood it uses jsonrpc protocol version 2. With session cookies `SESSION_CONNECT_WEBADMIN` and an `X-Token` Header for CSRF protection, and a `json` body
11
+
12
+ The official documentation can be viewed here: [https://manuals.gfi.com/en/kerio/api/connect/admin/reference/index.html](https://manuals.gfi.com/en/kerio/api/connect/admin/reference/index.html)
13
+
14
+ Although the documentation looks hard to read I did find an official [PHP library example](https://cdn.kerio.com/dwn/kerio-api-php.zip) that helped greatly.
15
+
16
+ ## Features
17
+
18
+ Currently only implemented a very small set of methods for the Administrative API under endpoint `/admin/api/jsonrpc`
19
+
20
+ When importing this library a log file is created relative to the where the process is started. E.g. `node /opt/kerio/app.js` will have a `json` log file at `/opt/kerio/log/kerio.log` Generated by the [pino](https://www.npmjs.com/package/pino) package. With also `pino-pretty` printed to `stdout`
21
+
22
+ ## Installation
23
+
24
+ Use the package manager [npm](https://npmjs.org) to install.
25
+
26
+ ```bash
27
+ npm install @brickert/kerio-connect-api
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ A typical quick one-off script can be such as:
33
+
34
+ ```javascript
35
+ import { Kerio } from "@brickert/kerio-connect-api";
36
+
37
+ var kerio = new Kerio({
38
+ hostname: "192.168.1.1",
39
+ port: 4040,
40
+ endpoint_url: '/admin/api/jsonrpc',
41
+ https: true
42
+ }, {
43
+ name: "kerio-connect-api",
44
+ vendor: "Example Inc",
45
+ version: "1.0"
46
+ }, {
47
+ logLevel: 'info'
48
+ });
49
+
50
+ try {
51
+ await kerio.login({
52
+ username: "operator",
53
+ password: "foobar"
54
+ });
55
+
56
+ let add_host_id = await kerio.IPAddressGroups.addHostToIPAddressGroup("192.168.1.2", "Example Client Name", "My Group Name", true);
57
+
58
+ console.log(add_host_id);
59
+ // keriodb://ipaddressgroup/1234-5678-901-2345
60
+
61
+ } catch (e) {
62
+ console.log(e);
63
+ }
64
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,246 @@
1
+ import pino from "pino";
2
+
3
+ export class WebClient {
4
+ constructor(config: WebClientConfig);
5
+
6
+ hostname: string;
7
+ port: number;
8
+ endpoint_url: string;
9
+ https: boolean;
10
+ fullURL: string;
11
+
12
+ /**
13
+ * Executes a request to the WebClient.fullURL with body object
14
+ * @param requestBody
15
+ * @param method
16
+ * @param follow_redirects
17
+ */
18
+ doRequest(
19
+ requestBody: GenericJsonRPCBodyPayload,
20
+ method?: HTTPMethod,
21
+ follow_redirects?: RequestRedirect
22
+ ): Promise<GenericResponse>;
23
+
24
+ }
25
+
26
+ export interface WebClientConfig {
27
+ /** FQDN or IP address of the Kerio Connect Instance */
28
+ hostname: string;
29
+
30
+ /** Port number of the Kerio Connect Instance's API */
31
+ port?: number;
32
+
33
+ /** URL endpoint of the Kerio Connect Instance's Admin API
34
+ * @example '/admin/api/jsonrpc'
35
+ */
36
+ endpoint_url?: string;
37
+
38
+ /** The WebClient instance to use https or http */
39
+ https?: boolean;
40
+ }
41
+
42
+ export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'HEAD';
43
+
44
+ export interface GenericJsonRPCBodyPayload {
45
+ jsonrpc: string;
46
+ id: number;
47
+ method: string;
48
+ params?: {
49
+ userName?: string;
50
+ password?: string;
51
+ application?: KerioUserAgent;
52
+ groups?: Array<object>;
53
+ query?: object;
54
+ groupIds?: Array<string>;
55
+ }
56
+ }
57
+
58
+ export interface GenericResponse {
59
+ _headers: object;
60
+ _body: {
61
+ jsonrpc: string;
62
+ id: number;
63
+ error?: {
64
+ code: number;
65
+ message: string;
66
+ data: object;
67
+ }
68
+ results?: {
69
+ errors?: {
70
+ code: number;
71
+ message: string;
72
+ }
73
+ }
74
+ result?: {
75
+ userDetails?: object;
76
+ list?: Array<object> | Array<IPAddressGroupItem>;
77
+ totalItems?: number;
78
+ errors?: Array<{
79
+ code: number,
80
+ message: string
81
+ }>;
82
+ result?: Array;
83
+ token?: string;
84
+ }
85
+ };
86
+ }
87
+
88
+ export class Kerio extends WebClient {
89
+ constructor(config: WebClientConfig, userAgent: KerioUserAgent, logConfig?: KerioLogConfig);
90
+
91
+ hostname: string;
92
+ port: number;
93
+ endpoint_url: string;
94
+ https: boolean;
95
+ fullURL: string;
96
+
97
+ userAgent: KerioUserAgent;
98
+ session_cookie: string;
99
+ x_token: string;
100
+ logged_in: boolean;
101
+ heartbeat: object;
102
+
103
+ logger: pino.Logger;
104
+
105
+ IPAddressGroups: IPAddressGroup;
106
+ Session: Session;
107
+
108
+ login(user: KerioUser): void;
109
+
110
+ _createHeartBeatCronJob(): CronJob<null, null>;
111
+
112
+ renewSessionHeartBeat(): Promise<boolean>;
113
+ }
114
+
115
+ export interface KerioLogConfig {
116
+ logLevel: string;
117
+ }
118
+
119
+ /**
120
+ * Mainly to be shown in Kerio Connect's logs
121
+ */
122
+ export interface KerioUserAgent {
123
+ /**
124
+ * The name for your API Connector
125
+ * @example 'KerioConnector'
126
+ */
127
+ name: string;
128
+
129
+ /**
130
+ * The name for your API Connector Author or Company
131
+ * @example 'Example Inc'
132
+ */
133
+ vendor: string;
134
+ /**
135
+ * The version of your API Connector
136
+ * @example '1.0'
137
+ */
138
+ version: string;
139
+ }
140
+
141
+ export interface KerioUser {
142
+ /**
143
+ * The username of the built-in user that has access to the Admin GUI
144
+ */
145
+ username: string;
146
+ /**
147
+ * The password of the built-in user that has access to the Admin GUI
148
+ */
149
+ password: string;
150
+ }
151
+
152
+ export class KerioModules {
153
+ sessionedRequest(options: SessionedRequestOptions): GenericResponse;
154
+ reset(): void;
155
+ }
156
+
157
+ export interface SessionedRequestOptions {
158
+ http_method: HTTPMethod;
159
+ api_method: string;
160
+ auth: SessionKeys;
161
+ body_params: object;
162
+ }
163
+
164
+ export interface SessionKeys {
165
+ cookie: string;
166
+ token: string;
167
+ }
168
+
169
+ export class IPAddressGroup extends KerioModules {
170
+ /**
171
+ * Obtain a Mapped Array of IPAddressGroupItem indexed by their ID for the specified group name
172
+ * @param group_name Name of the IPAddressGroup
173
+ */
174
+ listIPAddressItemsByGroupName(group_name: string): Promise<Map<KerioIPAddressID, IPAddressGroupItem>>;
175
+
176
+ /**
177
+ * Add a IPAddressGroupItem of type 'host' of a single IP Address, with a specified description name, within a specified group name, and whether to have it enabled.
178
+ * The IPAddressGroup will also be created automatically if it doesn't exist yet
179
+ * @param host_ip The single IP Address string
180
+ * @param description Name of this IPAddressGroupItem string
181
+ * @param group_name Name of the IPAddressGroup string to add this host in
182
+ * @param enabled True to enable this host in the group
183
+ * @returns Provides the unique KerioID of this host entry
184
+ */
185
+ addHostToIPAddressGroup(host_ip: string, description: string, group_name: string, enabled: boolean): Promise<KerioIPAddressID>;
186
+
187
+ /**
188
+ * Remove this IPAddressGroupItem of type 'host' of a single IP Address from this group name
189
+ * @param host_ip The single IP Address string
190
+ * @param group_name Name of the IPAddressGroup string to remove this host in
191
+ * @returns Returns True when removal was successful
192
+ */
193
+ removeHostFromIPAddressGroupByIP(host_ip: string, group_name: string): Promise<boolean>;
194
+
195
+ /**
196
+ * Remove IPAddressGroupItem(s) of type 'host' by the description name from this group name. TAKE CARE as this will remove all items upto the specified max limit that share the same name. You may limit number of items to remove. They are sorted by their unique ID.
197
+ * @param description Name of the group item(s)
198
+ * @param group_name Name of the IPAddressGroup string to remove from
199
+ * @param remove_limit Max number of group items to remove upto. Specifying -1 will remove ALL by the description name
200
+ * @returns Returns an array of host IP addresses that were removed
201
+ */
202
+ removeHostFromIPAddressGroupByDesc(description: string, group_name: string, remove_limit: number): Promise<Array<KerioIPAddressHost>>;
203
+
204
+ }
205
+
206
+ export type KerioIPAddressHost = string;
207
+
208
+ export interface IPAddressGroupItem {
209
+ id: KerioIPAddressID;
210
+ groupId: KerioIPAddressGroupID;
211
+ sharedId?: string;
212
+ appManagerId?: string;
213
+ groupName: KerioIPAddressGroupName;
214
+ description: string;
215
+ type: IPAddressGroupItemType;
216
+ enabled: boolean;
217
+ host?: string;
218
+ addr1?: string;
219
+ addr2?: string | number;
220
+ childGroupId?: string;
221
+ childGroupName?: string;
222
+ }
223
+
224
+ export type IPAddressGroupItemType = 'Host' | 'Network' | 'Range' | 'ChildGroup';
225
+ /**
226
+ * @example "keriodb://ipaddressgroup/1234-5678-901-2345"
227
+ */
228
+ export type KerioIPAddressGroupID = string;
229
+ /**
230
+ * @example "keriodb://ipaddress/1234-5678-901-2345"
231
+ */
232
+ export type KerioIPAddressID = string;
233
+ export type KerioIPAddressGroupName = string;
234
+
235
+ export class Session extends KerioModules {
236
+ whoAmI(): Promise<KerioInstanceUserResponse>;
237
+ }
238
+
239
+ export interface KerioInstanceUserResponse {
240
+ id: string;
241
+ username: string;
242
+ name: string;
243
+ roles: {
244
+ userRole: string;
245
+ }
246
+ }
package/jsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "target": "ESNext",
5
+ "lib": ["ESNext", "DOM"],
6
+ "checkJs": true,
7
+ "moduleResolution": "node"
8
+ },
9
+ "include": ["src/**/*", "index.d.ts"]
10
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@brickert/kerio-connect-api",
3
+ "version": "0.0.1",
4
+ "description": "An unofficial API wrapper for Kerio Connect Mail Server jsonrpc Admin API.",
5
+ "keywords": [
6
+ "kerio",
7
+ "connect",
8
+ "mail",
9
+ "server",
10
+ "email",
11
+ "wrapper",
12
+ "smtp",
13
+ "imap"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://gitlab.com/beckrickert/kerio-connect-api.git"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "license": "ISC",
23
+ "author": "Beck Rickert",
24
+ "type": "module",
25
+ "main": "src/kerio.js",
26
+ "types": "index.d.ts",
27
+ "directories": {
28
+ "src": "src"
29
+ },
30
+ "dependencies": {
31
+ "cron": "^4.4.0",
32
+ "fastify": "^5.6.2",
33
+ "lodash": "^4.17.21",
34
+ "node-fetch": "^3.3.2",
35
+ "pino": "^10.2.0",
36
+ "pino-pretty": "^13.1.3",
37
+ "qs": "^6.14.1",
38
+ "set-cookie-parser": "^2.7.2"
39
+ }
40
+ }
package/src/client.js ADDED
@@ -0,0 +1,97 @@
1
+ import fetch from 'node-fetch';
2
+ import https from 'node:https';
3
+ import setCookie from 'set-cookie-parser';
4
+
5
+ /**
6
+ * @type {import('../index.d.ts').WebClient}
7
+ */
8
+ export class WebClient {
9
+
10
+ /**
11
+ * @param {import('../index.d.ts').WebClientConfig} config
12
+ */
13
+ constructor(config) {
14
+ this.hostname = config.hostname;
15
+ this.port = config.port ?? 4040;
16
+ this.endpoint_url = config.endpoint_url ?? "/admin/api/jsonrpc";
17
+ this.https = config.https ?? true;
18
+
19
+ this.fullURL = this.#getWebClientURL();
20
+ }
21
+
22
+
23
+ #GLOBAL_REQUEST_TIMEOUT = 5000;
24
+
25
+ /**
26
+ * Obtain the full URL string from WebClientConfig
27
+ * @returns {string}
28
+ */
29
+ #getWebClientURL() {
30
+
31
+ let baseURL;
32
+
33
+ if (this.https) {
34
+ baseURL = `https://${this.hostname}:${this.port}`;
35
+ } else {
36
+ baseURL = `http://${this.hostname}:${this.port}`;
37
+ }
38
+
39
+ return new URL(this.endpoint_url, baseURL).toString();
40
+ }
41
+
42
+ /**
43
+ * Executes a request to the WebClient.fullURL with body of GenericJsonRPCBodyPayload
44
+ * @param {import('../index.d.ts').GenericJsonRPCBodyPayload} requestBody
45
+ * @param {import('../index.d.ts').HTTPMethod} method
46
+ * @param {import('node-fetch').RequestRedirect} follow_redirects
47
+ * @returns {Promise<import('../index.d.ts').GenericResponse>}
48
+ */
49
+ async doRequest(requestBody = null, method = "GET", follow_redirects = "follow") {
50
+ try {
51
+
52
+ let fetchOptions = {
53
+ method: method,
54
+ headers: new Object(),
55
+ redirect: follow_redirects,
56
+ signal: AbortSignal.timeout(this.#GLOBAL_REQUEST_TIMEOUT)
57
+ }
58
+
59
+ if (typeof requestBody == 'object') {
60
+ fetchOptions.body = JSON.stringify(requestBody);
61
+ fetchOptions.headers['Content-Type'] = 'application/json';
62
+ } else {
63
+ throw "Expected Request Body to be an object";
64
+ }
65
+
66
+ if (this.https) {
67
+ fetchOptions.agent = new https.Agent({
68
+ rejectUnauthorized: false,
69
+ requestCert: true,
70
+ keepAlive: true
71
+ });
72
+ }
73
+
74
+ // @ts-ignore
75
+ let response = await fetch(this.fullURL, fetchOptions);
76
+
77
+ if (response.status < 400) {
78
+
79
+ /**
80
+ * @type {object}
81
+ */
82
+ let response_data = await response.json();
83
+
84
+ return {
85
+ _headers: response.headers.raw(),
86
+ _body: response_data
87
+ }
88
+
89
+ } else {
90
+ throw "Unexpected HTTP response";
91
+ }
92
+
93
+ } catch (e) {
94
+ throw e;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,489 @@
1
+ import { KerioModules } from './modules.js';
2
+ import net from 'node:net';
3
+ /**
4
+ * @type {import('../index.d.ts').IPAddressGroup}
5
+ */
6
+ export class IPAddressGroup extends KerioModules {
7
+
8
+ /**
9
+ * Gets a Map Set of IPAddressGroupItem indexed by their UID
10
+ * @param {string} group_name Name of the IPAddressGroup these items are a member of
11
+ * @returns {Promise<Map<import('../index.d.ts').KerioIPAddressID, import('../index.d.ts').IPAddressGroupItem>>}
12
+ */
13
+ async listIPAddressItemsByGroupName(group_name = null) {
14
+ try {
15
+
16
+ if (!this.instance.logged_in) {
17
+
18
+ this.reset();
19
+
20
+ throw {
21
+ name: "KerioClientSessionError",
22
+ message: `Kerio session invalid. Try logging in again.`,
23
+ type: 'Kerio',
24
+ from: "Kerio.IPAddressGroups.listIPAddressItemsByGroupName"
25
+ }
26
+ }
27
+
28
+
29
+ if (!group_name) {
30
+ throw {
31
+ name: "KerioInvalidIPAddressGroupNameError",
32
+ message: `Invalid referred Group Name while processing API method 'IPAddressGroups.get' for name '${group_name}'`,
33
+ type: 'Kerio',
34
+ from: "Kerio.IPAddressGroups.listIPAddressItemsByGroupName"
35
+ }
36
+ }
37
+
38
+
39
+ let ip_address_items_response = await this.sessionedRequest({
40
+ http_method: 'POST',
41
+ api_method: 'IpAddressGroups.get',
42
+ auth: {
43
+ cookie: this.instance.session_cookie,
44
+ token: this.instance.x_token
45
+ },
46
+ body_params: {
47
+ query: {
48
+ conditions: [
49
+ {
50
+ fieldName: 'name',
51
+ comparator: "Eq",
52
+ value: group_name
53
+ }
54
+ ]
55
+ }
56
+ }
57
+ });
58
+
59
+ let response_body = ip_address_items_response._body;
60
+
61
+ if (!response_body.result?.errors && !response_body?.error) {
62
+ /**
63
+ * @type {import('../index.d.ts').IPAddressGroupItem[]}
64
+ */
65
+ // @ts-ignore
66
+ let ipAddressItems = response_body.result.list;
67
+
68
+ if (ipAddressItems.length != 0 || response_body.result.totalItems > 0) {
69
+ //clean up properties who have empty strings and values
70
+ ipAddressItems.forEach(ipItem => {
71
+ return Object.fromEntries(Object.entries(ipItem).filter(([param, value]) => {
72
+ if (value != null && value !== "") {
73
+ return true;
74
+ }
75
+ }));
76
+ });
77
+
78
+ let ipAddresslist = new Map();
79
+
80
+ ipAddressItems.forEach(item => {
81
+ ipAddresslist.set(item.id, item);
82
+ });
83
+
84
+ this.instance.logger.debug({
85
+ name: "KerioListIPAddressItems",
86
+ message: `listed ${ipAddresslist.size} items in IPAddressGroup '${group_name}'`,
87
+ type: 'Kerio',
88
+ from: "Kerio.IPAddressGroups.listIPAddressItemsByGroupName"
89
+ });
90
+
91
+ return ipAddresslist;
92
+ } else {
93
+ return new Map();
94
+ }
95
+
96
+ } else {
97
+ throw {
98
+ name: "KerioRequestError",
99
+ message: `Error occured while fetching results from API method 'IPAddressGroups.get' by name of '${group_name}'`,
100
+ type: 'Kerio',
101
+ from: "Kerio.IPAddressGroups.listIPAddressItemsByGroupName"
102
+ }
103
+ }
104
+
105
+ } catch (e) {
106
+ this.instance.logger.error(e);
107
+ throw e;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Add an IPAddressEntry whose a member under the specific IPAddressGroup name and returns the new KerioIPAddressID. This will also create the IPAddressGroup at root level (no parent) if it does not exist.
113
+ * @param {string} host_ip Host's IPv4/IPv6 to add
114
+ * @param {string} description Descriptive name for this host
115
+ * @param {string} group_name IPAddressGroup name this host is a member of
116
+ * @param {boolean} enabled Enabled or disabled for this entry, defaults true
117
+ * @returns {Promise<import('../index.d.ts').KerioIPAddressID>}
118
+ */
119
+ async addHostToIPAddressGroup(host_ip, description = null, group_name = null, enabled = true) {
120
+ try {
121
+
122
+ if (!this.instance.logged_in) {
123
+
124
+ this.reset();
125
+
126
+ throw {
127
+ name: "KerioClientSessionError",
128
+ message: `Kerio session invalid. Try logging in again.`,
129
+ type: 'Kerio',
130
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
131
+ }
132
+ }
133
+
134
+ if (net.isIP(host_ip) == 0) {
135
+ throw {
136
+ name: "KerioInvalidHostError",
137
+ message: `Invalid Host IP while processing API method 'IPAddressGroups.create' for host '${host_ip}' in group '${group_name}'`,
138
+ type: 'Kerio',
139
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
140
+ }
141
+ }
142
+
143
+ if (!description) {
144
+ throw {
145
+ name: "KerioInvalidHostDescriptionError",
146
+ message: `Invalid Description for IPAddressEntry while processing API method 'IPAddressGroups.create' for host '${host_ip}' in group '${group_name}'`,
147
+ type: 'Kerio',
148
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
149
+ }
150
+ }
151
+
152
+ if (!group_name) {
153
+ throw {
154
+ name: "KerioInvalidIPAddressGroupNameError",
155
+ message: `Invalid referred Group Name while processing API method 'IPAddressGroups.create' for host '${host_ip}' in group '${group_name}'`,
156
+ type: 'Kerio',
157
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
158
+ }
159
+ }
160
+
161
+ if (typeof enabled !== 'boolean') {
162
+ throw {
163
+ name: "KerioInvalidIPAddressEntryEnableError",
164
+ message: `Invalid Enabled value while processing API method 'IPAddressGroups.create' for host '${host_ip}' in group '${group_name}'`,
165
+ type: 'Kerio',
166
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
167
+ }
168
+ }
169
+
170
+ let add_host_response = await this.sessionedRequest({
171
+ http_method: 'POST',
172
+ api_method: 'IpAddressGroups.create',
173
+ auth: {
174
+ cookie: this.instance.session_cookie,
175
+ token: this.instance.x_token
176
+ },
177
+ body_params: {
178
+ groups: [
179
+ {
180
+ groupName: group_name,
181
+ host: host_ip,
182
+ type: "Host",
183
+ description: description,
184
+ enabled: enabled
185
+ }
186
+ ]
187
+ }
188
+ });
189
+
190
+ let response_body = add_host_response._body;
191
+
192
+ if (response_body.result?.errors.length == 0) {
193
+
194
+ this.instance.logger.info({
195
+ name: "KerioAddHostIPAddressGroup",
196
+ message: `Added host ${host_ip} with description ${description} in IPAddressGroup '${group_name}'`,
197
+ type: 'Kerio',
198
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
199
+ });
200
+
201
+ return response_body.result.result[0].id;
202
+
203
+ } else {
204
+ switch (response_body.result.errors[0].code) {
205
+ case 1001:
206
+ throw {
207
+ name: "KerioDuplicateError",
208
+ message: `Duplicate Host IP while processing API method 'IPAddressGroups.create' for host '${host_ip}' in group '${group_name}'`,
209
+ type: 'Kerio',
210
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
211
+ }
212
+ case 1003:
213
+ throw {
214
+ name: "KerioInvalidHostError",
215
+ message: `Invalid Host IP while processing API method 'IPAddressGroups.create' for host '${host_ip}' in group '${group_name}'`,
216
+ type: 'Kerio',
217
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
218
+ }
219
+ default:
220
+ throw {
221
+ name: "KerioRequestError",
222
+ message: `Error occured while fetching results from API method 'IPAddressGroups.create' for host '${host_ip}' in group '${group_name}'`,
223
+ type: 'Kerio',
224
+ from: "Kerio.IPAddressGroups.addHostToIPAddressGroup"
225
+ }
226
+ }
227
+ }
228
+
229
+ } catch (e) {
230
+ this.instance.logger.error(e);
231
+ throw e;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Remove an IPAddressEntry from the specific IPAddressGroup name. Returns true when successfully removed.
237
+ * @param {string} host_ip Host's IPv4/IPv6 to add
238
+ * @param {string} group_name IPAddressGroup name this host is a member of
239
+ * @returns {Promise<boolean>} Returns true when removal was successful
240
+ */
241
+ async removeHostFromIPAddressGroupByIP(host_ip, group_name = null) {
242
+ try {
243
+
244
+ if (!this.instance.logged_in) {
245
+
246
+ this.reset();
247
+
248
+ throw {
249
+ name: "KerioClientSessionError",
250
+ message: `Kerio session invalid. Try logging in again.`,
251
+ type: 'Kerio',
252
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
253
+ }
254
+ }
255
+
256
+ if (net.isIP(host_ip) == 0) {
257
+ throw {
258
+ name: "KerioInvalidHostError",
259
+ message: `Invalid Host IP while processing API method 'IPAddressGroups.remove' for host '${host_ip}' in group '${group_name}'`,
260
+ type: 'Kerio',
261
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
262
+ }
263
+ }
264
+
265
+ if (!group_name) {
266
+ throw {
267
+ name: "KerioInvalidIPAddressGroupNameError",
268
+ message: `Invalid referred Group Name while processing API method 'IPAddressGroups.remove' for host '${host_ip}' in group '${group_name}'`,
269
+ type: 'Kerio',
270
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
271
+ }
272
+ }
273
+
274
+ let get_host_ID_response = await this.listIPAddressItemsByGroupName(group_name);
275
+
276
+ if (get_host_ID_response.size != 0) {
277
+
278
+ var foundHostID = "";
279
+
280
+ for (const [hostID, entryproperties] of get_host_ID_response) {
281
+ if (entryproperties.host == host_ip) {
282
+ foundHostID = hostID;
283
+ break;
284
+ }
285
+ }
286
+
287
+ if (foundHostID.length > 0) {
288
+ let remove_host_response = await this.sessionedRequest({
289
+ http_method: 'POST',
290
+ api_method: 'IpAddressGroups.remove',
291
+ auth: {
292
+ cookie: this.instance.session_cookie,
293
+ token: this.instance.x_token
294
+ },
295
+ body_params: {
296
+ groupIds: [foundHostID]
297
+ }
298
+ });
299
+
300
+ let response_body = remove_host_response._body;
301
+
302
+ if (Array.isArray(response_body.result.errors) && response_body.result.errors.length == 0) {
303
+
304
+ this.instance.logger.info({
305
+ name: "KerioRemoveHostIPAddressGroup",
306
+ message: `Removed host '${host_ip}' in group '${group_name}'`,
307
+ type: 'Kerio',
308
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
309
+ });
310
+
311
+ return true;
312
+
313
+ } else {
314
+ switch (response_body.result.errors[0].code) {
315
+ case 1002:
316
+ throw {
317
+ name: "KerioHostRemoveError",
318
+ message: `Host does not exist within group while fetching results from API method 'IPAddressGroups.remove' for host '${host_ip}' in group '${group_name}'`,
319
+ type: 'Kerio',
320
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
321
+ }
322
+ case 1003:
323
+ throw {
324
+ name: "KerioHostRemoveError",
325
+ message: `Host ID invalid while fetching results from API method 'IPAddressGroups.remove' for host '${host_ip}' in group '${group_name}'`,
326
+ type: 'Kerio',
327
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
328
+ }
329
+ default:
330
+ throw {
331
+ name: "KerioRequestError",
332
+ message: `Error occured while fetching results from API method 'IPAddressGroups.remove' for host '${host_ip}' in group '${group_name}'`,
333
+ type: 'Kerio',
334
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
335
+ }
336
+ }
337
+ }
338
+ } else {
339
+ throw {
340
+ name: "KerioHostNotExistError",
341
+ message: `Host IP does not exist in group while processing API method 'IPAddressGroups.remove' for host '${host_ip}' in group '${group_name}'`,
342
+ type: 'Kerio',
343
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
344
+ }
345
+ }
346
+ } else {
347
+ throw {
348
+ name: "KerioHostNotExistGroupError",
349
+ message: `Group has no hosts while processing API method 'IPAddressGroups.remove' for host '${host_ip}' in group '${group_name}'`,
350
+ type: 'Kerio',
351
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByIP"
352
+ }
353
+ }
354
+
355
+ } catch (e) {
356
+ this.instance.logger.error(e);
357
+ throw e;
358
+ }
359
+ }
360
+
361
+ async removeHostFromIPAddressGroupByDesc(description = null, group_name = null, remove_limit = 1) {
362
+ try {
363
+
364
+ if (!this.instance.logged_in) {
365
+
366
+ this.reset();
367
+
368
+ throw {
369
+ name: "KerioClientSessionError",
370
+ message: `Kerio session invalid. Try logging in again.`,
371
+ type: 'Kerio',
372
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
373
+ }
374
+ }
375
+
376
+ if (remove_limit < -1 || !Number.isInteger(remove_limit)) {
377
+ throw {
378
+ name: "KerioInvalidGroupEntryRemoveLimitError",
379
+ message: `Invalid number of max host entries to remove while processing API method 'IPAddressGroups.remove' for description '${description}' in group '${group_name}'`,
380
+ type: 'Kerio',
381
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
382
+ }
383
+ }
384
+
385
+ if (!description) {
386
+ throw {
387
+ name: "KerioInvalidGroupEntryDescriptionError",
388
+ message: `Invalid Description to search query while processing API method 'IPAddressGroups.remove' for description '${description}' in group '${group_name}'`,
389
+ type: 'Kerio',
390
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
391
+ }
392
+ }
393
+
394
+ if (!group_name) {
395
+ throw {
396
+ name: "KerioInvalidIPAddressGroupNameError",
397
+ message: `Invalid referred Group Name while processing API method 'IPAddressGroups.remove' for description '${description}' in group '${group_name}'`,
398
+ type: 'Kerio',
399
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
400
+ }
401
+ }
402
+
403
+ let get_host_ID_response = await this.listIPAddressItemsByGroupName(group_name);
404
+
405
+ if (get_host_ID_response.size != 0) {
406
+
407
+ var foundHostIds = new Array();
408
+ var foundHostIPs = new Array();
409
+
410
+ for (const [hostID, entryproperties] of get_host_ID_response) {
411
+ if (entryproperties.description == description) {
412
+ foundHostIds.push(hostID);
413
+ foundHostIPs.push(entryproperties.host);
414
+ }
415
+ }
416
+
417
+ if (remove_limit > 0) {
418
+ //only the beginning of the array is kept
419
+ foundHostIPs = foundHostIPs.slice(0, remove_limit);
420
+ foundHostIds = foundHostIds.slice(0, remove_limit);
421
+ }
422
+
423
+ if (foundHostIds.length > 0) {
424
+ let remove_host_response = await this.sessionedRequest({
425
+ http_method: 'POST',
426
+ api_method: 'IpAddressGroups.remove',
427
+ auth: {
428
+ cookie: this.instance.session_cookie,
429
+ token: this.instance.x_token
430
+ },
431
+ body_params: {
432
+ groupIds: foundHostIds
433
+ }
434
+ });
435
+
436
+ let response_body = remove_host_response._body;
437
+
438
+ if (Array.isArray(response_body.result.errors) && response_body.result.errors.length == 0) {
439
+
440
+ return foundHostIPs;
441
+
442
+ } else {
443
+ switch (response_body.result.errors[0].code) {
444
+ case 1002:
445
+ throw {
446
+ name: "KerioHostRemoveError",
447
+ message: `Host does not exist within group while fetching results from API method 'IPAddressGroups.remove' in group '${group_name}'`,
448
+ type: 'Kerio',
449
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
450
+ }
451
+ case 1003:
452
+ throw {
453
+ name: "KerioHostRemoveError",
454
+ message: `Host ID invalid while fetching results from API method 'IPAddressGroups.remove' in group '${group_name}'`,
455
+ type: 'Kerio',
456
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
457
+ }
458
+ default:
459
+ throw {
460
+ name: "KerioRequestError",
461
+ message: `Error occured while fetching results from API method 'IPAddressGroups.remove' in group '${group_name}'`,
462
+ type: 'Kerio',
463
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
464
+ }
465
+ }
466
+ }
467
+ } else {
468
+ throw {
469
+ name: "KerioDescriptionHostsNotExistError",
470
+ message: `Host ID(s) from specified description does not exist in group while processing API method 'IPAddressGroups.remove' for description '${description}' in group '${group_name}'`,
471
+ type: 'Kerio',
472
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
473
+ }
474
+ }
475
+ } else {
476
+ throw {
477
+ name: "KerioHostNotExistGroupError",
478
+ message: `Group has no hosts with the specified description while processing API method 'IPAddressGroups.remove' for description '${description}' in group '${group_name}'`,
479
+ type: 'Kerio',
480
+ from: "Kerio.IPAddressGroups.removeHostFromIPAddressGroupByDesc"
481
+ }
482
+ }
483
+
484
+ } catch (e) {
485
+ this.instance.logger.error(e);
486
+ throw e;
487
+ }
488
+ }
489
+ }
package/src/kerio.js ADDED
@@ -0,0 +1,260 @@
1
+ import { IPAddressGroup } from "./ipaddressgroups.js";
2
+ import { Session } from "./session.js";
3
+ import { WebClient } from "./client.js";
4
+ import { CronJob } from 'cron';
5
+ import pino from 'pino';
6
+ import path from 'node:path';
7
+
8
+ //https://manuals.gfi.com/en/kerio/api/connect/admin/reference/index.html
9
+
10
+ export class Kerio extends WebClient {
11
+
12
+ /**
13
+ * @param {import('../index.d.ts').WebClientConfig} config
14
+ * @param {import('../index.d.ts').KerioUserAgent} userAgent
15
+ * @param {import('../index.d.ts').KerioLogConfig} logConfig
16
+ */
17
+ constructor(config, userAgent, logConfig) {
18
+ super(config);
19
+
20
+ this.userAgent = userAgent;
21
+
22
+ this.IPAddressGroups = new IPAddressGroup(this);
23
+
24
+ this.Session = new Session(this);
25
+
26
+ this.logger = pino({
27
+ level: "debug",
28
+ transport: {
29
+ targets: [
30
+ {
31
+ //Pretty logging to stdout
32
+ target: 'pino-pretty',
33
+ level: "info",
34
+ options: {
35
+ destination: 1
36
+ }
37
+ },
38
+ {
39
+ target: 'pino/file',
40
+ level: logConfig.logLevel ?? "debug",
41
+ options: {
42
+ destination: path.join(process.cwd(), 'log', 'kerio.log'),
43
+ mkdir: true
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Defined once successfully logged in via Kerio.login(user: KerioUser).
53
+ * Contains the name=value cookie to be passed in subsequent requests in the Cookie header.
54
+ * @type {string}
55
+ */
56
+ session_cookie = null;
57
+
58
+ /**
59
+ * Defined once successfully logged in via Kerio.login(user: KerioUser). Used in subsequent requests in the X-Token header.
60
+ * @type {string}
61
+ */
62
+ x_token = null;
63
+
64
+ /**
65
+ * If successfully logged in via Kerio.login(user: KerioUser). This becomes true.
66
+ * @type {boolean}
67
+ */
68
+ logged_in = false;
69
+
70
+ /**
71
+ * The heartbeat cron job to retain the current logged in session on an interval.
72
+ * @type {object}
73
+ */
74
+ heartbeat = new Object();
75
+
76
+ /**
77
+ * @type {pino.Logger}
78
+ */
79
+ logger;
80
+
81
+ /**
82
+ * Attempts to login with the specified Kerio user
83
+ * @param {import('../index.d.ts').KerioUser} user
84
+ */
85
+ async login(user) {
86
+ try {
87
+ let login_response = await this.doRequest({
88
+ jsonrpc: "2.0",
89
+ id: 1,
90
+ method: "Session.login",
91
+ params: {
92
+ userName: user.username,
93
+ password: user.password,
94
+ application: this.userAgent
95
+ }
96
+ }, "POST", "manual");
97
+
98
+ let response_body = login_response._body;
99
+
100
+ if (!response_body.result?.errors && !response_body?.error) {
101
+
102
+ this.x_token = response_body.result.token;
103
+
104
+ let cookies = login_response._headers["set-cookie"];
105
+ let raw_session_connect_webadmin_cookie = cookies.find(cookie_row => cookie_row.includes("SESSION_CONNECT_WEBADMIN"));
106
+ this.session_cookie = raw_session_connect_webadmin_cookie.split(";")[0];
107
+
108
+ this.logged_in = true;
109
+
110
+ this.logger.info({
111
+ name: "KerioLogin",
112
+ message: `Logged into Kerio successfully`,
113
+ type: "Kerio",
114
+ from: "Kerio.login"
115
+ });
116
+
117
+ this.logger.debug({
118
+ name: "KerioSession",
119
+ message: `Obtained valid cookies and tokens for current session`,
120
+ type: "Kerio",
121
+ from: "Kerio.login"
122
+ });
123
+
124
+ this.heartbeat = this._createHeartBeatCronJob();
125
+
126
+ this.logger.debug({
127
+ name: "KerioHeartBeatTask",
128
+ message: `Created session heartbeat cron task to renew session on an interval`,
129
+ type: "Kerio",
130
+ from: "Kerio.login"
131
+ });
132
+
133
+ this.heartbeat.start();
134
+
135
+ this.logger.debug({
136
+ name: "KerioHeartBeat",
137
+ message: `Started session heartbeat cron task`,
138
+ type: "Kerio",
139
+ from: "Kerio.login"
140
+ });
141
+
142
+ return;
143
+
144
+ } else {
145
+ if (response_body.error.code == 1000) {
146
+
147
+ throw {
148
+ name: "KerioLoginError",
149
+ message: `Kerio username or password is incorrect`,
150
+ type: 'Kerio',
151
+ from: "Kerio.login"
152
+ }
153
+ } else {
154
+
155
+ throw {
156
+ name: "KerioRequestError",
157
+ message: `Error occured while attempting login to Kerio`,
158
+ type: 'Kerio',
159
+ from: "Kerio.login"
160
+ }
161
+ }
162
+ }
163
+
164
+ } catch (e) {
165
+ this.logged_in = false;
166
+ this.session_cookie = null;
167
+ this.x_token = null;
168
+
169
+ if (e.name == "AbortError" || e.name == "TimeoutError") {
170
+
171
+ this.logger.error({
172
+ name: "KerioLoginTimeout",
173
+ message: `Login attempt to Kerio timed out`,
174
+ type: 'Kerio',
175
+ from: "Kerio.login"
176
+ });
177
+
178
+ throw {
179
+ name: "KerioLoginTimeout",
180
+ message: "Login attempt Kerio timed out",
181
+ type: "Kerio",
182
+ from: "Kerio.login"
183
+ }
184
+ } else {
185
+
186
+ this.logger.error(e);
187
+
188
+ throw e;
189
+ }
190
+ }
191
+ }
192
+
193
+ _createHeartBeatCronJob() {
194
+ return CronJob.from({
195
+ cronTime: `*/15 * * * *`,
196
+ onTick: async () => {
197
+ try {
198
+ await this.renewSessionHeartBeat();
199
+ } catch (e) {
200
+ throw e;
201
+ }
202
+ },
203
+ start: false,
204
+ runOnInit: false,
205
+ waitForCompletion: true
206
+ });
207
+ }
208
+
209
+ async renewSessionHeartBeat() {
210
+ try {
211
+ if (!this.logged_in) {
212
+
213
+ throw {
214
+ name: "KerioClientSessionRenewError",
215
+ message: `Kerio session invalid to renew. Try logging in again.`,
216
+ type: 'Kerio',
217
+ from: "Kerio.renewSessionHeartBeat"
218
+ }
219
+ }
220
+
221
+ if (this.session_cookie == null) {
222
+
223
+ throw {
224
+ name: "KerioClientSessionRenewError",
225
+ message: `Kerio session cookie invalid to renew. Try logging in again.`,
226
+ type: 'Kerio',
227
+ from: "Kerio.renewSessionHeartBeat"
228
+ }
229
+ }
230
+
231
+ if (this.x_token == null) {
232
+
233
+ throw {
234
+ name: "KerioClientSessionRenewError",
235
+ message: `Kerio session token invalid to renew. Try logging in again.`,
236
+ type: 'Kerio',
237
+ from: "Kerio.renewSessionHeartBeat"
238
+ }
239
+ }
240
+
241
+ let renewed = await this.Session.whoAmI();
242
+
243
+ if (renewed.id) {
244
+ this.logger.info({
245
+ name: "KerioClientSessionRenew",
246
+ message: `Kerio session renewed, timeout extended`,
247
+ type: 'Kerio',
248
+ from: "Kerio.renewSessionHeartBeat"
249
+ });
250
+ return true;
251
+ }
252
+
253
+ } catch (e) {
254
+ this.logger.error(e);
255
+ throw e;
256
+ }
257
+ }
258
+
259
+
260
+ }
package/src/modules.js ADDED
@@ -0,0 +1,112 @@
1
+ import https from 'node:https';
2
+ import fetch from 'node-fetch';
3
+
4
+ export class KerioModules {
5
+ /**
6
+ * @param {import('../index.d.ts').Kerio} instance
7
+ */
8
+ constructor(instance) {
9
+ /**
10
+ * @type {import('../index.d.ts').Kerio}
11
+ */
12
+ this.instance = instance;
13
+ }
14
+
15
+ /**
16
+ * @param {import("../index.d.ts").SessionedRequestOptions} options
17
+ * @returns {Promise<import('../index.d.ts').GenericResponse>}
18
+ */
19
+ async sessionedRequest(options) {
20
+ try {
21
+
22
+ if (!this.instance.logged_in) {
23
+
24
+ throw {
25
+ name: "KerioClientSessionError",
26
+ message: "This Kerio instance currently has no valid login session",
27
+ type: "Kerio"
28
+ }
29
+ }
30
+
31
+ if (!this.instance.session_cookie?.includes("SESSION_CONNECT_WEBADMIN") || this.instance.x_token?.length != 64) {
32
+ throw {
33
+ name: "KerioClientSessionTokenError",
34
+ message: "This Kerio instance currently has no valid tokens",
35
+ type: "Kerio"
36
+ }
37
+ }
38
+
39
+ let fetchOptions = {
40
+ method: options.http_method,
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ 'Cookie': options.auth.cookie,
44
+ 'X-Token': options.auth.token
45
+ },
46
+ body: JSON.stringify({
47
+ jsonrpc: '2.0',
48
+ id: 1,
49
+ method: options.api_method,
50
+ params: options.body_params
51
+ }),
52
+ redirect: 'follow',
53
+ signal: AbortSignal.timeout(5000)
54
+ }
55
+
56
+ if (this.instance.https) {
57
+ fetchOptions.agent = new https.Agent({
58
+ rejectUnauthorized: false,
59
+ requestCert: true,
60
+ keepAlive: true
61
+ });
62
+ }
63
+
64
+ // @ts-ignore
65
+ let sessionResponse = await fetch(this.instance.fullURL, fetchOptions);
66
+
67
+ if (sessionResponse.ok) {
68
+
69
+ let sessionResponseBody = await sessionResponse.json();
70
+
71
+ return {
72
+ _headers: sessionResponse.headers.raw(),
73
+ // @ts-ignore
74
+ _body: sessionResponseBody
75
+ }
76
+
77
+ } else {
78
+ let errBody = await sessionResponse.text();
79
+ let _e = {
80
+ name: "HTTPError",
81
+ message: `HTTP Error ${sessionResponse.status} ${sessionResponse.statusText} ${fetchOptions.method} ${options.api_method}`,
82
+ type: 'fetch',
83
+ data: errBody
84
+ }
85
+ throw(_e);
86
+ }
87
+
88
+ } catch (e) {
89
+ this.instance.logger.debug(e);
90
+ throw e;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Reset login flag and session token and cookie
96
+ * @returns {void}
97
+ */
98
+ reset() {
99
+ this.instance.logged_in = false;
100
+ this.instance.session_cookie = null;
101
+ this.instance.x_token = null;
102
+
103
+ this.instance.logger.debug({
104
+ name: "KerioInstanceSessionReset",
105
+ message: "Reset login flag, session cookie, and token to defaults",
106
+ type: "Kerio",
107
+ from: "Kerio._Modules.reset"
108
+ });
109
+
110
+ return;
111
+ }
112
+ }
package/src/session.js ADDED
@@ -0,0 +1,66 @@
1
+ import { KerioModules } from './modules.js';
2
+
3
+ /**
4
+ * @type {import('../index.d.ts').Session}
5
+ */
6
+ export class Session extends KerioModules {
7
+
8
+ /**
9
+ * Obtain the Kerio Instance's user details. Also used to renew the Instance's session to prevent timeout expiration.
10
+ * @returns {Promise<import('../index.d.ts').KerioInstanceUserResponse>}
11
+ */
12
+ async whoAmI() {
13
+ try {
14
+ if (!this.instance.logged_in) {
15
+ throw {
16
+ name: "KerioSessionErrorError",
17
+ message: `Kerio session invalid. Try logging in again.`,
18
+ type: 'Kerio',
19
+ from: "Kerio.Session.whoAmI"
20
+ }
21
+ }
22
+
23
+ let whoami_response = await this.sessionedRequest({
24
+ http_method: 'POST',
25
+ api_method: 'Session.whoAmI',
26
+ auth: {
27
+ cookie: this.instance.session_cookie,
28
+ token: this.instance.x_token
29
+ },
30
+ body_params: {
31
+ }
32
+ });
33
+
34
+ let response_body = whoami_response._body;
35
+
36
+ if (!response_body.result?.errors && !response_body?.error) {
37
+
38
+ return {
39
+ // @ts-ignore
40
+ id: response_body.result.userDetails.id,
41
+ // @ts-ignore
42
+ username: response_body.result.userDetails.loginName,
43
+ // @ts-ignore
44
+ name: response_body.result.userDetails.fullName,
45
+ roles: {
46
+ // @ts-ignore
47
+ userRole: response_body.result.userDetails.effectiveRole.userRole
48
+ }
49
+ }
50
+
51
+ } else {
52
+ throw {
53
+ name: "KerioRequestError",
54
+ message: `Error occured while fetching results from API method 'Session.whoAmI'`,
55
+ type: 'Kerio',
56
+ from: "Kerio.Session.whoAmI"
57
+ }
58
+ }
59
+
60
+ } catch (e) {
61
+ this.instance.logger.error(e);
62
+ throw e;
63
+ }
64
+ }
65
+
66
+ }