From f9611db63734e46ad703b7093cd957175bf84eaf Mon Sep 17 00:00:00 2001 From: Hari Nugraha <15191978+haricnugraha@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:14:42 +0700 Subject: [PATCH] Feat: Configurable Follow HTTP Redirect for Probe Requests (#1270) * refactor: move global variable to context to prevent recreating in many places * refactor: extract log running info and fetch and cache public network info function * refactor: extract init prometheus function * docs: add follow redirect documentation * feat: add follow redirects json schema property * feat: implement follow redirects on request level * fix: adjust affected codes * test: increase test timeout * test: debug test * test: remove CI env var as an indicator of running test * refactor: move fields down to child function * chore: remove unnecessary logs * test: add httpbin docker compose for testing purposes * docs: add readme for HTTPbin --- README.md | 28 +++- dev/docker-compose.yaml | 4 + docs/src/pages/guides/probes.md | 2 + src/commands/monika.ts | 4 +- .../__tests__/expected.sitemap-oneprobe.yml | 5 + .../config/__tests__/expected.textfile.yml | 2 + src/components/config/index.test.ts | 18 ++- src/components/config/index.ts | 6 +- src/components/config/parse-sitemap.ts | 5 +- src/components/config/parse-text.ts | 2 + src/components/config/validate.test.ts | 97 +++++++++++++ .../config/validation/validator/probe.test.ts | 3 +- .../config/validation/validator/probe.ts | 4 + src/components/logger/startup-message.test.ts | 9 +- src/components/notification/alert-message.ts | 3 +- .../probe/prober/http/index.test.ts | 17 ++- src/components/probe/prober/http/index.ts | 6 - .../probe/prober/http/request.test.ts | 127 ++++++++++++++++-- src/components/probe/prober/http/request.ts | 69 +++++----- src/components/probe/prober/index.test.ts | 40 +++++- src/context/index.ts | 2 + src/events/subscribers/application.ts | 5 +- src/interfaces/request.ts | 1 + src/loaders/index.ts | 90 +++++++------ src/looper/index.ts | 2 +- src/monika-config-schema.json | 7 + .../metrics/prometheus/collector.test.ts | 2 + src/symon/index.test.ts | 38 ++++-- src/symon/index.ts | 25 ++-- src/utils/pino.ts | 3 +- src/utils/probe-state.test.ts | 120 +++++++++++++++-- src/utils/public-ip.ts | 24 +++- test/others/hide-ip.test.ts | 16 +-- 33 files changed, 617 insertions(+), 169 deletions(-) create mode 100644 src/components/config/validate.test.ts diff --git a/README.md b/README.md index 74b9fc9f1..aa8cd2ae2 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,38 @@ See this [docs](https://turbo.build/repo/docs/handbook/workspaces#workspaces-whi ### How to Test Probe Locally -If you need to test a probe locally, there are predefined services in /dev/docker-compose.yaml. You are **encouraged** to add other services that can be probed by Monika. Run `cd dev && docker compose up` to run those services. +If you need to test a probe locally, there are predefined services in `/dev/docker-compose.yaml`. You are **encouraged** to add other services that can be probed by Monika. Run `cd dev && docker compose up` to run those services. #### Available Services Use the following Monika config to probe the service. +##### HTTPBin + +```yaml +probes: + - id: 'should not follow redirect' + requests: + - url: http://localhost:3000/status/302 + followRedirects: 0 + alerts: + - assertion: response.status != 302 + message: You should not follow redirect + - id: 'should follow redirect with default config' + requests: + - url: http://localhost:3000/absolute-redirect/20 + alerts: + - assertion: response.status == 302 + message: You are not follow redirect + - id: 'should follow redirect with customize config' + requests: + - url: http://localhost:3000/status/302 + followRedirects: 2 + alerts: + - assertion: response.status == 302 + message: You are not follow redirect +``` + ##### MariaDB ```yaml diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 829ab3b46..a7c678281 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -1,4 +1,8 @@ services: + httpbin: + image: kennethreitz/httpbin + ports: + - 3000:80 mariadb: image: mariadb:11 container_name: monika_mariadb diff --git a/docs/src/pages/guides/probes.md b/docs/src/pages/guides/probes.md index c6f37893c..9e96a23e2 100644 --- a/docs/src/pages/guides/probes.md +++ b/docs/src/pages/guides/probes.md @@ -50,6 +50,7 @@ probes: - assertion: response.status != 200 message: Status not 2xx allowUnauthorized: true + followRedirects: 1 incidentThreshold: 3 alerts: - assertion: response.status != 200 @@ -72,6 +73,7 @@ Details of the field are given in the table below. | alerts (optional) | The condition which will trigger an alert, and the subsequent notification method to send out the alert. See below for further details on alerts and notifications. See [alerts](./alerts) section for detailed information. | | ping (optional) | (boolean), If set true then send a PING to the specified url instead. | | allowUnauthorized (optional) | (boolean), If set to true, will make https agent to not check for ssl certificate validity | +| followRedirects (optional) | The request follows redirects as many times as specified here. If unspecified, it will fallback to the value set by the [follow redirects flag](https://monika.hyperjump.tech/guides/cli-options#follow-redirects) | ## Request Body diff --git a/src/commands/monika.ts b/src/commands/monika.ts index 437439d04..cb82e426b 100644 --- a/src/commands/monika.ts +++ b/src/commands/monika.ts @@ -36,7 +36,7 @@ import { closeLog, openLogfile } from '../components/logger/history' import { logStartupMessage } from '../components/logger/startup-message' import { scheduleSummaryNotification } from '../components/notification/schedule-notification' import { sendMonikaStartMessage } from '../components/notification/start-message' -import { setContext } from '../context' +import { getContext, setContext } from '../context' import events from '../events' import { type MonikaFlags, @@ -410,7 +410,7 @@ export default class Monika extends Command { signal, }) - if (process.env.NODE_ENV === 'test') { + if (getContext().isTest) { break } diff --git a/src/components/config/__tests__/expected.sitemap-oneprobe.yml b/src/components/config/__tests__/expected.sitemap-oneprobe.yml index 3cde2b3e1..da5302258 100644 --- a/src/components/config/__tests__/expected.sitemap-oneprobe.yml +++ b/src/components/config/__tests__/expected.sitemap-oneprobe.yml @@ -6,22 +6,27 @@ probes: method: GET timeout: 10000 body: '' + followRedirects: 21 - url: https://de.wiktionary.org/wiki/hyperjump method: GET timeout: 10000 body: '' + followRedirects: 21 - url: https://id.wiktionary.org/wiki/hyperjump method: GET timeout: 10000 body: '' + followRedirects: 21 - url: https://en.wiktionary.org/wiki/hyperjump method: GET timeout: 10000 body: '' + followRedirects: 21 - url: https://monika.hyperjump.tech/index method: GET timeout: 10000 body: '' + followRedirects: 21 interval: 900 alerts: - assertion: response.status < 200 or response.status > 299 diff --git a/src/components/config/__tests__/expected.textfile.yml b/src/components/config/__tests__/expected.textfile.yml index 9143e99ab..c1c19fe0b 100644 --- a/src/components/config/__tests__/expected.textfile.yml +++ b/src/components/config/__tests__/expected.textfile.yml @@ -6,6 +6,7 @@ probes: method: GET timeout: 10000 body: {} + followRedirects: 21 interval: 900 alerts: - assertion: response.status < 200 or response.status > 299 @@ -19,6 +20,7 @@ probes: method: GET timeout: 10000 body: {} + followRedirects: 21 interval: 900 alerts: - assertion: response.status < 200 or response.status > 299 diff --git a/src/components/config/index.test.ts b/src/components/config/index.test.ts index 592e45dec..470e7418a 100644 --- a/src/components/config/index.test.ts +++ b/src/components/config/index.test.ts @@ -147,7 +147,14 @@ describe('updateConfig', () => { id: '1', name: '', interval: 1000, - requests: [{ url: 'https://example.com', body: '', timeout: 1000 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 1000, + }, + ], alerts: [], }, ], @@ -181,7 +188,14 @@ describe('updateConfig', () => { id: '1', name: '', interval: 1000, - requests: [{ url: 'https://example.com', body: '', timeout: 1000 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 1000, + }, + ], alerts: [], }, ], diff --git a/src/components/config/index.ts b/src/components/config/index.ts index 27e95e0a3..5808b9911 100644 --- a/src/components/config/index.ts +++ b/src/components/config/index.ts @@ -62,7 +62,6 @@ type WatchConfigFileParams = { path: string } -const isTestEnvironment = process.env.CI || process.env.NODE_ENV === 'test' const emitter = getEventEmitter() const defaultConfigs: Partial[] = [] @@ -103,7 +102,8 @@ export const updateConfig = async (config: Config): Promise => { log.info('Config file update detected') } catch (error: unknown) { const message = getErrorMessage(error) - if (isTestEnvironment) { + + if (getContext().isTest) { // return error during tests throw new Error(message) } @@ -220,7 +220,7 @@ function scheduleRemoteConfigFetcher({ } function watchConfigFile({ flags, path }: WatchConfigFileParams) { - const isWatchConfigFile = !(isTestEnvironment || flags.repeat !== 0) + const isWatchConfigFile = !(getContext().isTest || flags.repeat !== 0) if (isWatchConfigFile) { const watcher = watch(path) watcher.on('change', async () => { diff --git a/src/components/config/parse-sitemap.ts b/src/components/config/parse-sitemap.ts index 6dd4da2f6..ecf733013 100644 --- a/src/components/config/parse-sitemap.ts +++ b/src/components/config/parse-sitemap.ts @@ -24,11 +24,12 @@ import type { Config } from '../../interfaces/config' import { XMLParser } from 'fast-xml-parser' +import { getContext } from '../../context' import { monikaFlagsDefaultValue } from '../../flag' import type { MonikaFlags } from '../../flag' import type { Probe, ProbeAlert } from '../../interfaces/probe' +import type { RequestConfig } from '../../interfaces/request' import Joi from 'joi' -import { RequestConfig } from 'src/interfaces/request' const sitemapValidator = Joi.object({ config: Joi.object({ @@ -95,6 +96,7 @@ const generateProbesFromXmlOneProbe = (parseResult: unknown) => { method: 'GET', timeout: 10_000, body: '', + followRedirects: getContext().flags['follow-redirects'], }, ] if (item['xhtml:link']) { @@ -106,6 +108,7 @@ const generateProbesFromXmlOneProbe = (parseResult: unknown) => { method: 'GET', timeout: 10_000, body: '', + followRedirects: getContext().flags['follow-redirects'], }, ] } diff --git a/src/components/config/parse-text.ts b/src/components/config/parse-text.ts index 528d39533..5ab44d021 100644 --- a/src/components/config/parse-text.ts +++ b/src/components/config/parse-text.ts @@ -46,6 +46,7 @@ * SOFTWARE. * **********************************************************************************/ +import { getContext } from '../../context' import type { Config } from '../../interfaces/config' import { monikaFlagsDefaultValue } from '../../flag' import type { Probe, ProbeAlert } from '../../interfaces/probe' @@ -67,6 +68,7 @@ export const parseConfigFromText = (configString: string): Config => { method: 'GET', timeout: 10_000, body: {} as JSON, + followRedirects: getContext().flags['follow-redirects'], }, ], interval: monikaFlagsDefaultValue['config-interval'], diff --git a/src/components/config/validate.test.ts b/src/components/config/validate.test.ts new file mode 100644 index 000000000..87149decb --- /dev/null +++ b/src/components/config/validate.test.ts @@ -0,0 +1,97 @@ +/********************************************************************************** + * MIT License * + * * + * Copyright (c) 2021 Hyperjump Technology * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy * + * of this software and associated documentation files (the "Software"), to deal * + * in the Software without restriction, including without limitation the rights * + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * + * copies of the Software, and to permit persons to whom the Software is * + * furnished to do so, subject to the following conditions: * + * * + * The above copyright notice and this permission notice shall be included in all * + * copies or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * + * SOFTWARE. * + **********************************************************************************/ + +import { expect } from '@oclif/test' + +import { getContext, setContext } from '../../context' +import type { Probe } from '../../interfaces/probe' +import { validateProbes } from './validation' + +describe('Configuration validation', () => { + it('should use default follow redirect from flag', async () => { + // arrange + const probes: Probe[] = [ + { + id: 'hrf6g', + requests: [ + { + body: '', + url: 'https://example.com', + timeout: 1000, + }, + ], + } as Probe, + ] + // act + const validatedProbes = await validateProbes(probes) + + // assert + expect(validatedProbes[0].requests![0].followRedirects).eq(21) + }) + + it('should use follow redirect from flag', async () => { + // arrange + setContext({ flags: { ...getContext().flags, 'follow-redirects': 0 } }) + const probes: Probe[] = [ + { + id: 'hrf6g', + requests: [ + { + body: '', + url: 'https://example.com', + timeout: 1000, + }, + ], + } as Probe, + ] + // act + const validatedProbes = await validateProbes(probes) + + // assert + expect(validatedProbes[0].requests![0].followRedirects).eq(0) + }) + + it('should use follow redirect from config', async () => { + // arrange + setContext({ flags: { ...getContext().flags, 'follow-redirects': 0 } }) + const probes: Probe[] = [ + { + id: 'hrf6g', + requests: [ + { + body: '', + followRedirects: 1, + url: 'https://example.com', + timeout: 1000, + }, + ], + } as Probe, + ] + // act + const validatedProbes = await validateProbes(probes) + + // assert + expect(validatedProbes[0].requests![0].followRedirects).eq(1) + }) +}) diff --git a/src/components/config/validation/validator/probe.test.ts b/src/components/config/validation/validator/probe.test.ts index 3f16ed0c6..b8af791ba 100644 --- a/src/components/config/validation/validator/probe.test.ts +++ b/src/components/config/validation/validator/probe.test.ts @@ -28,7 +28,7 @@ import type { Probe } from '../../../../interfaces/probe' import { FAILED_REQUEST_ASSERTION } from '../../../../looper' import { validateProbes } from './probe' -import { resetContext, setContext } from '../../../../context' +import { getContext, resetContext, setContext } from '../../../../context' import type { MonikaFlags } from '../../../../flag' describe('Probe validation', () => { @@ -175,6 +175,7 @@ describe('Probe validation', () => { // arrange setContext({ flags: { + ...getContext().flags, symonKey: 'bDF8j', symonUrl: 'https://example.com', } as MonikaFlags, diff --git a/src/components/config/validation/validator/probe.ts b/src/components/config/validation/validator/probe.ts index 01276f63c..46bb0842e 100644 --- a/src/components/config/validation/validator/probe.ts +++ b/src/components/config/validation/validator/probe.ts @@ -259,6 +259,10 @@ export async function validateProbes(probes: Probe[]): Promise { joi.number(), joi.bool() ), + followRedirects: joi + .number() + .min(0) + .default(getContext().flags['follow-redirects']), headers: joi.object().allow(null), id: joi.string().allow(''), interval: joi.number().min(1), diff --git a/src/components/logger/startup-message.test.ts b/src/components/logger/startup-message.test.ts index bc292c15d..0b3f49b97 100644 --- a/src/components/logger/startup-message.test.ts +++ b/src/components/logger/startup-message.test.ts @@ -37,7 +37,13 @@ const defaultConfig: Config = { name: 'Acme Inc.', interval: 3000, requests: [ - { url: 'https://example.com', headers: {}, body: '', timeout: 0 }, + { + url: 'https://example.com', + headers: {}, + body: '', + followRedirects: 21, + timeout: 0, + }, ], alerts: [ { @@ -240,6 +246,7 @@ describe('Startup message', () => { url: 'https://example.com', headers: {}, body: '', + followRedirects: 21, timeout: 0, }, ], diff --git a/src/components/notification/alert-message.ts b/src/components/notification/alert-message.ts index 6892d5c8f..abb6a14e6 100644 --- a/src/components/notification/alert-message.ts +++ b/src/components/notification/alert-message.ts @@ -32,7 +32,7 @@ import { getContext } from '../../context' import type { NotificationMessage } from '@hyperjumptech/monika-notification' import { ProbeRequestResponse } from '../../interfaces/request' import { ProbeAlert } from '../../interfaces/probe' -import { publicIpAddress, publicNetworkInfo } from '../../utils/public-ip' +import { getPublicNetworkInfo, publicIpAddress } from '../../utils/public-ip' import { getIncidents } from '../incident' const getLinuxDistro = promisify(getos) @@ -40,6 +40,7 @@ const getLinuxDistro = promisify(getos) export const getMonikaInstance = async (ipAddress: string): Promise => { const osHostname = hostname() + const publicNetworkInfo = getPublicNetworkInfo() if (publicNetworkInfo) { const { city, isp } = publicNetworkInfo diff --git a/src/components/probe/prober/http/index.test.ts b/src/components/probe/prober/http/index.test.ts index f38df3935..a474417a4 100644 --- a/src/components/probe/prober/http/index.test.ts +++ b/src/components/probe/prober/http/index.test.ts @@ -66,6 +66,7 @@ const probes: Probe[] = [ { url: 'https://example.com', body: '', + followRedirects: 21, timeout: 30, }, ], @@ -205,6 +206,7 @@ describe('HTTP Probe processing', () => { { url: 'https://example.com', body: '', + followRedirects: 21, timeout: 30, }, ], @@ -314,11 +316,18 @@ describe('HTTP Probe processing', () => { }) }) ) - const probe = { + const probe: Probe = { ...probes[0], id: 'fj43l', incidentThreshold: 1, - requests: [{ url: 'https://example.com', body: '', timeout: 30 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 30, + }, + ], alerts: [ { id: 'jFQBd', assertion: 'response.status != 200', message: '' }, ], @@ -348,10 +357,10 @@ describe('HTTP Probe processing', () => { await sleep(3 * seconds) // assert - expect(notificationAlert?.[probe.requests[0].url]?.body?.url).eq( + expect(notificationAlert?.[probe.requests![0].url]?.body?.url).eq( 'https://example.com' ) - expect(notificationAlert?.[probe.requests[0].url]?.body?.alert).eq( + expect(notificationAlert?.[probe.requests![0].url]?.body?.alert).eq( 'response.status != 200' ) }).timeout(10_000) diff --git a/src/components/probe/prober/http/index.ts b/src/components/probe/prober/http/index.ts index 5c0b1e633..0d0541208 100644 --- a/src/components/probe/prober/http/index.ts +++ b/src/components/probe/prober/http/index.ts @@ -22,7 +22,6 @@ * SOFTWARE. * **********************************************************************************/ -import { monikaFlagsDefaultValue } from '../../../../flag' import { BaseProber, NotificationType, type ProbeParams } from '..' import { getContext } from '../../../../context' import events from '../../../../events' @@ -57,11 +56,6 @@ export class HTTPProber extends BaseProber { responses.push( // eslint-disable-next-line no-await-in-loop await httpRequest({ - isVerbose: getContext().flags.verbose, - maxRedirects: - getContext().flags['follow-redirects'] || - monikaFlagsDefaultValue['follow-redirects'], - isEnableFetch: getContext().flags['native-fetch'], requestConfig: { ...requestConfig, signal }, responses, }) diff --git a/src/components/probe/prober/http/request.test.ts b/src/components/probe/prober/http/request.test.ts index c5367796e..8518a70aa 100644 --- a/src/components/probe/prober/http/request.test.ts +++ b/src/components/probe/prober/http/request.test.ts @@ -26,6 +26,7 @@ import { expect } from '@oclif/test' import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' +import { getContext, resetContext, setContext } from '../../../../context' import type { ProbeRequestResponse, RequestConfig, @@ -38,9 +39,14 @@ describe('probingHTTP', () => { describe('httpRequest function', () => { before(() => { server.listen() + setContext({ + ...getContext(), + flags: { ...getContext().flags, 'follow-redirects': 0 }, + }) }) afterEach(() => { server.resetHandlers() + resetContext() }) after(() => { server.close() @@ -66,10 +72,11 @@ describe('probingHTTP', () => { ) // create the requests - const requests = [ + const requests: RequestConfig[] = [ { url: 'http://localhost:4000/get_key', body: '', + followRedirects: 21, timeout: 10_000, }, { @@ -79,6 +86,7 @@ describe('probingHTTP', () => { Authorization: '{{ responses.[0].data.token }}', }, body: '', + followRedirects: 21, timeout: 10_000, }, ] @@ -91,7 +99,6 @@ describe('probingHTTP', () => { try { // eslint-disable-next-line no-await-in-loop const resp = await httpRequest({ - maxRedirects: 0, requestConfig: request, responses, }) @@ -135,11 +142,11 @@ describe('probingHTTP', () => { body: JSON.parse( '{"username": "example@example.com", "password": "example"}' ), + followRedirects: 21, timeout: 10_000, } const result = await httpRequest({ - maxRedirects: 0, requestConfig: request, responses: [], }) @@ -173,12 +180,12 @@ describe('probingHTTP', () => { method: 'POST', headers: { 'content-type': 'multipart/form-data' }, body: { username: 'john@example.com', password: 'drowssap' } as never, + followRedirects: 21, timeout: 10_000, } // act const res = await httpRequest({ - maxRedirects: 0, requestConfig: request, responses: [], }) @@ -216,12 +223,12 @@ describe('probingHTTP', () => { method: 'POST', headers: { 'content-type': 'text/plain' }, body: 'multiline string\nexample', + followRedirects: 21, timeout: 10_000, } // act const res = await httpRequest({ - maxRedirects: 0, requestConfig: request, responses: [], }) @@ -259,12 +266,12 @@ describe('probingHTTP', () => { method: 'POST', headers: { 'content-type': 'text/yaml' }, body: { username: 'john@example.com', password: 'secret' } as never, + followRedirects: 21, timeout: 10_000, } // act const res = await httpRequest({ - maxRedirects: 0, requestConfig: request, responses: [], }) @@ -302,12 +309,12 @@ describe('probingHTTP', () => { method: 'POST', headers: { 'content-type': 'application/xml' }, body: { username: 'john@example.com', password: 'secret' } as never, + followRedirects: 21, timeout: 10_000, } // act const res = await httpRequest({ - maxRedirects: 0, requestConfig: request, responses: [], }) @@ -318,6 +325,13 @@ describe('probingHTTP', () => { it('Should handle HTTP redirect with axios', async () => { // arrange + setContext({ + ...getContext(), + flags: { + ...getContext().flags, + 'follow-redirects': 3, + }, + }) server.use( http.get( 'https://example.com/get', @@ -348,11 +362,10 @@ describe('probingHTTP', () => { method: 'GET', body: '', timeout: 10_000, + followRedirects: 3, } const res = await httpRequest({ - isEnableFetch: true, - maxRedirects: 3, requestConfig, responses: [], }) @@ -391,11 +404,10 @@ describe('probingHTTP', () => { method: 'GET', body: '', timeout: 10_000, + followRedirects: 3, } const res = await httpRequest({ - isEnableFetch: false, - maxRedirects: 3, requestConfig, responses: [], }) @@ -432,13 +444,13 @@ describe('probingHTTP', () => { method: 'POST', headers: { 'content-type': 'text/plain' }, body: 'multiline string\nexample', + followRedirects: 21, timeout: 10_000, allowUnauthorized: true, } // act const res = await httpRequest({ - maxRedirects: 0, requestConfig: request, responses: [], }) @@ -446,6 +458,97 @@ describe('probingHTTP', () => { // assert expect(res.status).to.eq(200) }) + + it('should follow redirect', async () => { + // arrange + server.use( + http.get( + 'https://example.com/redirect-1', + async () => + new HttpResponse(null, { + status: 302, + headers: { + Location: '/redirect-2', + }, + }) + ), + http.get( + 'https://example.com/redirect-2', + async () => + new HttpResponse(null, { + status: 302, + headers: { + Location: '/', + }, + }) + ), + http.get( + 'https://example.com', + async () => + new HttpResponse(null, { + status: 200, + }) + ) + ) + const request = { + url: 'https://example.com/redirect-1', + } as RequestConfig + + // act + const res = await httpRequest({ + requestConfig: request, + responses: [], + }) + + // assert + expect(res.status).to.eq(200) + }) + + it('should not follow redirect', async () => { + // arrange + server.use( + http.get( + 'https://example.com/redirect-1', + async () => + new HttpResponse(null, { + status: 302, + headers: { + Location: '/redirect-2', + }, + }) + ), + http.get( + 'https://example.com/redirect-2', + async () => + new HttpResponse(null, { + status: 301, + headers: { + Location: '/', + }, + }) + ), + http.get( + 'https://example.com', + async () => + new HttpResponse(null, { + status: 200, + }) + ) + ) + const request = { + url: 'https://example.com/redirect-1', + followRedirects: 0, + } as RequestConfig + + // act + const res = await httpRequest({ + requestConfig: request, + responses: [], + }) + + // assert + expect(res.status).to.eq(302) + }) }) describe('Unit test', () => { diff --git a/src/components/probe/prober/http/request.ts b/src/components/probe/prober/http/request.ts index ceba67416..0911747dd 100644 --- a/src/components/probe/prober/http/request.ts +++ b/src/components/probe/prober/http/request.ts @@ -24,34 +24,30 @@ import * as Handlebars from 'handlebars' import FormData from 'form-data' +import Joi from 'joi' +// eslint-disable-next-line no-restricted-imports +import * as qs from 'querystring' +import { errors as undiciErrors } from 'undici' import YAML from 'yaml' import { type ProbeRequestResponse, type RequestConfig, probeRequestResult, } from '../../../../interfaces/request' - -// eslint-disable-next-line no-restricted-imports -import * as qs from 'querystring' - +import { getContext } from '../../../../context' import { icmpRequest } from '../icmp/request' import registerFakes from '../../../../utils/fakes' import { sendHttpRequest, sendHttpRequestFetch } from '../../../../utils/http' import { log } from '../../../../utils/pino' import { AxiosError } from 'axios' import { getErrorMessage } from '../../../../utils/catch-error-handler' -import { errors as undiciErrors } from 'undici' -import Joi from 'joi' // Register Handlebars helpers registerFakes(Handlebars) type probingParams = { - maxRedirects: number requestConfig: Omit // is a config object responses: Array // an array of previous responses - isVerbose?: boolean - isEnableFetch?: boolean } const UndiciErrorValidator = Joi.object({ @@ -64,15 +60,20 @@ const UndiciErrorValidator = Joi.object({ * @returns ProbeRequestResponse, response to the probe request */ export async function httpRequest({ - maxRedirects, requestConfig, responses, - isEnableFetch, - isVerbose, }: probingParams): Promise { // Compile URL using handlebars to render URLs that uses previous responses data - const { method, url, headers, timeout, body, ping, allowUnauthorized } = - requestConfig + const { + method, + url, + headers, + timeout, + body, + ping, + allowUnauthorized, + followRedirects, + } = requestConfig const newReq = { method, headers, timeout, body, ping } const renderURL = Handlebars.compile(url) const renderedURL = renderURL({ responses }) @@ -99,11 +100,10 @@ export async function httpRequest({ } // Do the request using compiled URL and compiled headers (if exists) - if (isEnableFetch) { + if (getContext().flags['native-fetch']) { return await probeHttpFetch({ startTime, - isVerbose, - maxRedirects, + maxRedirects: followRedirects, renderedURL, requestParams: { ...newReq, headers: requestHeaders }, allowUnauthorized, @@ -112,7 +112,7 @@ export async function httpRequest({ return await probeHttpAxios({ startTime, - maxRedirects, + maxRedirects: followRedirects, renderedURL, requestParams: { ...newReq, headers: requestHeaders }, allowUnauthorized, @@ -235,13 +235,11 @@ async function probeHttpFetch({ requestParams, allowUnauthorized, maxRedirects, - isVerbose, }: { startTime: number renderedURL: string allowUnauthorized: boolean | undefined maxRedirects: number - isVerbose?: boolean requestParams: { method: string | undefined headers: Headers | undefined @@ -250,15 +248,10 @@ async function probeHttpFetch({ ping: boolean | undefined } }): Promise { - if (isVerbose) log.info(`Probing ${renderedURL} with Node.js fetch`) - console.log( - 'renderedURL =', - renderedURL, - '| requestParams =', - JSON.stringify(requestParams), - '| maxRedirects =', - maxRedirects - ) + if (getContext().flags.verbose) { + log.info(`Probing ${renderedURL} with Node.js fetch`) + } + const response = await sendHttpRequestFetch({ ...requestParams, allowUnauthorizedSsl: allowUnauthorized, @@ -297,13 +290,7 @@ async function probeHttpFetch({ } } -async function probeHttpAxios({ - startTime, - renderedURL, - allowUnauthorized, - maxRedirects, - requestParams, -}: { +type ProbeHTTPAxiosParams = { startTime: number renderedURL: string allowUnauthorized: boolean | undefined @@ -315,7 +302,15 @@ async function probeHttpAxios({ body: string | object ping: boolean | undefined } -}): Promise { +} + +async function probeHttpAxios({ + startTime, + renderedURL, + requestParams, + allowUnauthorized, + maxRedirects, +}: ProbeHTTPAxiosParams): Promise { const resp = await sendHttpRequest({ ...requestParams, allowUnauthorizedSsl: allowUnauthorized, diff --git a/src/components/probe/prober/index.test.ts b/src/components/probe/prober/index.test.ts index e47af500f..9b81291ba 100644 --- a/src/components/probe/prober/index.test.ts +++ b/src/components/probe/prober/index.test.ts @@ -78,7 +78,14 @@ describe('Prober', () => { id: 'bcDnX', interval: 2, name: 'Example', - requests: [{ body: '', timeout: 30_000, url: 'https://example.com' }], + requests: [ + { + body: '', + followRedirects: 21, + timeout: 30_000, + url: 'https://example.com', + }, + ], }, } @@ -115,7 +122,14 @@ describe('Prober', () => { createdAt: new Date(), recoveredAt: null, }, - requests: [{ body: '', timeout: 30_000, url: 'https://example.com' }], + requests: [ + { + body: '', + followRedirects: 21, + timeout: 30_000, + url: 'https://example.com', + }, + ], }, } @@ -153,7 +167,14 @@ describe('Prober', () => { createdAt: new Date(), recoveredAt: new Date(), }, - requests: [{ body: '', timeout: 30_000, url: 'https://example.com' }], + requests: [ + { + body: '', + followRedirects: 21, + timeout: 30_000, + url: 'https://example.com', + }, + ], }, } @@ -191,7 +212,14 @@ describe('Prober', () => { createdAt: new Date(), recoveredAt: null, }, - requests: [{ body: '', timeout: 30_000, url: 'https://example.com' }], + requests: [ + { + body: '', + followRedirects: 21, + timeout: 30_000, + url: 'https://example.com', + }, + ], }, } @@ -233,6 +261,7 @@ describe('Prober', () => { requests: [ { body: '', + followRedirects: 21, timeout: 30_000, url: 'https://example.com', alerts: [ @@ -295,6 +324,7 @@ describe('Prober', () => { requests: [ { body: '', + followRedirects: 21, timeout: 30_000, url: 'https://example.com', alerts: [ @@ -347,6 +377,7 @@ describe('Prober', () => { requests: [ { body: '', + followRedirects: 21, timeout: 30_000, url: 'https://example.com', alerts: [ @@ -408,6 +439,7 @@ describe('Prober', () => { requests: [ { body: '', + followRedirects: 21, timeout: 30_000, url: 'https://example.com', alerts: [ diff --git a/src/context/index.ts b/src/context/index.ts index 50eb13230..1913b6ac1 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -38,6 +38,7 @@ type Context = { // userAgent example: @hyperjumptech/monika/1.2.3 linux-x64 node-14.17.0 userAgent: string incidents: Incident[] + isTest: boolean config?: Omit flags: MonikaFlags } @@ -47,6 +48,7 @@ type NewContext = Partial const initialContext: Context = { userAgent: '', incidents: [], + isTest: process.env.NODE_ENV === 'test', flags: monikaFlagsDefaultValue, } diff --git a/src/events/subscribers/application.ts b/src/events/subscribers/application.ts index 5a781e943..6dee21842 100644 --- a/src/events/subscribers/application.ts +++ b/src/events/subscribers/application.ts @@ -26,6 +26,7 @@ import { hostname } from 'os' import { getConfig } from '../../components/config' import { sendNotifications } from '@hyperjumptech/monika-notification' import { getMessageForTerminate } from '../../components/notification/alert-message' +import { getContext } from '../../context' import events from '../../events' import { getEventEmitter } from '../../utils/events' import getIp from '../../utils/ip' @@ -34,9 +35,7 @@ import { log } from '../../utils/pino' const eventEmitter = getEventEmitter() eventEmitter.on(events.application.terminated, async () => { - const isTestEnvironment = process.env.NODE_ENV === 'test' - - if (!isTestEnvironment) { + if (!getContext().isTest) { const message = await getMessageForTerminate(hostname(), getIp()) const config = getConfig() diff --git a/src/interfaces/request.ts b/src/interfaces/request.ts index 1d81dc4d1..03931e121 100644 --- a/src/interfaces/request.ts +++ b/src/interfaces/request.ts @@ -61,6 +61,7 @@ export interface RequestConfig extends Omit { saveBody?: boolean // save response body to db? url: string body: object | string + followRedirects: number timeout: number // request timeout alerts?: ProbeAlert[] headers?: object diff --git a/src/loaders/index.ts b/src/loaders/index.ts index 15b8bb229..c915c85ec 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -24,7 +24,7 @@ import type { Config as IConfig } from '@oclif/core' import { isSymonModeFrom, setupConfig } from '../components/config' -import { setContext } from '../context' +import { getContext, setContext } from '../context' import events from '../events' import type { MonikaFlags } from '../flag' import { tlsChecker } from '../jobs/tls-check' @@ -35,7 +35,7 @@ import { startPrometheusMetricsServer, } from '../plugins/metrics/prometheus' import { getEventEmitter } from '../utils/events' -import { getPublicNetworkInfo } from '../utils/public-ip' +import { fetchAndCacheNetworkInfo } from '../utils/public-ip' import { jobsLoader } from './jobs' import { enableAutoUpdate } from '../plugins/updater' import { log } from '../utils/pino' @@ -48,27 +48,10 @@ export default async function init( flags: MonikaFlags, cliConfig: IConfig ): Promise { - const eventEmitter = getEventEmitter() - const isTestEnvironment = process.env.CI || process.env.NODE_ENV === 'test' const isSymonMode = isSymonModeFrom(flags) setContext({ userAgent: cliConfig.userAgent }) - - if (flags.verbose || isSymonMode) { - // cache location & ISP info - getPublicNetworkInfo() - .then(({ city, hostname, isp, privateIp, publicIp }) => { - log.info( - `Monika is running from: ${city} - ${isp} (${publicIp}) - ${hostname} (${privateIp})` - ) - }) - .catch((error) => - log.warn(`Failed to obtain location/ISP info. Got: ${error}`) - ) - } else { - // if note verbose, remove location details - ;`Monika is running.` - } + await logRunningInfo({ isSymonMode, isVerbose: flags.verbose }) // check if connected to STUN Server and getting the public IP in the same time loopCheckSTUNServer(flags.stun) @@ -79,26 +62,7 @@ export default async function init( // start Promotheus server if (flags.prometheus) { - const { - collectProbeTotal, - collectProbeRequestMetrics, - collectTriggeredAlert, - decrementProbeRunningTotal, - incrementProbeRunningTotal, - resetProbeRunningTotal, - } = new PrometheusCollector() - - // collect prometheus metrics - eventEmitter.on(events.config.sanitized, (probes: Probe[]) => { - collectProbeTotal(probes.length) - }) - eventEmitter.on(events.probe.response.received, collectProbeRequestMetrics) - eventEmitter.on(events.probe.alert.triggered, collectTriggeredAlert) - eventEmitter.on(events.probe.ran, incrementProbeRunningTotal) - eventEmitter.on(events.probe.finished, decrementProbeRunningTotal) - eventEmitter.on(events.config.updated, resetProbeRunningTotal) - - startPrometheusMetricsServer(flags.prometheus) + initPrometheus(flags.prometheus) } if (!isSymonMode) { @@ -107,9 +71,53 @@ export default async function init( // check TLS when Monika starts tlsChecker() - if (!isTestEnvironment) { + if (!getContext().isTest) { // load cron jobs jobsLoader() } } } + +type RunningInfoParams = { isSymonMode: boolean; isVerbose: boolean } + +async function logRunningInfo({ isVerbose, isSymonMode }: RunningInfoParams) { + if (!isVerbose && !isSymonMode) { + log.info('Monika is running.') + return + } + + try { + const { city, hostname, isp, privateIp, publicIp } = + await fetchAndCacheNetworkInfo() + + log.info( + `Monika is running from: ${city} - ${isp} (${publicIp}) - ${hostname} (${privateIp})` + ) + } catch (error) { + log.warn(`Failed to obtain location/ISP info. Got: ${error}`) + } +} + +function initPrometheus(prometheusPort: number) { + const eventEmitter = getEventEmitter() + const { + collectProbeTotal, + collectProbeRequestMetrics, + collectTriggeredAlert, + decrementProbeRunningTotal, + incrementProbeRunningTotal, + resetProbeRunningTotal, + } = new PrometheusCollector() + + // collect prometheus metrics + eventEmitter.on(events.config.sanitized, (probes: Probe[]) => { + collectProbeTotal(probes.length) + }) + eventEmitter.on(events.probe.response.received, collectProbeRequestMetrics) + eventEmitter.on(events.probe.alert.triggered, collectTriggeredAlert) + eventEmitter.on(events.probe.ran, incrementProbeRunningTotal) + eventEmitter.on(events.probe.finished, decrementProbeRunningTotal) + eventEmitter.on(events.config.updated, resetProbeRunningTotal) + + startPrometheusMetricsServer(prometheusPort) +} diff --git a/src/looper/index.ts b/src/looper/index.ts index 51504b69f..b2c04b350 100644 --- a/src/looper/index.ts +++ b/src/looper/index.ts @@ -93,7 +93,7 @@ export async function loopCheckSTUNServer(interval: number): Promise { if (interval === -1) return // if interval = 0 get ip once and exit. No need to setup interval. - if (interval === 0 || process.env.CI || process.env.NODE_ENV === 'test') { + if (interval === 0 || getContext().isTest) { await getPublicIp() return } diff --git a/src/monika-config-schema.json b/src/monika-config-schema.json index b0ad06f30..8ec49ced8 100644 --- a/src/monika-config-schema.json +++ b/src/monika-config-schema.json @@ -302,6 +302,13 @@ "body": { "$ref": "#/definitions/body" }, + "followRedirects": { + "title": "Follow Redirects", + "description": "The request follows redirects as many times as specified here", + "type": "integer", + "examples": ["21"], + "default": "21" + }, "headers": { "$ref": "#/definitions/headers" } diff --git a/src/plugins/metrics/prometheus/collector.test.ts b/src/plugins/metrics/prometheus/collector.test.ts index c45a19494..efea4e200 100644 --- a/src/plugins/metrics/prometheus/collector.test.ts +++ b/src/plugins/metrics/prometheus/collector.test.ts @@ -59,6 +59,7 @@ describe('Prometheus collector', () => { { url: '', body: '', + followRedirects: 21, timeout: 0, }, ], @@ -167,6 +168,7 @@ describe('Prometheus collector', () => { { url: '', body: '', + followRedirects: 21, timeout: 0, }, ], diff --git a/src/symon/index.test.ts b/src/symon/index.test.ts index 366d09e5a..31a31b889 100644 --- a/src/symon/index.test.ts +++ b/src/symon/index.test.ts @@ -26,7 +26,7 @@ import { expect } from '@oclif/test' import { type DefaultBodyType, HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' -import { monikaFlagsDefaultValue, type MonikaFlags } from '../flag' +import { monikaFlagsDefaultValue } from '../flag' import type { Config } from '../interfaces/config' import type { Probe } from '../interfaces/probe' @@ -54,7 +54,14 @@ const config: Config = { id: '2', name: 'Example', interval: 10, - requests: [{ url: 'https://example.com', body: '', timeout: 2000 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2000, + }, + ], alerts: [], }, ], @@ -103,11 +110,12 @@ describe('Symon initiate', () => { resetContext() setContext({ flags: { + ...getContext().flags, symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs: monikaFlagsDefaultValue.symonGetProbesIntervalMs, - } as MonikaFlags, + }, }) // reset probe cache @@ -127,9 +135,10 @@ describe('Symon initiate', () => { setContext({ userAgent: 'v1.5.0', flags: { + ...getContext().flags, symonUrl: 'http://localhost:4000', symonKey: 'random-key', - } as MonikaFlags, + }, }) let body: DefaultBodyType = {} // mock the outgoing requests @@ -277,7 +286,14 @@ describe('Symon initiate', () => { id: '3', name: 'New Probe', interval: 2, - requests: [{ url: 'https://example.com', body: '', timeout: 2000 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2000, + }, + ], alerts: [], } server.use( @@ -291,10 +307,11 @@ describe('Symon initiate', () => { const symonGetProbesIntervalMs = 100 setContext({ flags: { + ...getContext().flags, symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - } as MonikaFlags, + }, }) const symon = new SymonClient({ symonUrl: 'http://localhost:4000', @@ -343,10 +360,11 @@ describe('Symon initiate', () => { const symonGetProbesIntervalMs = 100 setContext({ flags: { + ...getContext().flags, symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - } as MonikaFlags, + }, }) const symon = new SymonClient({ symonUrl: 'http://localhost:4000', @@ -396,10 +414,11 @@ describe('Symon initiate', () => { const symonGetProbesIntervalMs = 100 setContext({ flags: { + ...getContext().flags, symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - } as MonikaFlags, + }, }) const symon = new SymonClient({ symonUrl: 'http://localhost:4000', @@ -449,10 +468,11 @@ describe('Symon initiate', () => { const symonGetProbesIntervalMs = 100 setContext({ flags: { + ...getContext().flags, symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - } as MonikaFlags, + }, }) const symon = new SymonClient({ symonUrl: 'http://localhost:4000', diff --git a/src/symon/index.ts b/src/symon/index.ts index d024d9497..3b67893fa 100644 --- a/src/symon/index.ts +++ b/src/symon/index.ts @@ -53,10 +53,10 @@ import getIp from '../utils/ip' import { log } from '../utils/pino' import { removeProbeState, syncProbeStateFrom } from '../utils/probe-state' import { + fetchAndCacheNetworkInfo, getPublicIp, getPublicNetworkInfo, publicIpAddress, - publicNetworkInfo, } from '../utils/public-ip' type SymonHandshakeData = { @@ -111,20 +111,23 @@ type ProbeChange = { type ProbeAssignmentTotal = { total: number; updatedAt?: Date } const getHandshakeData = async (): Promise => { - await retry(handleAll, { - backoff: new ExponentialBackoff(), - }).execute(async () => { - await getPublicNetworkInfo() - .then(({ city, hostname, isp, privateIp, publicIp }) => { + if (!getPublicNetworkInfo()) { + await retry(handleAll, { + backoff: new ExponentialBackoff(), + }).execute(async () => { + try { + const { city, hostname, isp, privateIp, publicIp } = + await fetchAndCacheNetworkInfo() log.info( `[Symon] Monika is running from: ${city} - ${isp} (${publicIp}) - ${hostname} (${privateIp})` ) - }) - .catch((error) => { + } catch (error) { log.error(`[Symon] ${error}. Retrying...`) throw error - }) - }) + } + }) + } + await getPublicIp() const os = await getOSName() @@ -132,7 +135,7 @@ const getHandshakeData = async (): Promise => { const host = hostname() const publicIp = publicIpAddress const privateIp = getIp() - const { city, country, isp } = publicNetworkInfo! + const { city, country, isp } = getPublicNetworkInfo()! const { pid } = process const { userAgent: version } = getContext() diff --git a/src/utils/pino.ts b/src/utils/pino.ts index 1825d40a4..f42e3f28f 100644 --- a/src/utils/pino.ts +++ b/src/utils/pino.ts @@ -25,11 +25,12 @@ import fs from 'fs' import path from 'path' import { pino, transport } from 'pino' +import { getContext } from '../context' export const log = pino(getOptions()) function getOptions() { - if (process.env.NODE_ENV === 'test') { + if (getContext().isTest) { return { base: undefined, level: 'error', diff --git a/src/utils/probe-state.test.ts b/src/utils/probe-state.test.ts index 5a0a84bb5..af0e1ae37 100644 --- a/src/utils/probe-state.test.ts +++ b/src/utils/probe-state.test.ts @@ -23,6 +23,8 @@ **********************************************************************************/ import { expect } from 'chai' +import sinon from 'sinon' + import type { Probe } from '../interfaces/probe' import { getProbeContext, @@ -31,7 +33,6 @@ import { setProbeFinish, setProbeRunning, } from './probe-state' -import sinon from 'sinon' describe('probe-state', () => { const timeNow = new Date() @@ -54,7 +55,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -79,7 +87,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], lastEvent: { createdAt: lastEventCreatedAt, }, @@ -109,7 +124,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], lastEvent: { recoveredAt: lastEventCreatedAt, }, @@ -137,7 +159,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], lastEvent: { createdAt: lastEventCreatedAt, recoveredAt: lastEventCreatedAt, @@ -167,7 +196,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -187,7 +223,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -208,7 +251,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -228,7 +278,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -248,7 +305,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -269,7 +333,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -292,7 +363,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -314,7 +392,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] @@ -336,7 +421,14 @@ describe('probe-state', () => { id: '1', interval: 1, name: 'Example', - requests: [{ url: 'https://example.com', body: '', timeout: 2 }], + requests: [ + { + url: 'https://example.com', + body: '', + followRedirects: 21, + timeout: 2, + }, + ], }, ] diff --git a/src/utils/public-ip.ts b/src/utils/public-ip.ts index d3b2306ed..3c4799576 100644 --- a/src/utils/public-ip.ts +++ b/src/utils/public-ip.ts @@ -42,9 +42,7 @@ type PublicNetwork = { export let publicIpAddress = '' export let isConnectedToSTUNServer = true -export let publicNetworkInfo: PublicNetwork | undefined - -const isTestEnvironment = process.env.CI || process.env.NODE_ENV === 'test' +let publicNetworkInfo: PublicNetwork | undefined /** * pokeStun sends a poke/request to stun server @@ -53,7 +51,7 @@ const isTestEnvironment = process.env.CI || process.env.NODE_ENV === 'test' async function pokeStun(): Promise { // for testing, bypass ping/stun server... apparently ping cannot run in github actions // reference: https://github.com/actions/virtual-environments/issues/1519 - if (isTestEnvironment) { + if (getContext().isTest) { return '192.168.1.1' // adding for specific asserts in other tests } @@ -66,14 +64,14 @@ async function pokeStun(): Promise { throw new Error('stun inaccessible') // could not connect to STUN server } -export async function getPublicNetworkInfo(): Promise { +async function fetchPublicNetworkInfo(): Promise { const publicIp = await pokeStun() const response = await sendHttpRequest({ url: `http://ip-api.com/json/${publicIp}`, }) const { country, city, isp } = response.data - publicNetworkInfo = { + return { country, city, hostname: hostname(), @@ -81,10 +79,24 @@ export async function getPublicNetworkInfo(): Promise { privateIp: getIp(), publicIp, } +} + +async function setPublicNetworkInfo(publicNetwork: PublicNetwork) { + publicNetworkInfo = publicNetwork +} +export function getPublicNetworkInfo(): PublicNetwork | undefined { return publicNetworkInfo } +// cache location & ISP info +export async function fetchAndCacheNetworkInfo() { + const publicNetwork = await fetchPublicNetworkInfo() + await setPublicNetworkInfo(publicNetwork) + + return publicNetwork +} + /** * getPublicIP sends a request to stun server getting IP address * @returns Promise diff --git a/test/others/hide-ip.test.ts b/test/others/hide-ip.test.ts index 9164d838b..b2d45e609 100644 --- a/test/others/hide-ip.test.ts +++ b/test/others/hide-ip.test.ts @@ -30,12 +30,12 @@ import * as IpUtil from '../../src/utils/public-ip' describe('Monika should hide ip unless verbose', () => { let getPublicIPStub: sinon.SinonStub - let getPublicNetworkInfoStub: sinon.SinonStub + let fetchAndCacheNetworkInfoStub: sinon.SinonStub beforeEach(() => { getPublicIPStub = sinon.stub(IpUtil, 'getPublicIp' as never) - getPublicNetworkInfoStub = sinon - .stub(IpUtil, 'getPublicNetworkInfo' as never) + fetchAndCacheNetworkInfoStub = sinon + .stub(IpUtil, 'fetchAndCacheNetworkInfo' as never) .callsFake(async () => ({ country: 'Earth', city: 'Gotham', @@ -48,7 +48,7 @@ describe('Monika should hide ip unless verbose', () => { afterEach(() => { getPublicIPStub.restore() - getPublicNetworkInfoStub.restore() + fetchAndCacheNetworkInfoStub.restore() }) test @@ -56,8 +56,8 @@ describe('Monika should hide ip unless verbose', () => { .do(() => cmd.run(['--config', resolve('./test/testConfigs/simple-1p-1n.yaml')]) ) - .it('should not call getPublicNetworkInfo()', () => { - sinon.assert.notCalled(getPublicNetworkInfoStub) + .it('should not call fetchAndCacheNetworkInfo()', () => { + sinon.assert.notCalled(fetchAndCacheNetworkInfoStub) }) test @@ -69,7 +69,7 @@ describe('Monika should hide ip unless verbose', () => { '--verbose', ]) ) - .it('should call getPublicNetworkInfo() when --verbose', () => { - sinon.assert.calledOnce(getPublicNetworkInfoStub) + .it('should call fetchAndCacheNetworkInfo() when --verbose', () => { + sinon.assert.calledOnce(fetchAndCacheNetworkInfoStub) }) })