some basic hygiene before I can hack

This commit is contained in:
Ricardo Pardini
2023-01-20 14:10:02 +01:00
parent 4550e4593c
commit dac363442f
20 changed files with 2791 additions and 1764 deletions

View File

@@ -6,3 +6,6 @@ node_modules
*.swo
data
*.DS_Store
.github
.idea
config.json

View File

@@ -1,2 +1,2 @@
**/*.min.js
config.js
config.json

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ node_modules
data
*.DS_Store
docker-compose.override.yml
.idea

View File

@@ -1,4 +1,4 @@
FROM node:16-stretch
FROM node:16
RUN mkdir -p /usr/src/app && \
chown node:node /usr/src/app
@@ -7,25 +7,23 @@ USER node:node
WORKDIR /usr/src/app
COPY --chown=node:node package*.json .
RUN npm ci
COPY --chown=node:node . .
RUN npm install && \
npm install redis@0.8.1 && \
npm install pg@4.5.7 && \
npm install memcached@2.2.2 && \
npm install aws-sdk@2.814.0 && \
npm install rethinkdbdash@2.3.31
ENV STORAGE_TYPE=memcached \
STORAGE_HOST=127.0.0.1 \
STORAGE_PORT=11211\
STORAGE_EXPIRE_SECONDS=2592000\
STORAGE_DB=2 \
STORAGE_AWS_BUCKET= \
STORAGE_AWS_REGION= \
STORAGE_USENAME= \
STORAGE_PASSWORD= \
STORAGE_FILEPATH=
STORAGE_AWS_BUCKET="" \
STORAGE_AWS_REGION="" \
STORAGE_USENAME="" \
STORAGE_PASSWORD="" \
STORAGE_FILEPATH=""
ENV LOGGING_LEVEL=verbose \
LOGGING_TYPE=Console \
@@ -39,20 +37,20 @@ ENV HOST=0.0.0.0\
RECOMPRESS_STATIC_ASSETS=true
ENV KEYGENERATOR_TYPE=phonetic \
KEYGENERATOR_KEYSPACE=
KEYGENERATOR_KEYSPACE=""
ENV RATELIMITS_NORMAL_TOTAL_REQUESTS=500\
RATELIMITS_NORMAL_EVERY_MILLISECONDS=60000 \
RATELIMITS_WHITELIST_TOTAL_REQUESTS= \
RATELIMITS_WHITELIST_EVERY_MILLISECONDS= \
RATELIMITS_WHITELIST_TOTAL_REQUESTS="" \
RATELIMITS_WHITELIST_EVERY_MILLISECONDS="" \
# comma separated list for the whitelisted \
RATELIMITS_WHITELIST=example1.whitelist,example2.whitelist \
\
RATELIMITS_BLACKLIST_TOTAL_REQUESTS= \
RATELIMITS_BLACKLIST_EVERY_MILLISECONDS= \
RATELIMITS_BLACKLIST_TOTAL_REQUESTS="" \
RATELIMITS_BLACKLIST_EVERY_MILLISECONDS="" \
# comma separated list for the blacklisted \
RATELIMITS_BLACKLIST=example1.blacklist,example2.blacklist
ENV DOCUMENTS=about=./about.md
ENV DOCUMENTS="about=./about.md"
EXPOSE ${PORT}
STOPSIGNAL SIGINT

View File

@@ -39,7 +39,7 @@ STDOUT. Check the README there for more details and usages.
## Installation
1. Download the package, and expand it
2. Explore the settings inside of config.js, but the defaults should be good
2. Explore the settings inside of config.json, but the defaults should be good
3. `npm install`
4. `npm start` (you may specify an optional `<config-path>` as well)
@@ -62,7 +62,7 @@ STDOUT. Check the README there for more details and usages.
When present, the `rateLimits` option enables built-in rate limiting courtesy
of `connect-ratelimit`. Any of the options supported by that library can be
used and set in `config.js`.
used and set in `config.json`.
See the README for [connect-ratelimit](https://github.com/dharmafly/connect-ratelimit)
for more information!
@@ -97,7 +97,7 @@ for the key.
### File
To use file storage (the default) change the storage section in `config.js` to
To use file storage (the default) change the storage section in `config.json` to
something like:
``` json

View File

@@ -1,16 +1,10 @@
{
"host": "0.0.0.0",
"port": 7777,
"keyLength": 10,
"maxLength": 400000,
"staticMaxAge": 86400,
"recompressStaticAssets": true,
"logging": [
{
"level": "verbose",
@@ -18,11 +12,9 @@
"colorize": true
}
],
"keyGenerator": {
"type": "phonetic"
},
"rateLimits": {
"categories": {
"normal": {
@@ -31,13 +23,10 @@
}
}
},
"storage": {
"type": "file"
},
"documents": {
"about": "./about.md"
}
}

View File

@@ -7,6 +7,6 @@ services:
- STORAGE_HOST=memcached
- STORAGE_PORT=11211
ports:
- 7777:7777
- "7777:7777"
memcached:
image: memcached:latest

View File

@@ -1,108 +1,108 @@
const {
HOST,
PORT,
KEY_LENGTH,
MAX_LENGTH,
STATIC_MAX_AGE,
RECOMPRESS_STATIC_ASSETS,
STORAGE_TYPE,
STORAGE_HOST,
STORAGE_PORT,
STORAGE_EXPIRE_SECONDS,
STORAGE_DB,
STORAGE_AWS_BUCKET,
STORAGE_AWS_REGION,
STORAGE_PASSWORD,
STORAGE_USERNAME,
STORAGE_FILEPATH,
LOGGING_LEVEL,
LOGGING_TYPE,
LOGGING_COLORIZE,
KEYGENERATOR_TYPE,
KEY_GENERATOR_KEYSPACE,
RATE_LIMITS_NORMAL_TOTAL_REQUESTS,
RATE_LIMITS_NORMAL_EVERY_MILLISECONDS,
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS,
RATE_LIMITS_WHITELIST,
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS,
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS,
RATE_LIMITS_BLACKLIST,
DOCUMENTS,
HOST,
PORT,
KEY_LENGTH,
MAX_LENGTH,
STATIC_MAX_AGE,
RECOMPRESS_STATIC_ASSETS,
STORAGE_TYPE,
STORAGE_HOST,
STORAGE_PORT,
STORAGE_EXPIRE_SECONDS,
STORAGE_DB,
STORAGE_AWS_BUCKET,
STORAGE_AWS_REGION,
STORAGE_PASSWORD,
STORAGE_USERNAME,
STORAGE_FILEPATH,
LOGGING_LEVEL,
LOGGING_TYPE,
LOGGING_COLORIZE,
KEYGENERATOR_TYPE,
KEY_GENERATOR_KEYSPACE,
RATE_LIMITS_NORMAL_TOTAL_REQUESTS,
RATE_LIMITS_NORMAL_EVERY_MILLISECONDS,
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS,
RATE_LIMITS_WHITELIST,
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS,
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS,
RATE_LIMITS_BLACKLIST,
DOCUMENTS,
} = process.env;
const config = {
host: HOST,
port: Number(PORT),
host: HOST,
port: Number(PORT),
keyLength: Number(KEY_LENGTH),
keyLength: Number(KEY_LENGTH),
maxLength: Number(MAX_LENGTH),
maxLength: Number(MAX_LENGTH),
staticMaxAge: Number(STATIC_MAX_AGE),
staticMaxAge: Number(STATIC_MAX_AGE),
recompressStaticAssets: RECOMPRESS_STATIC_ASSETS,
recompressStaticAssets: RECOMPRESS_STATIC_ASSETS,
logging: [
{
level: LOGGING_LEVEL,
type: LOGGING_TYPE,
colorize: LOGGING_COLORIZE,
logging: [
{
level: LOGGING_LEVEL,
type: LOGGING_TYPE,
colorize: LOGGING_COLORIZE,
},
],
keyGenerator: {
type: KEYGENERATOR_TYPE,
keyspace: KEY_GENERATOR_KEYSPACE,
},
],
keyGenerator: {
type: KEYGENERATOR_TYPE,
keyspace: KEY_GENERATOR_KEYSPACE,
},
rateLimits: {
whitelist: RATE_LIMITS_WHITELIST ? RATE_LIMITS_WHITELIST.split(",") : [],
blacklist: RATE_LIMITS_BLACKLIST ? RATE_LIMITS_BLACKLIST.split(",") : [],
categories: {
normal: {
totalRequests: RATE_LIMITS_NORMAL_TOTAL_REQUESTS,
every: RATE_LIMITS_NORMAL_EVERY_MILLISECONDS,
},
whitelist:
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS ||
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS
? {
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
every: RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS,
}
: null,
blacklist:
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS ||
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS
? {
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
every: RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS,
}
: null,
rateLimits: {
whitelist: RATE_LIMITS_WHITELIST ? RATE_LIMITS_WHITELIST.split(",") : [],
blacklist: RATE_LIMITS_BLACKLIST ? RATE_LIMITS_BLACKLIST.split(",") : [],
categories: {
normal: {
totalRequests: RATE_LIMITS_NORMAL_TOTAL_REQUESTS,
every: RATE_LIMITS_NORMAL_EVERY_MILLISECONDS,
},
whitelist:
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS ||
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS
? {
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
every: RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS,
}
: null,
blacklist:
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS ||
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS
? {
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
every: RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS,
}
: null,
},
},
},
storage: {
type: STORAGE_TYPE,
host: STORAGE_HOST,
port: Number(STORAGE_PORT),
expire: Number(STORAGE_EXPIRE_SECONDS),
bucket: STORAGE_AWS_BUCKET,
region: STORAGE_AWS_REGION,
connectionUrl: `postgres://${STORAGE_USERNAME}:${STORAGE_PASSWORD}@${STORAGE_HOST}:${STORAGE_PORT}/${STORAGE_DB}`,
db: STORAGE_DB,
user: STORAGE_USERNAME,
password: STORAGE_PASSWORD,
path: STORAGE_FILEPATH,
},
storage: {
type: STORAGE_TYPE,
host: STORAGE_HOST,
port: Number(STORAGE_PORT),
expire: Number(STORAGE_EXPIRE_SECONDS),
bucket: STORAGE_AWS_BUCKET,
region: STORAGE_AWS_REGION,
connectionUrl: `postgres://${STORAGE_USERNAME}:${STORAGE_PASSWORD}@${STORAGE_HOST}:${STORAGE_PORT}/${STORAGE_DB}`,
db: STORAGE_DB,
user: STORAGE_USERNAME,
password: STORAGE_PASSWORD,
path: STORAGE_FILEPATH,
},
documents: DOCUMENTS
? DOCUMENTS.split(",").reduce((acc, item) => {
const keyAndValueArray = item.replace(/\s/g, "").split("=");
return { ...acc, [keyAndValueArray[0]]: keyAndValueArray[1] };
}, {})
: null,
documents: DOCUMENTS
? DOCUMENTS.split(",").reduce((acc, item) => {
const keyAndValueArray = item.replace(/\s/g, "").split("=");
return {...acc, [keyAndValueArray[0]]: keyAndValueArray[1]};
}, {})
: null,
};
console.log(JSON.stringify(config));

View File

@@ -4,6 +4,6 @@
set -e
node ./docker-entrypoint.js > ./config.js
node ./docker-entrypoint.js > ./config.json
exec "$@"

View File

@@ -1,155 +1,152 @@
var winston = require('winston');
var Busboy = require('busboy');
const winston = require('winston');
const Busboy = require('busboy');
// For handling serving stored documents
var DocumentHandler = function(options) {
if (!options) {
options = {};
}
this.keyLength = options.keyLength || DocumentHandler.defaultKeyLength;
this.maxLength = options.maxLength; // none by default
this.store = options.store;
this.keyGenerator = options.keyGenerator;
const DocumentHandler = function (options) {
if (!options) {
options = {};
}
this.keyLength = options.keyLength || DocumentHandler.defaultKeyLength;
this.maxLength = options.maxLength; // none by default
this.store = options.store;
this.keyGenerator = options.keyGenerator;
};
DocumentHandler.defaultKeyLength = 10;
// Handle retrieving a document
DocumentHandler.prototype.handleGet = function(request, response, config) {
const key = request.params.id.split('.')[0];
const skipExpire = !!config.documents[key];
DocumentHandler.prototype.handleGet = function (request, response, config) {
const key = request.params.id.split('.')[0];
const skipExpire = !!config.documents[key];
this.store.get(key, function(ret) {
if (ret) {
winston.verbose('retrieved document', { key: key });
response.writeHead(200, { 'content-type': 'application/json' });
if (request.method === 'HEAD') {
response.end();
} else {
response.end(JSON.stringify({ data: ret, key: key }));
}
}
else {
winston.warn('document not found', { key: key });
response.writeHead(404, { 'content-type': 'application/json' });
if (request.method === 'HEAD') {
response.end();
} else {
response.end(JSON.stringify({ message: 'Document not found.' }));
}
}
}, skipExpire);
this.store.get(key, function (ret) {
if (ret) {
winston.verbose('retrieved document', {key: key});
response.writeHead(200, {'content-type': 'application/json'});
if (request.method === 'HEAD') {
response.end();
} else {
response.end(JSON.stringify({data: ret, key: key}));
}
} else {
winston.warn('document not found', {key: key});
response.writeHead(404, {'content-type': 'application/json'});
if (request.method === 'HEAD') {
response.end();
} else {
response.end(JSON.stringify({message: 'Document not found.'}));
}
}
}, skipExpire);
};
// Handle retrieving the raw version of a document
DocumentHandler.prototype.handleRawGet = function(request, response, config) {
const key = request.params.id.split('.')[0];
const skipExpire = !!config.documents[key];
DocumentHandler.prototype.handleRawGet = function (request, response, config) {
const key = request.params.id.split('.')[0];
const skipExpire = !!config.documents[key];
this.store.get(key, function(ret) {
if (ret) {
winston.verbose('retrieved raw document', { key: key });
response.writeHead(200, { 'content-type': 'text/plain; charset=UTF-8' });
if (request.method === 'HEAD') {
response.end();
} else {
response.end(ret);
}
}
else {
winston.warn('raw document not found', { key: key });
response.writeHead(404, { 'content-type': 'application/json' });
if (request.method === 'HEAD') {
response.end();
} else {
response.end(JSON.stringify({ message: 'Document not found.' }));
}
}
}, skipExpire);
this.store.get(key, function (ret) {
if (ret) {
winston.verbose('retrieved raw document', {key: key});
response.writeHead(200, {'content-type': 'text/plain; charset=UTF-8'});
if (request.method === 'HEAD') {
response.end();
} else {
response.end(ret);
}
} else {
winston.warn('raw document not found', {key: key});
response.writeHead(404, {'content-type': 'application/json'});
if (request.method === 'HEAD') {
response.end();
} else {
response.end(JSON.stringify({message: 'Document not found.'}));
}
}
}, skipExpire);
};
// Handle adding a new Document
DocumentHandler.prototype.handlePost = function (request, response) {
var _this = this;
var buffer = '';
var cancelled = false;
const _this = this;
let buffer = '';
let cancelled = false;
// What to do when done
var onSuccess = function () {
// Check length
if (_this.maxLength && buffer.length > _this.maxLength) {
cancelled = true;
winston.warn('document >maxLength', { maxLength: _this.maxLength });
response.writeHead(400, { 'content-type': 'application/json' });
response.end(
JSON.stringify({ message: 'Document exceeds maximum length.' })
);
return;
// What to do when done
const onSuccess = function () {
// Check length
if (_this.maxLength && buffer.length > _this.maxLength) {
cancelled = true;
winston.warn('document >maxLength', {maxLength: _this.maxLength});
response.writeHead(400, {'content-type': 'application/json'});
response.end(JSON.stringify({message: 'Document exceeds maximum length.'}));
return;
}
// And then save if we should
_this.chooseKey(function (key) {
_this.store.set(key, buffer, function (res) {
if (res) {
winston.verbose('added document', {key: key});
response.writeHead(200, {'content-type': 'application/json'});
response.end(JSON.stringify({key: key}));
} else {
winston.verbose('error adding document');
response.writeHead(500, {'content-type': 'application/json'});
response.end(JSON.stringify({message: 'Error adding document.'}));
}
});
});
};
// If we should, parse a form to grab the data
const ct = request.headers['content-type'];
if (ct && ct.split(';')[0] === 'multipart/form-data') {
const busboy = new Busboy({headers: request.headers});
busboy.on('field', function (field_name, val) {
if (field_name === 'data') {
buffer = val;
}
});
busboy.on('finish', function () {
onSuccess();
});
request.pipe(busboy);
// Otherwise, use our own and just grab flat data from POST body
} else {
request.on('data', function (data) {
buffer += data.toString();
});
request.on('end', function () {
if (cancelled) {
return;
}
onSuccess();
});
request.on('error', function (error) {
winston.error('connection error: ' + error.message);
response.writeHead(500, {'content-type': 'application/json'});
response.end(JSON.stringify({message: 'Connection error.'}));
cancelled = true;
});
}
// And then save if we should
_this.chooseKey(function (key) {
_this.store.set(key, buffer, function (res) {
if (res) {
winston.verbose('added document', { key: key });
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ key: key }));
}
else {
winston.verbose('error adding document');
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ message: 'Error adding document.' }));
}
});
});
};
// If we should, parse a form to grab the data
var ct = request.headers['content-type'];
if (ct && ct.split(';')[0] === 'multipart/form-data') {
var busboy = new Busboy({ headers: request.headers });
busboy.on('field', function (fieldname, val) {
if (fieldname === 'data') {
buffer = val;
}
});
busboy.on('finish', function () {
onSuccess();
});
request.pipe(busboy);
// Otherwise, use our own and just grab flat data from POST body
} else {
request.on('data', function (data) {
buffer += data.toString();
});
request.on('end', function () {
if (cancelled) { return; }
onSuccess();
});
request.on('error', function (error) {
winston.error('connection error: ' + error.message);
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ message: 'Connection error.' }));
cancelled = true;
});
}
};
// Keep choosing keys until one isn't taken
DocumentHandler.prototype.chooseKey = function(callback) {
var key = this.acceptableKey();
var _this = this;
this.store.get(key, function(ret) {
if (ret) {
_this.chooseKey(callback);
} else {
callback(key);
}
}, true); // Don't bump expirations when key searching
DocumentHandler.prototype.chooseKey = function (callback) {
const key = this.acceptableKey();
const _this = this;
this.store.get(key, function (ret) {
if (ret) {
_this.chooseKey(callback);
} else {
callback(key);
}
}, true); // Don't bump expirations when key searching
};
DocumentHandler.prototype.acceptableKey = function() {
return this.keyGenerator.createKey(this.keyLength);
DocumentHandler.prototype.acceptableKey = function () {
return this.keyGenerator.createKey(this.keyLength);
};
module.exports = DocumentHandler;

View File

@@ -1,63 +1,61 @@
var fs = require('fs');
var crypto = require('crypto');
const fs = require('fs');
const crypto = require('crypto');
var winston = require('winston');
const winston = require('winston');
// For storing in files
// options[type] = file
// options[path] - Where to store
var FileDocumentStore = function(options) {
this.basePath = options.path || './data';
this.expire = options.expire;
const FileDocumentStore = function (options) {
this.basePath = options.path || './data';
this.expire = options.expire;
};
// Generate md5 of a string
FileDocumentStore.md5 = function(str) {
var md5sum = crypto.createHash('md5');
md5sum.update(str);
return md5sum.digest('hex');
FileDocumentStore.md5 = function (str) {
const md5sum = crypto.createHash('md5');
md5sum.update(str);
return md5sum.digest('hex');
};
// Save data in a file, key as md5 - since we don't know what we could
// be passed here
FileDocumentStore.prototype.set = function(key, data, callback, skipExpire) {
try {
var _this = this;
fs.mkdir(this.basePath, '700', function() {
var fn = _this.basePath + '/' + FileDocumentStore.md5(key);
fs.writeFile(fn, data, 'utf8', function(err) {
if (err) {
callback(false);
}
else {
callback(true);
if (_this.expire && !skipExpire) {
winston.warn('file store cannot set expirations on keys');
}
}
});
});
} catch(err) {
callback(false);
}
FileDocumentStore.prototype.set = function (key, data, callback, skipExpire) {
try {
const _this = this;
fs.mkdir(this.basePath, '700', function () {
const fn = _this.basePath + '/' + FileDocumentStore.md5(key);
fs.writeFile(fn, data, 'utf8', function (err) {
if (err) {
callback(false);
} else {
callback(true);
if (_this.expire && !skipExpire) {
winston.warn('file store cannot set expirations on keys');
}
}
});
});
} catch (err) {
callback(false);
}
};
// Get data from a file from key
FileDocumentStore.prototype.get = function(key, callback, skipExpire) {
var _this = this;
var fn = this.basePath + '/' + FileDocumentStore.md5(key);
fs.readFile(fn, 'utf8', function(err, data) {
if (err) {
callback(false);
}
else {
callback(data);
if (_this.expire && !skipExpire) {
winston.warn('file store cannot set expirations on keys');
}
}
});
FileDocumentStore.prototype.get = function (key, callback, skipExpire) {
const _this = this;
const fn = this.basePath + '/' + FileDocumentStore.md5(key);
fs.readFile(fn, 'utf8', function (err, data) {
if (err) {
callback(false);
} else {
callback(data);
if (_this.expire && !skipExpire) {
winston.warn('file store cannot set expirations on keys');
}
}
});
};
module.exports = FileDocumentStore;

2659
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "haste",
"version": "0.1.0",
"version": "0.1.0-rpardini",
"private": true,
"description": "Private Pastebin Server",
"keywords": [
@@ -14,19 +14,20 @@
},
"main": "haste",
"dependencies": {
"busboy": "0.2.4",
"connect": "^3.7.0",
"connect-ratelimit": "0.0.7",
"connect-route": "0.1.5",
"pg": "^8.0.0",
"redis": "0.8.1",
"redis-url": "0.1.0",
"st": "^2.0.0",
"uglify-js": "3.1.6",
"winston": "^2.0.0"
"busboy": "^1",
"connect": "^3",
"connect-ratelimit": "^0",
"connect-route": "^0",
"memcached": "^2",
"pg": "^8",
"redis": "^0",
"redis-url": "^0",
"st": "^2",
"uglify-js": "^3",
"winston": "^2"
},
"devDependencies": {
"mocha": "^8.1.3"
"mocha": "^10"
},
"bundledDependencies": [],
"bin": {

192
server.js
View File

@@ -1,162 +1,158 @@
var http = require('http');
var fs = require('fs');
const http = require('http');
const fs = require('fs');
var uglify = require('uglify-js');
var winston = require('winston');
var connect = require('connect');
var route = require('connect-route');
var connect_st = require('st');
var connect_rate_limit = require('connect-ratelimit');
const uglify = require('uglify-js');
const winston = require('winston');
const connect = require('connect');
const route = require('connect-route');
const connect_st = require('st');
const connect_rate_limit = require('connect-ratelimit');
var DocumentHandler = require('./lib/document_handler');
const DocumentHandler = require('./lib/document_handler');
// Load the configuration and set some defaults
const configPath = process.argv.length <= 2 ? 'config.js' : process.argv[2];
const configPath = process.argv.length <= 2 ? 'config.json' : process.argv[2];
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
config.port = process.env.PORT || config.port || 7777;
config.host = process.env.HOST || config.host || 'localhost';
// Set up the logger
if (config.logging) {
try {
winston.remove(winston.transports.Console);
} catch(e) {
/* was not present */
}
try {
winston.remove(winston.transports.Console);
} catch (e) {
/* was not present */
}
var detail, type;
for (var i = 0; i < config.logging.length; i++) {
detail = config.logging[i];
type = detail.type;
delete detail.type;
winston.add(winston.transports[type], detail);
}
let detail, type;
for (let i = 0; i < config.logging.length; i++) {
detail = config.logging[i];
type = detail.type;
delete detail.type;
winston.add(winston.transports[type], detail);
}
}
// build the store from the config on-demand - so that we don't load it
// for statics
if (!config.storage) {
config.storage = { type: 'file' };
config.storage = {type: 'file'};
}
if (!config.storage.type) {
config.storage.type = 'file';
config.storage.type = 'file';
}
var Store, preferredStore;
let Store, preferredStore;
if (process.env.REDISTOGO_URL && config.storage.type === 'redis') {
var redisClient = require('redis-url').connect(process.env.REDISTOGO_URL);
Store = require('./lib/document_stores/redis');
preferredStore = new Store(config.storage, redisClient);
}
else {
Store = require('./lib/document_stores/' + config.storage.type);
preferredStore = new Store(config.storage);
const redisClient = require('redis-url').connect(process.env.REDISTOGO_URL);
Store = require('./lib/document_stores/redis');
preferredStore = new Store(config.storage, redisClient);
} else {
Store = require('./lib/document_stores/' + config.storage.type);
preferredStore = new Store(config.storage);
}
winston.info('Using storage type: ' + config.storage.type);
// Compress the static javascript assets
if (config.recompressStaticAssets) {
var list = fs.readdirSync('./static');
for (var j = 0; j < list.length; j++) {
var item = list[j];
if ((item.indexOf('.js') === item.length - 3) && (item.indexOf('.min.js') === -1)) {
var dest = item.substring(0, item.length - 3) + '.min' + item.substring(item.length - 3);
var orig_code = fs.readFileSync('./static/' + item, 'utf8');
if (false && config.recompressStaticAssets) { // hack, don't do this shit
winston.info('Recompressing static assets');
const list = fs.readdirSync('./static');
list.forEach(item => {
if ((item.indexOf('.js') === item.length - 3) && (item.indexOf('.min.js') === -1)) {
const dest = item.substring(0, item.length - 3) + '.min' + item.substring(item.length - 3);
const orig_code = fs.readFileSync('./static/' + item, 'utf8');
fs.writeFileSync('./static/' + dest, uglify.minify(orig_code).code, 'utf8');
winston.info('compressed ' + item + ' into ' + dest);
}
}
fs.writeFileSync('./static/' + dest, uglify.minify(orig_code).code, 'utf8');
winston.info('compressed ' + item + ' into ' + dest);
}
});
} else {
winston.warn('Not recompressing static assets');
}
// Send the static documents into the preferred store, skipping expirations
var path, data;
for (var name in config.documents) {
path = config.documents[name];
data = fs.readFileSync(path, 'utf8');
winston.info('loading static document', { name: name, path: path });
if (data) {
preferredStore.set(name, data, function(cb) {
winston.debug('loaded static document', { success: cb });
}, true);
}
else {
winston.warn('failed to load static document', { name: name, path: path });
}
let path, data;
for (let name in config.documents) {
path = config.documents[name];
data = fs.readFileSync(path, 'utf8');
winston.info('loading static document', {name: name, path: path});
if (data) {
preferredStore.set(name, data, function (cb) {
winston.debug('loaded static document', {success: cb});
}, true);
} else {
winston.warn('failed to load static document', {name: name, path: path});
}
}
// Pick up a key generator
var pwOptions = config.keyGenerator || {};
const pwOptions = config.keyGenerator || {};
pwOptions.type = pwOptions.type || 'random';
var gen = require('./lib/key_generators/' + pwOptions.type);
var keyGenerator = new gen(pwOptions);
const gen = require('./lib/key_generators/' + pwOptions.type);
winston.info('Using key generator: ' + pwOptions.type);
const keyGenerator = new gen(pwOptions);
// Configure the document handler
var documentHandler = new DocumentHandler({
store: preferredStore,
maxLength: config.maxLength,
keyLength: config.keyLength,
keyGenerator: keyGenerator
const documentHandler = new DocumentHandler({
store: preferredStore, maxLength: config.maxLength, keyLength: config.keyLength, keyGenerator: keyGenerator
});
var app = connect();
const app = connect();
// Rate limit all requests
if (config.rateLimits) {
config.rateLimits.end = true;
app.use(connect_rate_limit(config.rateLimits));
config.rateLimits.end = true;
app.use(connect_rate_limit(config.rateLimits));
}
// first look at API calls
app.use(route(function(router) {
// get raw documents - support getting with extension
app.use(route(function (router) {
// get raw documents - support getting with extension
router.get('/raw/:id', function(request, response) {
return documentHandler.handleRawGet(request, response, config);
});
router.get('/raw/:id', function (request, response) {
return documentHandler.handleRawGet(request, response, config);
});
router.head('/raw/:id', function(request, response) {
return documentHandler.handleRawGet(request, response, config);
});
router.head('/raw/:id', function (request, response) {
return documentHandler.handleRawGet(request, response, config);
});
// add documents
// add documents
router.post('/documents', function(request, response) {
return documentHandler.handlePost(request, response);
});
router.post('/documents', function (request, response) {
return documentHandler.handlePost(request, response);
});
// get documents
router.get('/documents/:id', function(request, response) {
return documentHandler.handleGet(request, response, config);
});
// get documents
router.get('/documents/:id', function (request, response) {
return documentHandler.handleGet(request, response, config);
});
router.head('/documents/:id', function(request, response) {
return documentHandler.handleGet(request, response, config);
});
router.head('/documents/:id', function (request, response) {
return documentHandler.handleGet(request, response, config);
});
}));
// Otherwise, try to match static files
app.use(connect_st({
path: __dirname + '/static',
content: { maxAge: config.staticMaxAge },
passthrough: true,
index: false
path: __dirname + '/static', content: {maxAge: config.staticMaxAge}, passthrough: true, index: false
}));
// Then we can loop back - and everything else should be a token,
// so route it back to /
app.use(route(function(router) {
router.get('/:id', function(request, response, next) {
request.sturl = '/';
next();
});
app.use(route(function (router) {
router.get('/:id', function (request, response, next) {
winston.info("wtf, redirect?");
request.sturl = '/';
next();
});
}));
// And match index
app.use(connect_st({
path: __dirname + '/static',
content: { maxAge: config.staticMaxAge },
index: 'index.html'
path: __dirname + '/static', content: {maxAge: config.staticMaxAge}, index: 'index.html'
}));
http.createServer(app).listen(config.port, config.host);

View File

@@ -1,175 +1,219 @@
body {
background: #002B36;
padding: 20px 50px;
margin: 0px;
background: #0d1117;
padding: 20px 50px;
margin: 0;
}
/* textarea */
textarea {
background: transparent;
border: 0px;
color: #fff;
padding: 0px;
width: 100%;
height: 100%;
font-family: monospace;
outline: none;
resize: none;
font-size: 13px;
margin-top: 0;
margin-bottom: 0;
background: transparent;
border: 0;
color: #fff;
padding: 0;
width: 100%;
height: 100%;
font-family: monospace;
outline: none;
resize: none;
font-size: 13px;
margin-top: 0;
margin-bottom: 0;
}
/* the line numbers */
#linenos {
color: #7d7d7d;
z-index: -1000;
position: absolute;
top: 20px;
left: 0px;
width: 30px; /* 30 to get 20 away from box */
font-size: 13px;
font-family: monospace;
text-align: right;
user-select: none;
color: #7d7d7d;
z-index: -1000;
position: absolute;
top: 20px;
left: 0;
width: 30px; /* 30 to get 20 away from box */
font-size: 13px;
font-family: monospace;
text-align: right;
user-select: none;
}
/* code box when locked */
#box {
padding: 0px;
margin: 0px;
width: 100%;
border: 0px;
outline: none;
font-size: 13px;
overflow: inherit;
padding: 0;
margin: 0;
width: 100%;
border: 0;
outline: none;
font-size: 13px;
overflow: inherit;
}
#box code {
padding: 0px;
background: transparent !important; /* don't hide hastebox */
padding: 0;
background: transparent !important; /* don't hide hastebox */
}
/* key */
#key {
position: fixed;
top: 0px;
right: 0px;
z-index: +1000; /* watch out */
position: fixed;
top: 0;
right: 0;
z-index: +1000; /* watch out */
}
#box1 {
padding: 5px;
text-align: center;
background: #00222b;
padding: 5px;
text-align: center;
background: #00222b;
}
#box2 {
background: #08323c;
font-size: 0px;
padding: 0px 5px;
background: #08323c;
font-size: 0;
padding: 0 5px;
}
#box1 a.logo, #box1 a.logo:visited {
display: inline-block;
background: url(logo.png);
width: 126px;
height: 42px;
display: inline-block;
background: url(logo.png);
width: 126px;
height: 42px;
}
#box1 a.logo:hover {
background-position: 0 bottom;
background-position: 0 bottom;
}
#box2 .function {
background: url(function-icons.png);
width: 32px;
height: 37px;
display: inline-block;
position: relative;
background: url(function-icons.png);
width: 32px;
height: 37px;
display: inline-block;
position: relative;
}
#box2 .link embed {
vertical-align: bottom; /* fix for zeroClipboard style */
vertical-align: bottom; /* fix for zeroClipboard style */
}
#box2 .function.enabled:hover {
cursor: hand;
cursor: pointer;
/* cursor: hand; */
cursor: pointer;
}
#pointer {
display: block;
height: 5px;
width: 10px;
background: url(hover-dropdown-tip.png);
bottom: 0px;
position: absolute;
margin: auto;
left: 0px;
right: 0px;
display: block;
height: 5px;
width: 10px;
background: url(hover-dropdown-tip.png);
bottom: 0;
position: absolute;
margin: auto;
left: 0;
right: 0;
}
#box3, #messages li {
background: #173e48;
font-family: Helvetica, sans-serif;
font-size: 12px;
line-height: 14px;
padding: 10px 15px;
user-select: none;
background: #173e48;
font-family: Helvetica, sans-serif;
font-size: 12px;
line-height: 14px;
padding: 10px 15px;
user-select: none;
}
#box3 .label, #messages li {
color: #fff;
font-weight: bold;
color: #fff;
font-weight: bold;
}
#box3 .shortcut {
color: #c4dce3;
font-weight: normal;
color: #c4dce3;
font-weight: normal;
}
#box2 .function.save { background-position: -5px top; }
#box2 .function.enabled.save { background-position: -5px center; }
#box2 .function.enabled.save:hover { background-position: -5px bottom; }
#box2 .function.save {
background-position: -5px top;
}
#box2 .function.new { background-position: -42px top; }
#box2 .function.enabled.new { background-position: -42px center; }
#box2 .function.enabled.new:hover { background-position: -42px bottom; }
#box2 .function.enabled.save {
background-position: -5px center;
}
#box2 .function.duplicate { background-position: -79px top; }
#box2 .function.enabled.duplicate { background-position: -79px center; }
#box2 .function.enabled.duplicate:hover { background-position: -79px bottom; }
#box2 .function.enabled.save:hover {
background-position: -5px bottom;
}
#box2 .function.raw { background-position: -116px top; }
#box2 .function.enabled.raw { background-position: -116px center; }
#box2 .function.enabled.raw:hover { background-position: -116px bottom; }
#box2 .function.new {
background-position: -42px top;
}
#box2 .function.twitter { background-position: -153px top; }
#box2 .function.enabled.twitter { background-position: -153px center; }
#box2 .function.enabled.twitter:hover { background-position: -153px bottom; }
#box2 .button-picture{ border-width: 0; font-size: inherit; }
#box2 .function.enabled.new {
background-position: -42px center;
}
#box2 .function.enabled.new:hover {
background-position: -42px bottom;
}
#box2 .function.duplicate {
background-position: -79px top;
}
#box2 .function.enabled.duplicate {
background-position: -79px center;
}
#box2 .function.enabled.duplicate:hover {
background-position: -79px bottom;
}
#box2 .function.raw {
background-position: -116px top;
}
#box2 .function.enabled.raw {
background-position: -116px center;
}
#box2 .function.enabled.raw:hover {
background-position: -116px bottom;
}
#box2 .function.twitter {
background-position: -153px top;
}
#box2 .function.enabled.twitter {
background-position: -153px center;
}
#box2 .function.enabled.twitter:hover {
background-position: -153px bottom;
}
#box2 .button-picture {
border-width: 0;
font-size: inherit;
}
#messages {
position:fixed;
top:0px;
right:138px;
margin:0;
padding:0;
width:400px;
position: fixed;
top: 0;
right: 138px;
margin: 0;
padding: 0;
width: 400px;
}
#messages li {
background:rgba(23,62,72,0.8);
margin:0 auto;
list-style:none;
background: rgba(23, 62, 72, 0.8);
margin: 0 auto;
list-style: none;
}
#messages li.error {
background:rgba(102,8,0,0.8);
background: rgba(102, 8, 0, 0.8);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,68 +1,86 @@
<html>
<html lang="en">
<head>
<head>
<title>hastebin</title>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="solarized_dark.css"/>
<link rel="stylesheet" type="text/css" href="application.css"/>
<title>hastebin</title>
<meta charset="utf-8"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css"
integrity="sha512-rO+olRTkcf304DQBxSWxln8JXCzTHlKnIdnMUwYvQa9/Jd4cQaNkItIUj6Z4nvW1dqK0SKXLbn9h4KwZTNtAyw=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
<link rel="stylesheet" type="text/css" href="application.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"
integrity="sha512-jGsMH83oKe9asCpkOVkBnUrDDTp8wl+adkB2D+//JtlxO4SrLoJdhbOysIFQJloQFD+C4Fl1rMsQZF76JjV0eQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="highlight.min.js"></script>
<script type="text/javascript" src="application.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"
integrity="sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<meta name="robots" content="noindex,nofollow"/>
<!--<script type="text/javascript" src="application.min.js"></script>-->
<script type="text/javascript">
var app = null;
// Handle pops
var handlePop = function(evt) {
var path = evt.target.location.pathname;
if (path === '/') { app.newDocument(true); }
else { app.loadDocument(path.substring(1, path.length)); }
};
// Set up the pop state to handle loads, skipping the first load
// to make chrome behave like others:
// http://code.google.com/p/chromium/issues/detail?id=63040
setTimeout(function() {
window.onpopstate = function(evt) {
try { handlePop(evt); } catch(err) { /* not loaded yet */ }
};
}, 1000);
// Construct app and load initial path
$(function() {
app = new haste('hastebin', { twitter: true });
handlePop({ target: window });
});
</script>
</head>
<script type="text/javascript" src="application.js"></script>
<body>
<ul id="messages"></ul>
<meta name="robots" content="noindex,nofollow"/>
<div id="key">
<div id="pointer" style="display:none;"></div>
<div id="box1">
<a href="/about.md" class="logo"></a>
</div>
<div id="box2">
<button class="save function button-picture">Save</button>
<button class="new function button-picture">New</button>
<button class="duplicate function button-picture">Duplicate & Edit</button>
<button class="raw function button-picture">Just Text</button>
<button class="twitter function button-picture">Twitter</button>
</div>
<div id="box3" style="display:none;">
<div class="label"></div>
<div class="shortcut"></div>
</div>
</div>
<script type="text/javascript">
var app = null;
// Handle pops
var handlePop = function (evt) {
var path = evt.target.location.pathname;
if (path === '/') {
app.newDocument(true);
} else {
app.loadDocument(path.substring(1, path.length));
}
};
// Set up the pop state to handle loads, skipping the first load
// to make chrome behave like others:
// http://code.google.com/p/chromium/issues/detail?id=63040
setTimeout(function () {
window.onpopstate = function (evt) {
try {
handlePop(evt);
} catch (err) { /* not loaded yet */
}
};
}, 1000);
// Construct app and load initial path
$(function () {
app = new haste('hastebin', {twitter: false});
handlePop({target: window});
});
</script>
<div id="linenos"></div>
<pre id="box" style="display:none;" class="hljs" tabindex="0"><code></code></pre>
<textarea spellcheck="false" style="display:none;"></textarea>
</head>
</body>
<body>
<ul id="messages"></ul>
<div id="key">
<div id="pointer" style="display:none;"></div>
<div id="box1">
<!--suppress HtmlUnknownTarget -->
<a href="/about.md" class="logo"></a>
</div>
<div id="box2">
<button class="save function button-picture">Save</button>
<button class="new function button-picture">New</button>
<button class="duplicate function button-picture">Duplicate & Edit</button>
<button class="raw function button-picture">Just Text</button>
<button class="twitter function button-picture">Twitter</button>
</div>
<div id="box3" style="display:none;">
<div class="label"></div>
<div class="shortcut"></div>
</div>
</div>
<div id="linenos"></div>
<pre id="box" style="display:none;" class="hljs" tabindex="0"><code></code></pre>
<!--suppress HtmlFormInputWithoutLabel -->
<textarea spellcheck="false" style="display:none;"></textarea>
</body>
</html>

View File

@@ -1,84 +0,0 @@
/*
Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull <sourdrums@gmail.com>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #002b36;
color: #839496;
}
.hljs-comment,
.hljs-quote {
color: #586e75;
}
/* Solarized Green */
.hljs-keyword,
.hljs-selector-tag,
.hljs-addition {
color: #859900;
}
/* Solarized Cyan */
.hljs-number,
.hljs-string,
.hljs-meta .hljs-meta-string,
.hljs-literal,
.hljs-doctag,
.hljs-regexp {
color: #2aa198;
}
/* Solarized Blue */
.hljs-title,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #268bd2;
}
/* Solarized Yellow */
.hljs-attribute,
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-class .hljs-title,
.hljs-type {
color: #b58900;
}
/* Solarized Orange */
.hljs-symbol,
.hljs-bullet,
.hljs-subst,
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-link {
color: #cb4b16;
}
/* Solarized Red */
.hljs-built_in,
.hljs-deletion {
color: #dc322f;
}
.hljs-formula {
background: #073642;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}