diff --git a/src/host-config/index.ts b/src/host-config/index.ts index f5d2e679..3cd9baee 100644 --- a/src/host-config/index.ts +++ b/src/host-config/index.ts @@ -1,5 +1,6 @@ import { promises as fs } from 'fs'; +import type { RedsocksConfig, ProxyConfig } from './types'; import * as config from '../config'; import { pathOnRoot } from '../lib/host-utils'; @@ -25,3 +26,29 @@ export async function setHostname(val: string) { // so the change gets reflected on containers await config.set({ hostname }); } + +export function patchProxy( + currentConf: RedsocksConfig, + inputConf: Partial<{ + redsocks: Partial; + }>, +): RedsocksConfig { + const patchedConf: RedsocksConfig = {}; + + // If input is empty, redsocks config should be removed + if (!inputConf || Object.keys(inputConf).length === 0) { + return patchedConf; + } + + if (inputConf.redsocks && Object.keys(inputConf.redsocks).length > 0) { + // This method assumes that currentConf is a full ProxyConfig + // for backwards compatibility, so patchedConf.redsocks should + // also be a full ProxyConfig. + const patched = { + ...currentConf.redsocks, + ...inputConf.redsocks, + } as Record; + patchedConf.redsocks = patched as ProxyConfig; + } + return patchedConf; +} diff --git a/src/host-config/proxy.ts b/src/host-config/proxy.ts index 61b62eda..c92ef4d8 100644 --- a/src/host-config/proxy.ts +++ b/src/host-config/proxy.ts @@ -3,8 +3,8 @@ import path from 'path'; import { isRight } from 'fp-ts/lib/Either'; import Reporter from 'io-ts-reporters'; -import type { RedsocksConfig } from './types'; import { ProxyConfig } from './types'; +import type { RedsocksConfig } from './types'; import { pathOnBoot, readFromBoot } from '../lib/host-utils'; import { unlinkAll } from '../lib/fs-utils'; import { isENOENT } from '../lib/errors'; diff --git a/src/host-config/types.ts b/src/host-config/types.ts index 180103b5..640aac7b 100644 --- a/src/host-config/types.ts +++ b/src/host-config/types.ts @@ -20,6 +20,10 @@ export const ProxyConfig = t.intersection([ ]); export type ProxyConfig = t.TypeOf; +/** + * The internal object representation of redsocks.conf, obtained + * from RedsocksConf.parse + */ export const RedsocksConfig = t.partial({ redsocks: ProxyConfig, }); diff --git a/test/unit/host-config.spec.ts b/test/unit/host-config.spec.ts index c46fb405..8a165667 100644 --- a/test/unit/host-config.spec.ts +++ b/test/unit/host-config.spec.ts @@ -3,12 +3,14 @@ import { stripIndent } from 'common-tags'; import type { SinonStub } from 'sinon'; import * as hostConfig from '~/src/host-config/index'; +import type { RedsocksConfig, ProxyConfig } from '~/src/host-config/types'; +import { RedsocksConf } from '~/src/host-config/index'; import log from '~/lib/supervisor-console'; describe('RedsocksConf', () => { describe('stringify', () => { it('stringifies RedsocksConfig into config string', () => { - const conf: hostConfig.RedsocksConfig = { + const conf: RedsocksConfig = { redsocks: { type: 'socks5', ip: 'example.org', @@ -17,7 +19,7 @@ describe('RedsocksConf', () => { password: '"bar"', }, }; - const confStr = hostConfig.RedsocksConf.stringify(conf); + const confStr = RedsocksConf.stringify(conf); expect(confStr).to.equal( stripIndent` base { @@ -42,7 +44,7 @@ describe('RedsocksConf', () => { }); it('adds double quotes to auth fields if not exists', () => { - const conf: hostConfig.RedsocksConfig = { + const conf: RedsocksConfig = { redsocks: { type: 'socks5', ip: 'example.org', @@ -51,7 +53,7 @@ describe('RedsocksConf', () => { password: 'bar', }, }; - const confStr = hostConfig.RedsocksConf.stringify(conf); + const confStr = RedsocksConf.stringify(conf); expect(confStr).to.equal( stripIndent` base { @@ -84,8 +86,8 @@ describe('RedsocksConf', () => { login: 'foo', password: 'bar', }, - } as unknown as hostConfig.RedsocksConfig; - const confStr = hostConfig.RedsocksConf.stringify(conf); + } as unknown as RedsocksConfig; + const confStr = RedsocksConf.stringify(conf); expect(confStr).to.equal( stripIndent` base { @@ -110,16 +112,16 @@ describe('RedsocksConf', () => { }); it('stringifies to empty string when provided empty RedsocksConfig', () => { - const conf: hostConfig.RedsocksConfig = {}; - const confStr = hostConfig.RedsocksConf.stringify(conf); + const conf: RedsocksConfig = {}; + const confStr = RedsocksConf.stringify(conf); expect(confStr).to.equal(''); }); it('stringifies to empty string when provided empty redsocks block', () => { - const conf: hostConfig.RedsocksConfig = { - redsocks: {} as hostConfig.ProxyConfig, + const conf: RedsocksConfig = { + redsocks: {} as ProxyConfig, }; - const confStr = hostConfig.RedsocksConf.stringify(conf); + const confStr = RedsocksConf.stringify(conf); expect(confStr).to.equal(''); }); }); @@ -150,7 +152,7 @@ describe('RedsocksConf', () => { } `; - const conf = hostConfig.RedsocksConf.parse(redsocksConfStr); + const conf = RedsocksConf.parse(redsocksConfStr); expect(conf).to.deep.equal({ redsocks: { type: 'socks5', @@ -184,7 +186,7 @@ describe('RedsocksConf', () => { redirector = iptables; } `; - const conf = hostConfig.RedsocksConf.parse(redsocksConfStr); + const conf = RedsocksConf.parse(redsocksConfStr); expect(conf).to.deep.equal({ redsocks: { type: 'http-connect', @@ -213,7 +215,7 @@ describe('RedsocksConf', () => { login = "us}{er"; password = "p{}a}}s{{s"; }`; // No newlines - const conf2 = hostConfig.RedsocksConf.parse(redsocksConfStr2); + const conf2 = RedsocksConf.parse(redsocksConfStr2); expect(conf2).to.deep.equal({ redsocks: { type: 'http-connect', @@ -244,7 +246,7 @@ describe('RedsocksConf', () => { password = pass; } `; - const conf = hostConfig.RedsocksConf.parse(confStr); + const conf = RedsocksConf.parse(confStr); expect(conf).to.deep.equal(expected); const confStr2 = stripIndent` @@ -256,7 +258,7 @@ describe('RedsocksConf', () => { password = pass"; } `; - const conf2 = hostConfig.RedsocksConf.parse(confStr2); + const conf2 = RedsocksConf.parse(confStr2); expect(conf2).to.deep.equal(expected); const confStr3 = stripIndent` @@ -268,7 +270,7 @@ describe('RedsocksConf', () => { password = "pass"; } `; - const conf3 = hostConfig.RedsocksConf.parse(confStr3); + const conf3 = RedsocksConf.parse(confStr3); expect(conf3).to.deep.equal(expected); }); @@ -286,7 +288,7 @@ describe('RedsocksConf', () => { } `; (log.warn as SinonStub).resetHistory(); - const conf = hostConfig.RedsocksConf.parse(redsocksConfStr); + const conf = RedsocksConf.parse(redsocksConfStr); expect((log.warn as SinonStub).lastCall.args[0]).to.equal( 'Invalid redsocks block in redsocks.conf:\n' + 'Expecting NumericIdentifier at 0.port but instead got: "bar" (must be be an positive integer)\n' + @@ -323,7 +325,7 @@ describe('RedsocksConf', () => { } `; (log.warn as SinonStub).resetHistory(); - const conf = hostConfig.RedsocksConf.parse(redsocksConfStr); + const conf = RedsocksConf.parse(redsocksConfStr); expect( (log.warn as SinonStub).getCalls().map((call) => call.firstArg), ).to.deep.equal([ @@ -355,7 +357,7 @@ describe('RedsocksConf', () => { } `; (log.warn as SinonStub).resetHistory(); - const conf = hostConfig.RedsocksConf.parse(redsocksConfStr); + const conf = RedsocksConf.parse(redsocksConfStr); expect( (log.warn as SinonStub).getCalls().map((call) => call.firstArg), ).to.deep.equal([ @@ -375,3 +377,56 @@ describe('RedsocksConf', () => { }); }); }); + +describe('src/host-config', () => { + describe('patchProxy', () => { + it('patches RedsocksConfig with new values', () => { + const current = { + redsocks: { + type: 'socks5', + ip: 'example.org', + port: 1080, + login: '"foo"', + password: '"bar"', + }, + } as RedsocksConfig; + const input = { + redsocks: { + type: 'http-connect', + ip: 'test.balena.io', + }, + } as any; + const patched = hostConfig.patchProxy(current, input); + expect(patched).to.deep.equal({ + redsocks: { + // Patched fields are updated + type: 'http-connect', + ip: 'test.balena.io', + // Unpatched fields retain their original values + port: 1080, + login: '"foo"', + password: '"bar"', + }, + }); + }); + + it('returns empty RedsocksConfig if redsocks config block is empty or invalid', () => { + const current: RedsocksConfig = { + redsocks: { + type: 'socks5', + ip: 'example.org', + port: 1080, + login: '"foo"', + password: '"bar"', + }, + }; + expect(hostConfig.patchProxy(current, { redsocks: {} })).to.deep.equal( + {}, + ); + expect( + hostConfig.patchProxy(current, { redsocks: true } as any), + ).to.deep.equal({}); + expect(hostConfig.patchProxy(current, {})).to.deep.equal({}); + }); + }); +});