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

Azure blob type header #82

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ import { spawn } from 'cy2';
await spawn(process.env.CYPRESS_API_URL);
```

## Azure blob storage support

Azure requires a `x-ms-blob-type` header to be present when uploading build artifacts to a storage account, in order to have these headers added to the upload requests configure the `AZURE_BLOB_URL` environment variable with the hostname associated with your Azure storage account.

Example:

```sh
AZURE_BLOB_URL=https://your-storage-account.blob.core.windows.net
```

## Breaking Changes

### Version 4
Expand Down
113 changes: 113 additions & 0 deletions src/azure-blob-interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type httpProxyType from 'http-proxy';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { cert, key } from './cert';
import { debugNet } from './debug';
import * as httpProxy from './http-proxy';
import { warn } from './log';

const interceptors = new Map<'upstream' | 'direct', httpProxyType>();

export async function stopAzureBlobInterceptors() {
debugNet('Stopping Azure blob interceptors');
await Promise.all(
Array.from(interceptors.values()).map(
(interceptor, i, all): Promise<void> =>
new Promise((resolve) =>
interceptor.close(() => {
debugNet('Stopped Azure blob interceptor %d/%d', i + 1, all.length);
resolve();
})
)
)
);
interceptors.clear();
}

export async function getUpstreamAzureBlobInterceptor({
target,
upstreamProxy,
}: {
target: string;
upstreamProxy: URL;
}) {
if (interceptors.has('upstream')) {
debugNet(
'Using Azure blob interceptor with upstream routing',
upstreamProxy.toString(),
target
);
return interceptors.get('upstream');
}
debugNet(
'Creating Azure blob interceptor with upstream routing path: %s -> %s',
upstreamProxy.toString(),
target
);
const agent = new HttpsProxyAgent({
protocol: upstreamProxy.protocol, // connect to upstreamProxy over https
host: upstreamProxy.hostname,
port: upstreamProxy.port,
path: upstreamProxy.pathname,
});

interceptors.set(
'upstream',
await createInterceptor({
target,
agent,
})
);
return interceptors.get('upstream');
}

export async function getDirectAzureBlobInterceptor({
target,
}: {
target: string;
}): Promise<httpProxyType> {
if (interceptors.has('direct')) {
debugNet('Using Azure blob interceptor with direct routing');
return interceptors.get('direct') as httpProxyType;
}
debugNet('Creating Azure blob interceptor with direct routing path: %s', target);
interceptors.set(
'direct',
await createInterceptor({
target,
})
);
return interceptors.get('direct') as httpProxyType;
}

function createInterceptor({
target,
agent,
}: {
target: string;
agent?: HttpsProxyAgent;
}): Promise<httpProxyType> {
return new Promise((resolve) => {
debugNet('Creating Azure blob interceptor for %s', target);
const i = httpProxy
// @ts-ignore
.createProxyServer({
target,
agent,
ssl: {
key,
cert,
},
})
.on("proxyReq", (proxyReq) => {
proxyReq.setHeader("x-ms-blob-type", "BlockBlob");
debugNet('Added x-ms-blob-type header to Azure blob request')
})
.on('error', (err) => {
const type = Boolean(agent) ? 'upstream proxy' : 'direct';
debugNet('Azure blob interceptor of type %s error: %s', type, err);
warn('Error connecting to %s: %s', target, err.message);
});

i.listen(0, undefined, () => resolve(i));
});
}
42 changes: 42 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import {
getUpstreamInterceptor,
stopInterceptors,
} from './interceptor';
import {
getDirectAzureBlobInterceptor,
getUpstreamAzureBlobInterceptor,
stopAzureBlobInterceptors,
} from './azure-blob-interceptor';
import {
pipeSocketToLocalPort,
pipeSocketToRemoteDestination,
Expand Down Expand Up @@ -60,6 +65,7 @@ export async function startProxy({
resolve({
stop: async () => {
await stopInterceptors();
await stopAzureBlobInterceptors();
await stopProxy();
debugNet('Stopped proxy + interceptors');
},
Expand Down Expand Up @@ -89,6 +95,11 @@ const getOnConnect = (
return;
}

if (shouldInterceptBlob(req.url)) {
interceptAzureBlobRequest({ target: process.env.AZURE_BLOB_URL as string, hostname, socket, upstreamProxy, noProxy });
return;
}

if (upstreamProxy && shouldUseUpstreamProxy(hostname, noProxy)) {
runProxyChain(req, socket, upstreamProxy);
return;
Expand Down Expand Up @@ -129,6 +140,33 @@ async function interceptRequest({
pipeSocketToLocalPort({ socket, port });
}

async function interceptAzureBlobRequest({
target,
hostname,
socket,
upstreamProxy = null,
noProxy = [],
}: {
target: string;
hostname: string;
socket: net.Socket;
upstreamProxy: URL | null;
noProxy: string[];
}) {
const interceptor = await pipe(
upstreamProxy,
o.fromNullable,
o.filter(() => shouldUseUpstreamProxy(new URL(target).hostname, noProxy)),
o.map((upstreamProxy) => getUpstreamAzureBlobInterceptor({ upstreamProxy, target })),
o.getOrElse(() => getDirectAzureBlobInterceptor({ target }))
);

// @ts-ignore
const port = interceptor._server.address().port;
debugNet('Intercepting Azure blob request to "%s" via port: %d', hostname, port);
pipeSocketToLocalPort({ socket, port });
}

function shouldUseUpstreamProxy(hostname: string, noProxy: string[] = []) {
const result = a.isEmpty(noProxy) ? true : !isMatch(hostname, noProxy);
debugNet(
Expand All @@ -144,6 +182,10 @@ function shouldIntercept(hostname: string) {
return hostname === enc('YXBpLmN5cHJlc3MuaW8=');
}

function shouldInterceptBlob(hostname: string) {
return process.env.AZURE_BLOB_URL && hostname.startsWith(new URL(process.env.AZURE_BLOB_URL).hostname);
}

function isAddress(value: unknown): value is net.AddressInfo {
return typeof value === 'object' && value !== null;
}