Route Google API calls through remote agent with insecure TLS
- validateAddress and getTimeZone now use proxyRequest instead of native fetch so the request originates from the on-prem IP, satisfying the API_KEY_IP_ADDRESS_BLOCKED restriction on the Google key - Both calls carry insecure:true because the agent's network path runs SSL-inspecting proxies that substitute an internal-CA chain (throws SELF_SIGNED_CERT_IN_CHAIN otherwise); scoped per-request, consistent with SIW - Fix latent bug in getTimeZone: use GET (per Google docs) instead of POST - Rename README section to "Remote agent (SIW + Google)" and document the SSL-inspection nuance Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
93b060bc8b
commit
a05dbb2733
2 changed files with 57 additions and 16 deletions
37
README.md
37
README.md
|
|
@ -10,8 +10,9 @@ greetings, build auto-attendants, and clean up user licensing.
|
||||||
- A Webex bot token, a Webex integration (service account) with the scopes
|
- A Webex bot token, a Webex integration (service account) with the scopes
|
||||||
currently used by admin API calls, a Twilio lookup account, an SIW basic-auth
|
currently used by admin API calls, a Twilio lookup account, an SIW basic-auth
|
||||||
user, and a Google API key with Address Validation + Time Zone enabled.
|
user, and a Google API key with Address Validation + Time Zone enabled.
|
||||||
- An on-prem host that can reach Store Info Web, to run the remote agent
|
- An on-prem host that can reach Store Info Web and Google APIs from an
|
||||||
(see [Remote SIW agent](#remote-siw-agent) below).
|
IP-whitelisted subnet, to run the remote agent (see
|
||||||
|
[Remote agent (SIW + Google)](#remote-agent-siw--google) below).
|
||||||
- Optional: a Google service-account JSON key. Only the REST API key is
|
- Optional: a Google service-account JSON key. Only the REST API key is
|
||||||
required today, but if/when you add code that uses `google-auth-library`,
|
required today, but if/when you add code that uses `google-auth-library`,
|
||||||
save the JSON at `config/google-service-account.json` and set
|
save the JSON at `config/google-service-account.json` and set
|
||||||
|
|
@ -57,21 +58,35 @@ The volume mount preserves `config/wbxTokens.json` across restarts so the
|
||||||
refresh cron doesn't lose state. The `-p 8080:8080` publishes the WebSocket
|
refresh cron doesn't lose state. The `-p 8080:8080` publishes the WebSocket
|
||||||
port so the remote SIW agent can connect back to the bot.
|
port so the remote SIW agent can connect back to the bot.
|
||||||
|
|
||||||
## Remote SIW agent
|
## Remote agent (SIW + Google)
|
||||||
|
|
||||||
Store Info Web only accepts connections from inside the corporate network,
|
Some services can only be reached from inside the corporate network:
|
||||||
but the bot runs in the cloud. To bridge the gap, the bot hosts a
|
|
||||||
WebSocket server; a small on-prem agent dials in and proxies HTTP requests
|
- **Store Info Web** — only accepts connections from internal IPs, and
|
||||||
back and forth.
|
presents an internal-CA TLS cert Node's default trust store doesn't know
|
||||||
|
about.
|
||||||
|
- **Google Maps / Address Validation** — the API key is IP-restricted, so
|
||||||
|
requests direct from the cloud bot IP get `API_KEY_IP_ADDRESS_BLOCKED
|
||||||
|
(403)`.
|
||||||
|
|
||||||
|
To bridge the gap, the bot hosts a WebSocket server; a small on-prem agent
|
||||||
|
dials in and proxies HTTP requests back and forth. Both SIW and Google
|
||||||
|
calls go over this bridge so they originate from the on-prem IP.
|
||||||
|
|
||||||
The agent itself is intentionally generic — it just proxies whatever
|
The agent itself is intentionally generic — it just proxies whatever
|
||||||
`{method, url, headers, auth, body}` payload arrives — and lives in
|
`{method, url, headers, auth, body, insecure?}` payload arrives — and
|
||||||
[`docker/remote-agent/`](docker/remote-agent) as a self-contained Docker
|
lives in [`docker/remote-agent/`](docker/remote-agent) as a self-contained
|
||||||
bundle you can build here and ship to the on-prem host.
|
Docker bundle you can build here and ship to the on-prem host. The
|
||||||
|
`insecure: true` flag is scoped per request. Both SIW and Google set it,
|
||||||
|
because the corporate network the agent lives on runs SSL-inspecting
|
||||||
|
proxies that intercept outbound HTTPS with an internal-CA chain — without
|
||||||
|
the flag Node throws `SELF_SIGNED_CERT_IN_CHAIN` even for Google's public
|
||||||
|
certs. Trust is delegated to that proxy by policy, so the scoped bypass is
|
||||||
|
consistent across all proxied traffic.
|
||||||
|
|
||||||
Once the agent is connected, the bot logs `Remote agent connected` and any
|
Once the agent is connected, the bot logs `Remote agent connected` and any
|
||||||
`/buildStore`, `/stageStore`, `/migrateStore` command will succeed. If the
|
`/buildStore`, `/stageStore`, `/migrateStore` command will succeed. If the
|
||||||
agent is not connected, the SIW-dependent commands fail immediately with
|
agent is not connected, those commands fail immediately with
|
||||||
`No remote SIW agent connected` rather than silently timing out.
|
`No remote SIW agent connected` rather than silently timing out.
|
||||||
|
|
||||||
### Deploying the agent
|
### Deploying the agent
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,21 @@
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { requestJson } from '../http.js';
|
import { proxyRequest } from '../services/websocket.js';
|
||||||
|
|
||||||
|
// All Google API calls are routed through the on-prem remote agent so the
|
||||||
|
// request originates from the whitelisted IP subnet. The API key is
|
||||||
|
// restricted by IP; calls direct from the cloud bot IP get 403 with
|
||||||
|
// API_KEY_IP_ADDRESS_BLOCKED. The remote agent runs inside the corporate
|
||||||
|
// network, so its egress IP matches the key's allow-list.
|
||||||
|
//
|
||||||
|
// `insecure: true` is set even though Google's public certs are trusted,
|
||||||
|
// because the corporate network the agent lives on intercepts outbound
|
||||||
|
// HTTPS with an SSL-inspecting proxy that presents an internal-CA chain.
|
||||||
|
// Without the flag Node throws SELF_SIGNED_CERT_IN_CHAIN on any outbound
|
||||||
|
// TLS. Trust is already delegated to that proxy by being on this network,
|
||||||
|
// so the scoped per-request bypass is consistent with SIW.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Address Validation API. Returns the raw response body.
|
* Google Address Validation API. Returns the parsed response body.
|
||||||
*/
|
*/
|
||||||
export async function validateAddress(street, city, state, postalCode, country) {
|
export async function validateAddress(street, city, state, postalCode, country) {
|
||||||
const body = {
|
const body = {
|
||||||
|
|
@ -15,18 +28,31 @@ export async function validateAddress(street, city, state, postalCode, country)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const url = `https://addressvalidation.googleapis.com/v1:validateAddress?key=${encodeURIComponent(config.google.apiKey)}`;
|
const url = `https://addressvalidation.googleapis.com/v1:validateAddress?key=${encodeURIComponent(config.google.apiKey)}`;
|
||||||
return requestJson('POST', url, body);
|
const response = await proxyRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body,
|
||||||
|
insecure: true,
|
||||||
|
});
|
||||||
|
return response?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Time Zone API for a given lat/lon at "now".
|
* Google Time Zone API for a given lat/lon at "now". Uses GET as documented
|
||||||
|
* by Google — parameters are entirely in the query string.
|
||||||
*/
|
*/
|
||||||
export async function getTimeZone(latitude, longitude) {
|
export async function getTimeZone(latitude, longitude) {
|
||||||
const timestamp = Math.floor(Date.now() / 1000);
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
const url =
|
const url =
|
||||||
`https://maps.googleapis.com/maps/api/timezone/json?location=${latitude}%2C${longitude}` +
|
`https://maps.googleapis.com/maps/api/timezone/json?location=${latitude}%2C${longitude}` +
|
||||||
`×tamp=${timestamp}&key=${encodeURIComponent(config.google.apiKey)}`;
|
`×tamp=${timestamp}&key=${encodeURIComponent(config.google.apiKey)}`;
|
||||||
return requestJson('POST', url);
|
const response = await proxyRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url,
|
||||||
|
insecure: true,
|
||||||
|
});
|
||||||
|
return response?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue