import * as _ from 'lodash' ; import * as Docker from 'dockerode' ; import * as path from 'path' ; import * as tarFs from 'tar-fs' ; import * as semver from 'semver' ; import { getTerminalEnvVars , OVERRIDES_DIR } from '../terminal/terminal-env-overrides' ; import { transformComposeCreationLabels } from './docker-compose' ; export const DOCKER_CONTAINER_LABEL = "tech.httptoolkit.docker.proxy" ; /**
 * The path inside the container where injected files will be stored, and so the paths that
 * env vars injected into the container need to reference.
 */ const HTTP_TOOLKIT_INJECTED_PATH = '/http-toolkit-injections' ; const HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH = path . posix . join ( HTTP_TOOLKIT_INJECTED_PATH , 'overrides' ) ; const HTTP_TOOLKIT_INJECTED_CA_PATH = path . posix . join ( HTTP_TOOLKIT_INJECTED_PATH , 'ca.pem' ) ; /**
 * The hostname that resolves to the host OS (i.e. generally: where HTTP Toolkit is running)
 * from inside containers.
 *
 * In Docker for Windows & Mac, host.docker.internal is supported automatically:
 * https://docs.docker.com/docker-for-windows/networking/#use-cases-and-workarounds
 * https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds
 *
 * On Linux this is _not_ supported, so we add it ourselves with (--add-host).
 */ export const DOCKER_HOST_HOSTNAME = "host.docker.internal" ; /**
 * To make the above hostname work on Linux, where it's not supported by default, we need to map it to the
 * host ip. This method works out the host IP to use to do so.
 */ export const getDockerHostIp = ( platform , dockerVersion , containerMetadata ? ) => { const semverVersion = semver . coerce ( 'apiVersion' in dockerVersion ? dockerVersion . apiVersion : dockerVersion . engineVersion ) ; if ( platform !== 'linux' ) { return DOCKER_HOST_HOSTNAME ; } else if ( semver . satisfies ( semverVersion ?? '0.0.0' , 'apiVersion' in dockerVersion ? '>=1.41' : '>=20.10' ) ) { return 'host-gateway' ; } else if ( containerMetadata ) { return containerMetadata . NetworkSettings . Gateway || "172.17.0.1" ; } else { return "172.17.0.1" ; } } ; export function isImageAvailable ( docker , name ) { return docker . getImage ( name ) . inspect ( ) . then ( ( ) => true ) . catch ( ( ) => false ) ; } export function isInterceptedContainer ( container , port ) { return container . Config . Labels [ DOCKER_CONTAINER_LABEL ] === port . toString ( ) ; } const envArrayToObject = ( envArray ) => _ . fromPairs ( ( envArray ?? [ ] ) . map ( ( e ) => { const equalsIndex = e . indexOf ( '=' ) ; if ( equalsIndex === - 1 ) throw new Error ( 'Env var without =' ) ; return [ e . slice ( 0 , equalsIndex ) , e . slice ( equalsIndex + 1 ) ] ; } ) ) ; const envObjectToArray = ( envObject ) : string [ ] => Object . keys ( envObject ) . map ( k => `${ k }=${ envObject [ k ] }` ) ; function packInterceptionFiles ( certContent ) { return tarFs . pack ( OVERRIDES_DIR , { map : ( fileHeader ) => { fileHeader . name = path . posix . join ( HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH , fileHeader . name ) ; fileHeader . uid = 0 ; fileHeader . gid = 0 ; fileHeader . mode = parseInt ( '555' , 8 ) ; return fileHeader ; } , finalize : false , finish : ( pack ) => { pack . entry ( { name : HTTP_TOOLKIT_INJECTED_CA_PATH } , certContent ) ; pack . finalize ( ) ; } } ) ; } export type DOCKER_INTERCEPTION_TYPE = 'mount' | 'inject' ; /**
 * Takes the config for a container, and returns the config to create the
 * same container, but fully intercepted.
 *
 * To hook the creation of any container, we need to get the full config of
 * the container (to make sure we get *all* env vars, for example) and then
 * combine that with the inter
 */ export function transformContainerCreationConfig ( containerConfig , baseImageConfig , { interceptionType , proxyPort , proxyHost , certPath } ) { const imageContainerConfig = baseImageConfig ?. Config ?? baseImageConfig ?. ContainerConfig ; const currentConfig = { ... imageContainerConfig , ... containerConfig , Env : [ ... ( imageContainerConfig ?. Env ?? [ ] ) , ... ( containerConfig . Env ?? [ ] ) ] , Labels : { ... ( imageContainerConfig ?. Labels ?? { } ) , ... ( containerConfig . Labels ?? { } ) } } ; const hostConfig = { ... currentConfig . HostConfig , ... ( interceptionType === 'mount' ? { Binds : [ ... ( currentConfig . HostConfig ?. Binds ?? [ ] ) . filter ( ( existingMount ) => ! existingMount . startsWith ( `${ certPath }:` ) && ! existingMount . startsWith ( `${ OVERRIDES_DIR }:` ) ) , `${ certPath }:${ HTTP_TOOLKIT_INJECTED_CA_PATH }:ro` , `${ OVERRIDES_DIR }:${ HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH }:ro` ] } : { } ) , ... ( process . platform === 'linux' ? { ExtraHosts : [ `${ DOCKER_HOST_HOSTNAME }:${ proxyHost }` , ... ( currentConfig . HostConfig ?. ExtraHosts ?? [ ] ) ] } : { } ) } ; return { ... currentConfig , HostConfig : hostConfig , Env : [ ... ( currentConfig . Env ?? [ ] ) , ... envObjectToArray ( getTerminalEnvVars ( proxyPort , { certPath : HTTP_TOOLKIT_INJECTED_CA_PATH } , envArrayToObject ( currentConfig . Env ) , { httpToolkitIp : DOCKER_HOST_HOSTNAME , overridePath : HTTP_TOOLKIT_INJECTED_OVERRIDES_PATH , targetPlatform : 'linux' } ) ) ] , Labels : { ... transformComposeCreationLabels ( proxyPort , currentConfig . Labels ) , [ DOCKER_CONTAINER_LABEL ] : String ( proxyPort ) } } ; } function deriveContainerCreationConfigFromInspection ( containerDetails ) { return { ... containerDetails . Config , HostConfig : containerDetails . HostConfig , name : containerDetails . Name , NetworkingConfig : { } } ; } async function connectNetworks ( docker , containerId , networks ) { await Promise . all ( Object . keys ( networks ) . map ( networkName => docker . getNetwork ( networkName ) . connect ( { Container : containerId , EndpointConfig : networks [ networkName ] } ) ) ) ; } export async function restartAndInjectContainer ( docker , containerId , { interceptionType , proxyPort , certContent , certPath } ) { const container = docker . getContainer ( containerId ) ; const containerDetails = await container . inspect ( ) ; await container . stop ( { t : 1 } ) ; await container . remove ( ) . catch ( ( e ) => { if ( [ 409 , 404 , 304 ] . includes ( e . statusCode ) ) { return ; } else { throw e ; } } ) ; const proxyHost = getDockerHostIp ( process . platform , { engineVersion : ( await docker . version ( ) ) . Version } , containerDetails ) ; const newContainer = await docker . createContainer ( transformContainerCreationConfig ( deriveContainerCreationConfigFromInspection ( containerDetails ) , undefined , { interceptionType , certPath , proxyPort , proxyHost } ) ) ; connectNetworks ( docker , newContainer . id , containerDetails . NetworkSettings . Networks ) ; if ( interceptionType === 'inject' ) { await newContainer . putArchive ( packInterceptionFiles ( certContent ) , { path : '/' } ) ; } await newContainer . start ( ) ; }