VS Code proxy exploration (#42802)

Co-authored-by: David Veszelovszki <veszelovszki@gmail.com>
This commit is contained in:
Philipp Spiess 2022-10-25 22:51:34 +01:00 committed by GitHub
parent 12a67271ba
commit 6b33130ad7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 205 additions and 55 deletions

View File

@ -1,3 +1,5 @@
import type { Response as NodeFetchResponse } from 'node-fetch'
import { isErrorLike } from '@sourcegraph/common'
const EHTTPSTATUS = 'HTTPStatusError'
@ -6,7 +8,7 @@ export class HTTPStatusError extends Error {
public readonly name = EHTTPSTATUS
public readonly status: number
constructor(response: Response) {
constructor(response: Response | NodeFetchResponse) {
super(`Request to ${response.url} failed with ${response.status} ${response.statusText}`)
this.status = response.status
}
@ -30,11 +32,11 @@ export const isHTTPAuthError = (error: unknown): boolean =>
failedWithHTTPStatus(error, 401) || failedWithHTTPStatus(error, 403)
/**
* Checks if a given fetch Response has a HTTP 2xx status code and throws an HTTPStatusError otherwise.
* Checks if a given fetch Response has an HTTP 2xx status code and throws an HTTPStatusError otherwise.
*/
export function checkOk(response: Response): Response {
export function checkOk(response: Response | NodeFetchResponse): Response {
if (!response.ok) {
throw new HTTPStatusError(response)
}
return response
return response as Response
}

View File

@ -95,7 +95,7 @@ export function applyConfig(config: PluginConfig): void {
customRequestHeaders = parseCustomRequestHeadersString(config.customRequestHeadersAsString)
anonymousUserId = config.anonymousUserId || 'no-user-id'
pluginVersion = config.pluginVersion
polyfillEventSource({ ...(accessToken ? { Authorization: `token ${accessToken}` } : {}), ...customRequestHeaders })
polyfillEventSource({ ...(accessToken ? { Authorization: `token ${accessToken}` } : {}), ...customRequestHeaders }, undefined)
}
function parseCustomRequestHeadersString(headersString: string | null): Record<string, string> | null {

View File

@ -11,8 +11,6 @@ import { hasProperty } from '@sourcegraph/common'
import { TextDocumentDecoration } from '@sourcegraph/extension-api-types'
// LINE DECORATIONS
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: This errors even though we removed config from TextDocumentDecorationType
export const createDecorationType = (): TextDocumentDecorationType => ({ key: uniqueId('TextDocumentDecorationType') })
/**

View File

@ -1,2 +1,2 @@
function polyfillEventSource(headers: { [name: string]: string }): void
function polyfillEventSource(headers: { [name: string]: string }, agent: any): void
export default polyfillEventSource

View File

@ -11,9 +11,11 @@ const https = require('https')
const util = require('util')
let fixedHeaders = {}
let proxyAgent = null
module.exports = function polyfillEventSource(headers) {
module.exports = function polyfillEventSource(headers, agent) {
fixedHeaders = { ...headers }
proxyAgent = agent
global.EventSource = EventSource
@ -171,6 +173,10 @@ function EventSource(url, eventSourceInitDict) {
options.withCredentials = eventSourceInitDict.withCredentials
}
if (proxyAgent) {
options.agent = proxyAgent(url)
}
request = (isSecure ? https : http).request(options, res => {
self.connectionInProgress = false
// Handle HTTP errors

View File

@ -124,11 +124,12 @@ client/vscode
#### Desktop and Web Version
1. `git clone` the [Sourcegraph repository](https://github.com/sourcegraph/sourcegraph)
1. Install dependencies via `yarn` for the Sourcegraph repository
1. Run `yarn generate` at the root directory to generate the required schemas
1. Make your changes to the files within the `client/vscode` directory with VS Code
1. Run `yarn build-vsce` to build or `yarn watch-vsce` to build and watch the tasks from the `root` directory
1. Select `Launch VS Code Extension` (`Launch VS Code Web Extension` for VS Code Web) from the dropdown menu in the `Run and Debug` sidebar view to see your changes
2. Install dependencies via `yarn` for the Sourcegraph repository
3. Run `yarn generate` at the root directory to generate the required schemas
4. Make your changes to the files within the `client/vscode` directory with VS Code
5. Run `yarn build-vsce` to build or `yarn watch-vsce` to build and watch the tasks from the `root` directory
6. To see your changes, open the `Run and Debug` sidebar view in VS Code, and
select `Launch VS Code Extension` (`Launch VS Code Web Extension` for VS Code Web) from the dropdown menu.
### Integration Tests

View File

@ -96,14 +96,18 @@ Alternatively you can use the `Extensions: Configure Recommended Extensions (Wor
This extension contributes the following settings:
| Setting | Description | Example |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| sourcegraph.url | Specify your on-premises Sourcegraph instance here, if applicable. The extension is connected to Sourcegraph Cloud by default. | "https://your-sourcegraph.com" |
| sourcegraph.accessToken | [Depreciated after 2.2.12] The access token to query the Sourcegraph API. Required to use this extension with private instances. Create a new access token at `${SOURCEGRAPH_URL}/users/<sourcegraph-username>/settings/tokens` | null |
| sourcegraph.remoteUrlReplacements | Object, where each `key` is replaced by `value` in the remote url. | {"github": "gitlab", "master": "main"} |
| sourcegraph.defaultBranch | String to set the name of the default branch. Always open files in the default branch. | "master" |
| sourcegraph.requestHeaders | Takes object, where each value pair will be added to the request headers made to your instance. | {"Cache-Control": "no-cache", "Proxy-Authenticate": "Basic"} |
| sourcegraph.basePath | The file path on the machine to the folder that is expected to contain all repositories. We will try to open search results using the basePath. | "/Users/USERNAME/Documents/" |
| Setting | Description | Example |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| sourcegraph.url | Specify your on-premises Sourcegraph instance here, if applicable. The extension is connected to Sourcegraph Cloud by default. | "https://your-sourcegraph.com" |
| sourcegraph.accessToken | [Depreciated after 2.2.12] The access token to query the Sourcegraph API. Required to use this extension with private instances. Create a new access token at `${SOURCEGRAPH_URL}/users/<sourcegraph-username>/settings/tokens` | null |
| sourcegraph.remoteUrlReplacements | Object, where each `key` is replaced by `value` in the remote url. | {"github": "gitlab", "master": "main"} |
| sourcegraph.defaultBranch | String to set the name of the default branch. Always open files in the default branch. | "master" |
| sourcegraph.requestHeaders | Takes object, where each value pair will be added to the request headers made to your instance. | {"Cache-Control": "no-cache", "Proxy-Authenticate": "Basic"} |
| sourcegraph.basePath | The file path on the machine to the folder that is expected to contain all repositories. We will try to open search results using the basePath. | "/Users/USERNAME/Documents/" |
| sourcegraph.proxyProtocol | The protocol to use when proxying requests to the Sourcegraph instance. | "http", "https" |
| sourcegraph.proxyHost | The host to use when proxying requests to the Sourcegraph instance. It shouldn't include a protocol (like "http://") or a port (like ":7080"). When this is set, port must be set as well. | "localhost" |
| sourcegraph.proxyPort | The port to use when proxying requests to the Sourcegraph instance. When this is set, host must be set as well. | 80, 443, 7080, 9090 |
| sourcegraph.proxyPath | The full path to a file when proxying requests to the Sourcegraph instance via a UNIX socket. | "/home/user/path/unix.socket" |
## Questions & Feedback

View File

@ -163,6 +163,43 @@
"examples": [
"/Users/USERNAME/Documents/"
]
},
"sourcegraph.proxyProtocol": {
"description": "The protocol to use when proxying requests to the Sourcegraph instance.",
"type": "string",
"default": "",
"examples": [
"http",
"https"
]
},
"sourcegraph.proxyHost": {
"description": "The host to use when proxying requests to the Sourcegraph instance. It shouldn't include a protocol (like \"http://\") or a port (like \":7080\"). When this is set, port must be set as well.",
"type": "string",
"default": "",
"examples": [
"localhost",
"1.2.3.4"
]
},
"sourcegraph.proxyPort": {
"description": "The port to use when proxying requests to the Sourcegraph instance. When this is set, host must be set as well.",
"type": "number",
"default": 0,
"examples": [
80,
443,
7080,
9090
]
},
"sourcegraph.proxyPath": {
"description": "The full path to a file when proxying requests to the Sourcegraph instance via a UNIX domain socket.",
"type": "string",
"default": "",
"examples": [
"/home/user/path/unix.socket"
]
}
}
},

View File

@ -0,0 +1,72 @@
import type net from 'net'
import { ClientRequest, RequestOptions } from 'agent-base'
import HttpProxyAgent from 'http-proxy-agent'
import HttpsProxyAgent from 'https-proxy-agent'
import fetch, { Headers } from 'node-fetch'
import vscode from 'vscode'
export { fetch, Headers }
export type { BodyInit, Response, HeadersInit } from 'node-fetch'
interface HttpsProxyAgentInterface {
callback(req: ClientRequest, opts: RequestOptions): Promise<net.Socket>
}
type HttpsProxyAgentConstructor = new (
options: string | { [key: string]: number | boolean | string | null }
) => HttpsProxyAgentInterface
export function getProxyAgent(): ((url: URL | string) => HttpsProxyAgentInterface | undefined) | undefined {
const proxyProtocol = vscode.workspace.getConfiguration('sourcegraph').get<string>('proxyProtocol')
const proxyHost = vscode.workspace.getConfiguration('sourcegraph').get<string>('proxyHost')
const proxyPort = vscode.workspace.getConfiguration('sourcegraph').get<number>('proxyPort')
const proxyPath = vscode.workspace.getConfiguration('sourcegraph').get<string>('proxyPath')
// Quit if we're in the browser—we don't need proxying there.
if (HttpsProxyAgent === null) {
return undefined
}
if (proxyHost && !proxyPort) {
console.error('proxyHost is set but proxyPort is not. These two settings must be set together.')
return undefined
}
if (proxyPort && !proxyHost) {
console.error('proxyPort is set but proxyHost is not. These two settings must be set together.')
return undefined
}
if (proxyHost || proxyPort || proxyPath) {
return (url: URL | string) => {
const protocol = getProtocol(url)
if (protocol === undefined) {
return undefined
}
const ProxyAgent = ((protocol === 'http'
? HttpProxyAgent
: HttpsProxyAgent) as unknown) as HttpsProxyAgentConstructor
return new ProxyAgent({
protocol: proxyProtocol === 'http' || proxyProtocol === 'https' ? proxyProtocol : 'https',
...(proxyHost ? { host: proxyHost } : null),
...(proxyPort ? { port: proxyPort } : null),
...(proxyPath ? { path: proxyPath } : null),
})
}
}
return undefined
}
function getProtocol(url: URL | string): string | undefined {
if (typeof url === 'string') {
return url.startsWith('http:') ? 'http' : 'https'
}
if (url instanceof URL) {
return url.protocol === 'http:' ? 'http' : 'https'
}
return undefined
}

View File

@ -0,0 +1,7 @@
// This fake reuses the built-in "fetch" in browsers.
// eslint-disable-next-line import/no-default-export
export default fetch
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const Headers = globalThis.Headers

View File

@ -0,0 +1,6 @@
// This is a fake that we use instead of the read http-proxy-agent and https-proxy-agent modules in the browser.
// (We don't need proxying in the browser—browser settings will determine the proxying.)
// Disabling ESLint because we want to match the library interfaces
// eslint-disable-next-line import/no-default-export
export default null

View File

@ -1,12 +1,15 @@
import { authentication } from 'vscode'
import { asError } from '@sourcegraph/common'
import { checkOk, GraphQLResult, GRAPHQL_URI, isHTTPAuthError } from '@sourcegraph/http-client'
import { checkOk, GRAPHQL_URI, GraphQLResult, isHTTPAuthError } from '@sourcegraph/http-client'
import { handleAccessTokenError } from '../settings/accessTokenSetting'
import { endpointSetting, endpointRequestHeadersSetting } from '../settings/endpointSetting'
import { endpointRequestHeadersSetting, endpointSetting } from '../settings/endpointSetting'
import { fetch, getProxyAgent, Headers, HeadersInit } from './fetch'
let invalidated = false
/**
* To be called when Sourcegraph URL changes.
*/
@ -43,19 +46,20 @@ export const requestGraphQLFromVSCode = async <R, V = object>(
// Debt: intercepted requests in integration tests
// have 0 status codes, so don't check in test environment.
const checkFunction = process.env.IS_TEST ? <T>(value: T): T => value : checkOk
const response = checkFunction(
await fetch(url, {
body: JSON.stringify({
query: request,
variables,
}),
method: 'POST',
headers,
})
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = {
agent: getProxyAgent(),
body: JSON.stringify({
query: request,
variables,
}),
method: 'POST',
headers,
}
const response = checkFunction(await fetch(url, options))
// TODO request cancellation w/ VS Code cancellation tokens.
// eslint-disable-next-line @typescript-eslint/return-await
return response.json() as Promise<GraphQLResult<any>>
return (await response.json()) as GraphQLResult<R>
} catch (error) {
// If `overrideAccessToken` is set, we're validating the token
// and errors will be displayed in the UI.

View File

@ -1,5 +1,3 @@
import 'cross-fetch/polyfill'
import { of, ReplaySubject } from 'rxjs'
import vscode from 'vscode'
@ -9,6 +7,7 @@ import { fetchStreamSuggestions } from '@sourcegraph/shared/src/search/suggestio
import { observeAuthenticatedUser } from './backend/authenticatedUser'
import { logEvent } from './backend/eventLogger'
import { getProxyAgent } from './backend/fetch'
import { initializeInstanceVersionNumber } from './backend/instanceVersion'
import { requestGraphQLFromVSCode } from './backend/requestGraphQl'
import { initializeSearchContexts } from './backend/searchContexts'
@ -26,7 +25,7 @@ import { invalidateContextOnSettingsChange } from './settings/invalidation'
import { LocalStorageService, SELECTED_SEARCH_CONTEXT_SPEC_KEY } from './settings/LocalStorageService'
import { watchUninstall } from './settings/uninstall'
import { createVSCEStateMachine, VSCEQueryState } from './state'
import { focusSearchPanel, openSourcegraphLinks, registerWebviews, copySourcegraphLinks } from './webview/commands'
import { copySourcegraphLinks, focusSearchPanel, openSourcegraphLinks, registerWebviews } from './webview/commands'
import { scretTokenKey, SourcegraphAuthActions, SourcegraphAuthProvider } from './webview/platform/AuthProvider'
/**
* See CONTRIBUTING docs for the Architecture Diagram
@ -57,7 +56,11 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
// Sets global `EventSource` for Node, which is required for streaming search.
// Add custom headers to `EventSource` Authorization header when provided
const customHeaders = endpointRequestHeadersSetting()
polyfillEventSource(initialAccessToken ? { Authorization: `token ${initialAccessToken}`, ...customHeaders } : {})
polyfillEventSource(
initialAccessToken ? { Authorization: `token ${initialAccessToken}`, ...customHeaders } : {},
getProxyAgent()
)
// For search panel webview to signal that it is ready for messages.
// Replay subject with large buffer size just in case panels are opened in quick succession.
const initializedPanelIDs = new ReplaySubject<string>(7)

View File

@ -4,6 +4,7 @@ import * as vscode from 'vscode'
import polyfillEventSource from '@sourcegraph/shared/src/polyfills/vendor/eventSource'
import { LATEST_VERSION } from '@sourcegraph/shared/src/search/stream'
import { getProxyAgent } from '../backend/fetch'
import { initializeSourcegraphSettings } from '../backend/sourcegraphSettings'
import { initializeCodeIntel } from '../code-intel/initialize'
import { ExtensionCoreAPI } from '../contract'
@ -65,7 +66,8 @@ export function registerWebviews({
if (config.affectsConfiguration('sourcegraph.requestHeaders') && session) {
const newCustomHeaders = endpointRequestHeadersSetting()
polyfillEventSource(
session.accessToken ? { Authorization: `token ${session.accessToken}`, ...newCustomHeaders } : {}
session.accessToken ? { Authorization: `token ${session.accessToken}`, ...newCustomHeaders } : {},
getProxyAgent()
)
}
})

View File

@ -13,6 +13,7 @@ import {
import polyfillEventSource from '@sourcegraph/shared/src/polyfills/vendor/eventSource'
import { getProxyAgent } from '../../backend/fetch'
import { endpointRequestHeadersSetting, endpointSetting, setEndpoint } from '../../settings/endpointSetting'
export const scretTokenKey = 'SOURCEGRAPH_AUTH'
@ -24,6 +25,7 @@ class SourcegraphAuthSession implements AuthenticationSession {
}
public readonly id = SourcegraphAuthProvider.id
public readonly scopes = []
constructor(public readonly accessToken: string) {}
}
@ -83,7 +85,10 @@ export class SourcegraphAuthProvider implements AuthenticationProvider, Disposab
await this.cacheTokenFromStorage()
// Update the polyfillEventSource on token changes
polyfillEventSource(
this.currentToken ? { Authorization: `token ${this.currentToken}`, ...endpointRequestHeadersSetting() } : {}
this.currentToken
? { Authorization: `token ${this.currentToken}`, ...endpointRequestHeadersSetting() }
: {},
getProxyAgent()
)
this._onDidChangeSessions.fire({ added, removed, changed })
}

View File

@ -62,6 +62,9 @@ function getExtensionCoreConfiguration(targetType) {
alias:
targetType === 'webworker'
? {
'http-proxy-agent': path.resolve(__dirname, 'src', 'backend', 'proxy-agent-fake-for-browser.ts'),
'https-proxy-agent': path.resolve(__dirname, 'src', 'backend', 'proxy-agent-fake-for-browser.ts'),
'node-fetch': path.resolve(__dirname, 'src', 'backend', 'node-fetch-fake-for-browser.ts'),
path: require.resolve('path-browserify'),
'./browserActionsNode': path.resolve(__dirname, 'src', 'commands', 'browserActionsWeb'),
}

View File

@ -25,7 +25,10 @@ const StanfordCommunitySearchContextPage = lazyComponent(
)
const CncfCommunitySearchContextPage = lazyComponent(() => import('./cncf'), 'CncfCommunitySearchContextPage')
const JuliaCommunitySearchContextPage = lazyComponent(() => import('./Julia'), 'JuliaCommunitySearchContextPage')
const BackstageCommunitySearchContextPage = lazyComponent(() => import('./Backstage'), 'BackstageCommunitySearchContextPage')
const BackstageCommunitySearchContextPage = lazyComponent(
() => import('./Backstage'),
'BackstageCommunitySearchContextPage'
)
// Hack! Hardcode these routes into cmd/frontend/internal/app/ui/router.go
export const communitySearchContextsRoutes: readonly LayoutRouteProps<any>[] = [

View File

@ -78,10 +78,7 @@ export const AddExternalServicePage: React.FunctionComponent<React.PropsWithChil
}
setIsCreating(true)
try {
const service = await addExternalService(
{ input: { ...getExternalServiceInput() } },
telemetryService
)
const service = await addExternalService({ input: { ...getExternalServiceInput() } }, telemetryService)
setIsCreating(false)
setCreatedExternalService(service)
} catch (error) {

View File

@ -77,7 +77,7 @@ export const BackendInsightView = forwardRef<HTMLElement, BackendInsightProps>((
seriesDisplayOptions: {
limit: parseSeriesLimit(debouncedFilters.seriesDisplayOptions.limit),
sortOptions: debouncedFilters.seriesDisplayOptions.sortOptions,
}
},
},
onCompleted: data => {
const parsedData = createBackendInsightData({ ...insight, filters }, data.insightViews.nodes[0])

View File

@ -13,12 +13,8 @@ import sinon from 'sinon'
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
import { MockIntersectionObserver } from '@sourcegraph/shared/src/testing/MockIntersectionObserver'
import { ALL_INSIGHTS_DASHBOARD } from '../constants';
import {
CodeInsightsBackend,
CodeInsightsBackendContext,
FakeDefaultCodeInsightsBackend,
} from '../core'
import { ALL_INSIGHTS_DASHBOARD } from '../constants'
import { CodeInsightsBackend, CodeInsightsBackendContext, FakeDefaultCodeInsightsBackend } from '../core'
import { CodeInsightsRootPage, CodeInsightsRootPageTab } from './CodeInsightsRootPage'

View File

@ -427,8 +427,10 @@
"graphiql": "^1.8.0",
"highlight.js": "^10.5.0",
"highlightjs-graphql": "^1.0.2",
"http-proxy-agent": "^5.0.0",
"http-status-codes": "^2.1.4",
"https-browserify": "^1.0.0",
"https-proxy-agent": "^5.0.1",
"is-absolute-url": "^3.0.3",
"iterare": "^1.2.1",
"js-base64": "^3.7.2",

View File

@ -20011,7 +20011,7 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:5, https-proxy-agent@npm:5.0.1, https-proxy-agent@npm:^5.0.0":
"https-proxy-agent@npm:5, https-proxy-agent@npm:5.0.1, https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1":
version: 5.0.1
resolution: "https-proxy-agent@npm:5.0.1"
dependencies:
@ -29449,9 +29449,11 @@ pvutils@latest:
highlightjs-graphql: ^1.0.2
html-webpack-harddisk-plugin: ^2.0.0
html-webpack-plugin: ^5.3.2
http-proxy-agent: ^5.0.0
http-proxy-middleware: ^1.1.2
http-status-codes: ^2.1.4
https-browserify: ^1.0.0
https-proxy-agent: ^5.0.1
identity-obj-proxy: ^3.0.0
is-absolute-url: ^3.0.3
iterare: ^1.2.1