mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
151 lines
5.6 KiB
TypeScript
151 lines
5.6 KiB
TypeScript
// Copyright (c) 2018, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import * as express from 'express';
|
|
import {profanities} from 'profanities';
|
|
|
|
import {logger} from '../logger.js';
|
|
import {PropertyGetter} from '../properties.interfaces.js';
|
|
import {CompilerProps} from '../properties.js';
|
|
import * as utils from '../utils.js';
|
|
|
|
const FILE_HASH_VERSION = 'Compiler Explorer Config Hasher 2';
|
|
/* How long a string to check for possible unusable hashes (Profanities or confusing text)
|
|
Note that a Hash might end up being longer than this!
|
|
*/
|
|
const USABLE_HASH_CHECK_LENGTH = 9; // Quite generous
|
|
const MAX_TRIES = 4;
|
|
|
|
export type ExpandedShortLink = {
|
|
config: string;
|
|
specialMetadata?: any;
|
|
created?: Date;
|
|
};
|
|
|
|
export type StoredObject = {
|
|
prefix: string;
|
|
uniqueSubHash: string;
|
|
fullHash: string;
|
|
config: string;
|
|
};
|
|
export abstract class StorageBase {
|
|
constructor(
|
|
protected readonly httpRootDir: string,
|
|
protected readonly compilerProps: CompilerProps | PropertyGetter,
|
|
) {}
|
|
|
|
/**
|
|
* Encode a buffer as a URL-safe string.
|
|
*/
|
|
static encodeBuffer(buffer: Buffer): string {
|
|
return utils.base32Encode(buffer);
|
|
}
|
|
|
|
static isCleanText(text: string) {
|
|
const lowercased = text.toLowerCase();
|
|
return !profanities.some(badWord => lowercased.includes(badWord));
|
|
}
|
|
|
|
static getRawConfigHash(config: any) {
|
|
return StorageBase.encodeBuffer(utils.getBinaryHash(JSON.stringify(config), FILE_HASH_VERSION));
|
|
}
|
|
|
|
static getSafeHash(config: any) {
|
|
// Keep rehashing until a usable text is found
|
|
let configHash = StorageBase.getRawConfigHash(config);
|
|
let tries = 1;
|
|
while (!StorageBase.isCleanText(configHash.substring(0, USABLE_HASH_CHECK_LENGTH))) {
|
|
// Shake up the hash a bit by adding, or incrementing a nonce value.
|
|
config.nonce = tries;
|
|
logger.info(`Unusable text found in full hash ${configHash} - Trying again (${tries})`);
|
|
if (tries <= MAX_TRIES) {
|
|
configHash = StorageBase.getRawConfigHash(config);
|
|
++tries;
|
|
} else {
|
|
logger.warn(`Gave up trying to find clean text for ${configHash}`);
|
|
break;
|
|
}
|
|
}
|
|
// And stringify it for the rest of the request
|
|
config = JSON.stringify(config);
|
|
return {config, configHash};
|
|
}
|
|
|
|
static configFor(req: express.Request) {
|
|
if (req.body.config) {
|
|
return req.body.config;
|
|
}
|
|
if (req.body.sessions) {
|
|
return req.body;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
handler(req: express.Request, res: express.Response) {
|
|
// Get the desired config and check for profanities in its hash
|
|
const origConfig = StorageBase.configFor(req);
|
|
if (!origConfig) {
|
|
logger.error('No configuration found');
|
|
res.status(500);
|
|
res.send('Missing config parameter');
|
|
return;
|
|
}
|
|
const {config, configHash} = StorageBase.getSafeHash(origConfig);
|
|
this.findUniqueSubhash(configHash)
|
|
.then(result => {
|
|
logger.info(
|
|
`Unique subhash '${result.uniqueSubHash}' ` +
|
|
`(${result.alreadyPresent ? 'was already present' : 'newly-created'})`,
|
|
);
|
|
if (result.alreadyPresent) {
|
|
return result;
|
|
}
|
|
const storedObject: StoredObject = {
|
|
prefix: result.prefix,
|
|
uniqueSubHash: result.uniqueSubHash,
|
|
fullHash: configHash,
|
|
config: config,
|
|
};
|
|
|
|
return this.storeItem(storedObject, req);
|
|
})
|
|
.then(result => {
|
|
res.send({url: `${req.protocol}://${req.get('host')}${this.httpRootDir}z/${result.uniqueSubHash}`});
|
|
})
|
|
.catch(err => {
|
|
logger.error(err);
|
|
res.status(500);
|
|
res.send(err.message);
|
|
});
|
|
}
|
|
|
|
abstract storeItem(item: StoredObject, req: express.Request): Promise<any>;
|
|
|
|
abstract findUniqueSubhash(hash: string): Promise<any>;
|
|
|
|
abstract expandId(id: string): Promise<ExpandedShortLink>;
|
|
|
|
abstract incrementViewCount(id: string): Promise<any>;
|
|
}
|