Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌎 Allow https proxy from env var #933

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/dull-falcons-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'myst-cli-utils': patch
'myst-templates': patch
'myst-execute': patch
'myst-to-jats': patch
'myst-cli': patch
---

Move fetch function to session
5 changes: 5 additions & 0 deletions .changeset/famous-cats-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'myst-cli': patch
---

Allow https proxy set as environment var
9 changes: 9 additions & 0 deletions docs/installing.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,12 @@ Try the `myst --version` command before and after, with an update you should be
### Dependencies for $\LaTeX$ and PDF

If you are exporting to $\LaTeX$ with an open-source template specified (see all [templates](https://github.com/myst-templates)) or if you are creating a PDF you will need to install a version of [LaTeX](https://www.latex-project.org/get).

## Working with a Proxy Policy

MyST performs web requests to download templates, check DOIs, etc. If you are working on a network that enforces a strict proxy policy for internet access, you may specify a proxy configuration string with the `HTTPS_PROXY` environment variable, for example:

```shell
HTTPS_PROXY=http://168.63.76.32:3128 \
myst build
```
28 changes: 27 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/myst-cli-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"pretty-hrtime": "^1.0.3"
},
"devDependencies": {
"@types/pretty-hrtime": "^1.0.1"
"@types/pretty-hrtime": "^1.0.1",
"node-fetch": "^3.3.0"
}
}
1 change: 0 additions & 1 deletion packages/myst-cli-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export {
export { exec, makeExecutable } from './exec.js';
export { clirun, tic } from './utils.js';
export { isUrl } from './isUrl.js';
export { Session, getSession } from './session.js';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This super minimal session implementation isn't used anywhere (besides a single test file - easily replaced). I just removed it so we did not need to implement fetch in this package.

export {
computeHash,
copyFileMaintainPath,
Expand Down
13 changes: 0 additions & 13 deletions packages/myst-cli-utils/src/session.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/myst-cli-utils/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { RequestInfo, RequestInit, Response } from 'node-fetch';

export type Logger = Pick<typeof console, 'debug' | 'info' | 'warn' | 'error'>;

export type LoggerDE = Pick<Logger, 'debug' | 'error'>;

export interface ISession {
log: Logger;
fetch(url: URL | RequestInfo, init?: RequestInit): Promise<Response>;
}
1 change: 1 addition & 0 deletions packages/myst-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"fs-extra": "^11.1.1",
"get-port": "^6.1.2",
"glob": "^10.3.1",
"https-proxy-agent": "^7.0.4",
"inquirer": "^8.2.2",
"intersphinx": "^1.0.2",
"js-yaml": "^4.1.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/myst-cli/src/build/html/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fs from 'fs-extra';
import path from 'node:path';
import { writeFileToFolder } from 'myst-cli-utils';
import fetch from 'node-fetch';
import type { ISession } from '../../session/types.js';
import { getSiteManifest } from '../site/manifest.js';
import { getMystTemplate } from '../site/template.js';
Expand Down Expand Up @@ -111,7 +110,7 @@ export async function buildHtml(session: ISession, opts: any) {
// Fetch all HTML pages and assets by the template
await Promise.all(
routes.map(async (page) => {
const resp = await fetch(page.url);
const resp = await session.fetch(page.url);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure about the implications of using an HTTPS proxy for localhost requests... These potentially happen here (for building static HTML) as well as in myst-execute, when searching for jupyter sessions.

Do we need logic in fetch for these cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes sounds like we do? i.e. skip proxy requests for any localhost / 127.0.0.1 requests based on the url, and proxy all others?

const content = await resp.text();
writeFileToFolder(path.join(htmlDir, page.path), content);
}),
Expand Down
3 changes: 1 addition & 2 deletions packages/myst-cli/src/process/citations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import fs from 'node:fs';
import fetch from 'node-fetch';
import type { CitationRenderer } from 'citation-js-utils';
import { getCitations } from 'citation-js-utils';
import { tic, isUrl } from 'myst-cli-utils';
Expand All @@ -14,7 +13,7 @@ export async function loadCitations(session: ISession, path: string): Promise<Ci
let data: string;
if (isUrl(path)) {
session.log.debug(`Fetching citations at "${path}"`);
const res = await fetch(path);
const res = await session.fetch(path);
if (!res.ok) {
throw new Error(`Error fetching citations from "${path}": ${res.status} ${res.statusText}`);
}
Expand Down
20 changes: 19 additions & 1 deletion packages/myst-cli/src/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { MystPlugin, RuleId } from 'myst-common';
import latestVersion from 'latest-version';
import boxen from 'boxen';
import chalk from 'chalk';
import { HttpsProxyAgent } from 'https-proxy-agent';
import {
findCurrentProjectAndLoad,
findCurrentSiteAndLoad,
Expand All @@ -22,6 +23,7 @@ import type { ISession } from './types.js';
import { KernelManager, ServerConnection, SessionManager } from '@jupyterlab/services';
import type { JupyterServerSettings } from 'myst-execute';
import { findExistingJupyterServer, launchJupyterServer } from 'myst-execute';
import type { RequestInfo, RequestInit } from 'node-fetch';
import { default as nodeFetch, Headers, Request, Response } from 'node-fetch';

// fetch polyfill for node<18
Expand All @@ -36,6 +38,7 @@ const CONFIG_FILES = ['myst.yml'];
const API_URL = 'https://api.mystmd.org';
const NPM_COMMAND = 'npm i -g mystmd@latest';
const PIP_COMMAND = 'pip install -U mystmd';
const LOCALHOSTS = ['localhost', '127.0.0.1', '::1'];

export function logUpdateAvailable({
current,
Expand Down Expand Up @@ -72,6 +75,7 @@ export class Session implements ISession {
store: Store<RootState>;
$logger: Logger;

proxyAgent?: HttpsProxyAgent<string>;
_shownUpgrade = false;
_latestVersion?: string;
_jupyterSessionManagerPromise?: Promise<SessionManager | undefined>;
Expand All @@ -84,6 +88,8 @@ export class Session implements ISession {
this.API_URL = API_URL;
this.configFiles = CONFIG_FILES;
this.$logger = opts.logger ?? chalkLogger(LogLevel.info, process.cwd());
const proxyUrl = process.env.HTTPS_PROXY;
if (proxyUrl) this.proxyAgent = new HttpsProxyAgent(proxyUrl);
this.store = createStore(rootReducer);
this.reload();
// Allow the latest version to be loaded
Expand Down Expand Up @@ -116,6 +122,18 @@ export class Session implements ISession {
return this;
}

async fetch(url: URL | RequestInfo, init?: RequestInit): Promise<Response> {
const urlOnly = new URL((url as Request).url ?? (url as URL | string));
this.log.debug(`Fetching: ${urlOnly}`);
if (this.proxyAgent && !LOCALHOSTS.includes(urlOnly.hostname)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this localhost check super simple... We could try to catch more edge cases with something like this: https://github.com/Kikobeats/localhost-url-regex/blob/master/src/index.js and/or we could add a process.env.LOCALHOST or something to allow users to specify if they've changed it....

I feel like all of that is probably overkill though.

if (!init) init = {};
init = { agent: this.proxyAgent, ...init };
this.log.debug(`Using HTTPS proxy: ${this.proxyAgent.proxy}`);
}
const resp = await nodeFetch(url, init);
return resp;
}

plugins: MystPlugin | undefined;

_pluginPromise: Promise<MystPlugin> | undefined;
Expand Down Expand Up @@ -196,7 +214,7 @@ export class Session implements ISession {
};
} else {
// Load existing running server
const existing = await findExistingJupyterServer();
const existing = await findExistingJupyterServer(this);
if (existing) {
this.log.debug(`Found existing server on: ${existing.appUrl}`);
partialServerSettings = existing;
Expand Down
3 changes: 2 additions & 1 deletion packages/myst-cli/src/session/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { MystPlugin, RuleId } from 'myst-common';
import type { ReferenceState } from 'myst-transforms';
import type { MinifiedContentCache } from 'nbtx';
import type { Store } from 'redux';

import type { RequestInfo, RequestInit, Response } from 'node-fetch';
import type { BuildWarning, RootState } from '../store/index.js';
import type { PreRendererData, RendererData, SingleCitationRenderer } from '../transforms/types.js';
import type { SessionManager } from '@jupyterlab/services';
Expand All @@ -27,6 +27,7 @@ export type ISession = {
getAllWarnings(ruleId: RuleId): (BuildWarning & { file: string })[];
jupyterSessionManager(): Promise<SessionManager | undefined>;
dispose(): void;
fetch(url: URL | RequestInfo, init?: RequestInit): Promise<Response>;
};

export type ISessionWithCache = ISession & {
Expand Down
15 changes: 8 additions & 7 deletions packages/myst-cli/src/transforms/dois.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { Link } from 'myst-spec';
import type { GenericNode, GenericParent } from 'myst-common';
import { fileWarn, toText, RuleId, plural } from 'myst-common';
import { selectAll } from 'unist-util-select';
import fetch from 'node-fetch';
import { computeHash, tic } from 'myst-cli-utils';
import type { Cite } from 'myst-spec-ext';
import type { SingleCitationRenderer } from './types.js';
Expand Down Expand Up @@ -36,12 +35,14 @@ export async function getDoiOrgBibtex(
const toc = tic();
session.log.debug('Fetching DOI information from doi.org');
const url = `https://doi.org/${normalizedDoi}`;
const response = await fetch(url, {
headers: [['Accept', 'application/x-bibtex']],
}).catch(() => {
session.log.debug(`Request to ${url} failed.`);
return null;
});
const response = await session
.fetch(url, {
headers: [['Accept', 'application/x-bibtex']],
})
.catch(() => {
session.log.debug(`Request to ${url} failed.`);
return null;
});
if (!response || !response.ok) {
session.log.debug(`doi.org fetch failed for ${doiString}}`);
return null;
Expand Down
3 changes: 1 addition & 2 deletions packages/myst-cli/src/transforms/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { RuleId, plural } from 'myst-common';
import { computeHash, hashAndCopyStaticFile, isUrl } from 'myst-cli-utils';
import { remove } from 'unist-util-remove';
import { selectAll } from 'unist-util-select';
import fetch from 'node-fetch';
import path from 'node:path';
import type { VFileMessage } from 'vfile-message';
import type { PageFrontmatter } from 'myst-frontmatter';
Expand Down Expand Up @@ -75,7 +74,7 @@ export async function downloadAndSaveImage(
session.log.debug(`Fetching image: ${url}...\n -> saving to: ${filePath}`);
try {
const github = getGithubRawUrl(url);
const res = await fetch(github ?? url, { headers: EXT_REQUEST_HEADERS });
const res = await session.fetch(github ?? url, { headers: EXT_REQUEST_HEADERS });
const contentType = res.headers.get('content-type') || '';
const extension = mime.extension(contentType);
if (!extension || !contentType) throw new Error('No content-type for image found.');
Expand Down
3 changes: 1 addition & 2 deletions packages/myst-cli/src/transforms/links.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import pLimit from 'p-limit';
import fetch from 'node-fetch';
import type { GenericNode, GenericParent } from 'myst-common';
import { selectAll } from 'unist-util-select';
import { updateLinkTextIfEmpty } from 'myst-transforms';
Expand Down Expand Up @@ -64,7 +63,7 @@ export async function checkLink(session: ISession, url: string): Promise<Externa
}
session.log.debug(`Checking that "${url}" exists`);
const linkCache = loadLinkCache(session, url);
const resp = linkCache ?? (await fetch(url, { headers: EXT_REQUEST_HEADERS }));
const resp = linkCache ?? (await session.fetch(url, { headers: EXT_REQUEST_HEADERS }));
link.ok = resp.ok;
link.status = resp.status;
link.statusText = resp.statusText;
Expand Down
13 changes: 7 additions & 6 deletions packages/myst-execute/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@
"url": "https://github.com/executablebooks/mystmd/issues"
},
"dependencies": {
"@jupyterlab/services": "^7.0.0",
"chalk": "^5.2.0",
"myst-cli-utils": "^2.0.7",
"myst-common": "^1.1.24",
"vfile": "^5.3.7",
"@jupyterlab/services": "^7.0.0",
"node-fetch": "^3.3.0",
"unist-util-select": "^4.0.3",
"which": "^4.0.0",
"node-fetch": "^3.3.0"
"vfile": "^5.3.7",
"which": "^4.0.0"
},
"devDependencies": {
"js-yaml": "^4.1.0",
"@jupyterlab/nbformat": "^3.5.2"
"@jupyterlab/nbformat": "^3.5.2",
"js-yaml": "^4.1.0"
}
}
9 changes: 5 additions & 4 deletions packages/myst-execute/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import type { ServerConnection } from '@jupyterlab/services';
import which from 'which';
import { spawn } from 'node:child_process';
import * as readline from 'node:readline';
import type { Logger } from 'myst-cli-utils';
import type { ISession, Logger } from 'myst-cli-utils';
import chalk from 'chalk';
import fetch from 'node-fetch';

export type JupyterServerSettings = Partial<ServerConnection.ISettings> & {
dispose?: () => void;
Expand All @@ -27,7 +26,9 @@ interface JupyterServerListItem {
/**
* Find the newest (by PID) active Jupyter Server, or return undefined.
*/
export async function findExistingJupyterServer(): Promise<JupyterServerSettings | undefined> {
export async function findExistingJupyterServer(
session: ISession,
): Promise<JupyterServerSettings | undefined> {
const pythonPath = which.sync('python');
const listProc = spawn(pythonPath, ['-m', 'jupyter_server', 'list', '--json']);

Expand All @@ -52,7 +53,7 @@ export async function findExistingJupyterServer(): Promise<JupyterServerSettings

// Return the first alive server
for (const entry of servers) {
const response = await fetch(`${entry.url}?token=${entry.token}`);
const response = await session.fetch(`${entry.url}?token=${entry.token}`);
if (response.ok) {
return {
baseUrl: entry.url,
Expand Down
Loading
Loading