Skip to content
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ heroku config:add PATH_PREFIX=/awesome/symbols

Now the symbol server URL can be `http://pepto-symbol.gadgetron.com`.

### Optional S3 request signing

When running against a private S3 origin, the server can sign outgoing `GET` requests.

- Signing is enabled automatically when both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are present.
- `AWS_REGION` is required when signing is enabled.
- `AWS_SESSION_TOKEN` is optional, you can use it for [temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html).

### Running locally

To run Pepto Symbol locally on port 5000:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"node": ">=14"
},
"dependencies": {
"aws4": "^1.13.2",
"http-proxy": "^1.18.1",
"lru-cache": "^6.0.0",
"uuid": "^8.3.2"
Expand All @@ -16,6 +17,7 @@
"start:dev": "PATH_PREFIX=/atom-shell/symbols S3_BUCKET=gh-contractor-zcbenz npm start"
},
"devDependencies": {
"@types/aws4": "^1.11.6",
"@types/http-proxy": "^1.17.6",
"@types/lru-cache": "^5.1.0",
"@types/node": "^15.6.1",
Expand Down
97 changes: 85 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'assert';
import aws4 from 'aws4';
import * as http from 'http';
import httpProxy from 'http-proxy';
import LRU from 'lru-cache';
Expand Down Expand Up @@ -39,6 +40,20 @@ const missingSymbolCache = new LRU<string, boolean>({
max: 10000,
});

const signingAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
const signingSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
const signingSessionToken = process.env.AWS_SESSION_TOKEN;
const signingRegion = process.env.AWS_REGION;
const shouldSignS3Requests = Boolean(signingAccessKeyId && signingSecretAccessKey);

assert(!shouldSignS3Requests || signingRegion, 'AWS_REGION is defined when S3 request signing is enabled');

type ProxyRequest = http.IncomingMessage & {
proxyPath?: string;
cacheKey?: string;
proxyMethod?: string;
};

function incomingPathToProxyPath(path: string): string {
// symstore.exe and symsrv.dll don't always agree on the case of the path to a
// given symbol file. Since our artifact URLs are case-sensitive, this causes symbol
Expand All @@ -58,11 +73,54 @@ function incomingPathToProxyPath(path: string): string {

// The symbols may be hosted a deeper path in the artifact store
// so we prefix the incoming path with that prefix
return `${PATH_PREFIX || ''}${newPath}`;
if (!PATH_PREFIX) {
return newPath;
}

// Handle cases where PATH_PREFIX or newPath may or may not have a
// leading/trailing slash to avoid double slashes or missing slashes
if (PATH_PREFIX.endsWith('/') && newPath.startsWith('/')) {
return `${PATH_PREFIX.slice(0, -1)}${newPath}`;
}

// Handle cases where PATH_PREFIX may or may not have a trailing slash
// and newPath may or may not have a leading slash
if (!PATH_PREFIX.endsWith('/') && !newPath.startsWith('/')) {
return `${PATH_PREFIX}/${newPath}`;
}

return `${PATH_PREFIX}${newPath}`;
}

function signS3ProxyPath(path: string, method: string): string {
if (!shouldSignS3Requests) {
return path;
}

const targetUrl = new url.URL(path, TARGET_URL);
const signedRequest = aws4.sign({
host: TARGET_HOST!,
method,
service: 's3',
region: signingRegion!,
signQuery: true,
path: `${targetUrl.pathname}${targetUrl.search}`,
}, {
accessKeyId: signingAccessKeyId!,
secretAccessKey: signingSecretAccessKey!,
sessionToken: signingSessionToken,
});

return signedRequest.path || `${targetUrl.pathname}${targetUrl.search}`;
}

proxy.on('proxyReq', (proxyReq, request, response, options) => {
proxyReq.path = incomingPathToProxyPath(proxyReq.path);
const req = request as ProxyRequest;
const proxyPath = req.proxyPath ?? incomingPathToProxyPath(proxyReq.path);
const cacheKey = req.cacheKey ?? incomingPathToProxyPath(proxyReq.path);
const proxyMethod = req.proxyMethod ?? proxyReq.method;

proxyReq.path = proxyPath;

// AZ CDN determines the bucket from the Host header
proxyReq.setHeader('Host', TARGET_HOST);
Expand All @@ -75,12 +133,17 @@ proxy.on('proxyReq', (proxyReq, request, response, options) => {
// convert 403s to 404s so symsrv.dll doesn't freak out.
const originalWriteHead = response.writeHead;
response.writeHead = (...args: [number, any]) => {
if (args[0] == 403) {
missingSymbolCache.set(proxyReq.path, true);
if (proxyMethod === 'GET') {
if (args[0] == 403) {
missingSymbolCache.set(cacheKey, true);
args[0] = 404;
} else {
missingSymbolCache.set(cacheKey, false);
}
} else if (args[0] == 403) {
args[0] = 404;
} else {
missingSymbolCache.set(proxyReq.path, false);
}

return originalWriteHead.apply(response, args);
};
});
Expand All @@ -104,23 +167,33 @@ http.createServer((req, res) => {
}

const cacheKey = incomingPathToProxyPath(parsed.pathname + parsed.search);
const requestMethod = (req.method || 'GET').toUpperCase();
const userAgent = req.headers['user-agent'];
const isSentryRequest = userAgent && userAgent.startsWith('symbolicator/');

let signedProxyPath: string;
try {
signedProxyPath = signS3ProxyPath(cacheKey, requestMethod);
} catch (error) {
const errorId = uuid.v4();
console.error('Signing Error:', errorId, 'Request:', req.url, error);
return res.writeHead(500).end(`Failed to sign S3 request. If this happens consistently please report to https://github.com/electron/symbol-server with this error ID: "${errorId}"`);
}

if (isSentryRequest || req.headers['x-electron-symbol-redirect'] === '1') {
res.setHeader('Location', url.format({
protocol: 'https:',
slashes: true,
host: TARGET_HOST,
pathname: cacheKey,
}));
res.setHeader('Location', `${TARGET_URL}${signedProxyPath}`);
return res.writeHead(302).end();
}

if (missingSymbolCache.get(cacheKey)) {
return res.writeHead(404).end();
}

const proxyReq = req as ProxyRequest;
proxyReq.cacheKey = cacheKey;
proxyReq.proxyPath = signedProxyPath;
proxyReq.proxyMethod = requestMethod;

proxy.web(req, res, { target: TARGET_URL });
}).listen(process.env.PORT || 8080);

Expand Down
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,28 @@ __metadata:
version: 0.0.0-use.local
resolution: "@electron/symbol-server@workspace:."
dependencies:
"@types/aws4": "npm:^1.11.6"
"@types/http-proxy": "npm:^1.17.6"
"@types/lru-cache": "npm:^5.1.0"
"@types/node": "npm:^15.6.1"
"@types/uuid": "npm:^8.3.0"
aws4: "npm:^1.13.2"
http-proxy: "npm:^1.18.1"
lru-cache: "npm:^6.0.0"
typescript: "npm:^4.3.2"
uuid: "npm:^8.3.2"
languageName: unknown
linkType: soft

"@types/aws4@npm:^1.11.6":
version: 1.11.6
resolution: "@types/aws4@npm:1.11.6"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/abb84d0dad0b791124389b1bfd2c4c2813077af162cac128f1eb0793f0d4fe3084aa1168f62b41e39f80730aa45b95a9a0034f85c254bebf2194721fe02bc9ad
languageName: node
linkType: hard

"@types/http-proxy@npm:^1.17.6":
version: 1.17.6
resolution: "@types/http-proxy@npm:1.17.6"
Expand Down Expand Up @@ -57,6 +68,13 @@ __metadata:
languageName: node
linkType: hard

"aws4@npm:^1.13.2":
version: 1.13.2
resolution: "aws4@npm:1.13.2"
checksum: 10c0/c993d0d186d699f685d73113733695d648ec7d4b301aba2e2a559d0cd9c1c902308cc52f4095e1396b23fddbc35113644e7f0a6a32753636306e41e3ed6f1e79
languageName: node
linkType: hard

"eventemitter3@npm:^4.0.0":
version: 4.0.4
resolution: "eventemitter3@npm:4.0.4"
Expand Down