add some fancy magic :kekw:

This commit is contained in:
2025-10-13 18:21:23 +02:00
parent 2e59c1f5e7
commit 22badaf535
15 changed files with 1328 additions and 786 deletions

164
.gitignore vendored
View File

@@ -1,22 +1,148 @@
# IDE (IntelliJ IDEA) # Created by https://www.toptal.com/developers/gitignore/api/node
.idea # Edit at https://www.toptal.com/developers/gitignore?templates=node
HastebinPlus.iml
# IDE (Microsoft Visual Studio) ### Node ###
obj/ # Logs
Microsoft.NodejsTools.WebRole.dll logs
.ntvs_analysis.dat *.log
*.njsproj npm-debug.log*
*.sln yarn-debug.log*
*.suo yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Application # Diagnostic reports (https://nodejs.org/api/report.html)
data report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
node_modules
*.min.css
*.min.js
!jquery.min.js
!highlight.min.js
# NodeJS # Runtime data
npm-debug.log pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node
data/*

View File

@@ -1,6 +1,6 @@
# Hastebin Plus # unknownBIN
Hastebin Plus is an open-source Pastebin software written in node.js, which is easily installable in any network. unknownBIN is a secure and modern open-source Pastebin software written in node.js.
It bases upon [haste](https://github.com/seejohnrun/haste-server) and got enhanced in matters of **Design, Speed and Simplicity**. It is a fork of the original Hastebin and Hastebin Plus, modernized for security and performance.
## Features ## Features
* Paste code, logs and ... almost everything! * Paste code, logs and ... almost everything!
@@ -8,19 +8,23 @@ It bases upon [haste](https://github.com/seejohnrun/haste-server) and got enhanc
* Add static documents * Add static documents
* Duplicate & edit pastes * Duplicate & edit pastes
* Raw paste-view * Raw paste-view
* Secure, unpredictable paste IDs
* Modernized backend with security enhancements
## Installation ## Installation
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/MarvinMenzerath/HastebinPlus) 1. Install Git and node.js (a recent LTS version is recommended).
2. Clone this repository: `git clone https://github.com/MrUnknownDE/unknownbin.git unknownbin`
1. Install Git and node.js: `sudo apt-get install git nodejs` 3. Change into the directory: `cd unknownbin`
2. Clone this repository: `git clone https://github.com/MarvinMenzerath/HastebinPlus.git hastebin-plus`
3. Open `config.json` and change the settings (if you want to)
4. Install dependencies: `npm install` 4. Install dependencies: `npm install`
5. Start the application: `npm start` 5. Build static assets: `npm run build`
6. Open `config.json` and change the settings (if you want to).
7. Start the application: `npm start`
## Update ## Update
1. Pull changes from this repository: `git pull` 1. Pull changes from the repository: `git pull`
2. Install new dependencies: `npm install` 2. Install new dependencies: `npm install`
3. Re-build static assets: `npm run build`
4. Restart the application.
## Settings ## Settings
| Key | Description | Default value | | Key | Description | Default value |
@@ -30,7 +34,7 @@ It bases upon [haste](https://github.com/seejohnrun/haste-server) and got enhanc
| `dataPath` | The directory where all pastes are stored | `./data` | | `dataPath` | The directory where all pastes are stored | `./data` |
| `keyLength` | The length of the pastes' key | `10` | | `keyLength` | The length of the pastes' key | `10` |
| `maxLength` | Maximum chars in a paste | `500000` | | `maxLength` | Maximum chars in a paste | `500000` |
| `createKey` | Needs to be in front of paste to allow creation | ` ` | | `createKey` | Needs to be in front of paste to allow creation | `""` |
| `documents` | Static documents to serve | See below | | `documents` | Static documents to serve | See below |
### Default Config ### Default Config
@@ -43,23 +47,6 @@ It bases upon [haste](https://github.com/seejohnrun/haste-server) and got enhanc
"maxLength": 500000, "maxLength": 500000,
"createKey": "", "createKey": "",
"documents": { "documents": {
"about": "./README.md", "about": "./README.md"
"javaTest": "./documents/test.java"
} }
} }
```
## Authors
* [haste](https://github.com/seejohnrun/haste-server): John Crepezzi - MIT License
* [jQuery](https://github.com/jquery/jquery): MIT License
* [highlight.js](https://github.com/isagalaev/highlight.js): Ivan Sagalaev - [License](https://github.com/isagalaev/highlight.js/blob/master/LICENSE)
* [Application Icon](https://www.iconfinder.com/icons/285631/notepad_icon): [Paomedia](https://www.iconfinder.com/paomedia) - [CC BY 3.0 License](http://creativecommons.org/licenses/by/3.0/)
## License
Copyright (c) 2014-2016 Marvin Menzerath
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

41
build.js Normal file
View File

@@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
const UglifyJS = require('uglify-js');
const CleanCSS = require('clean-css');
console.log('Starting build process...');
const staticDir = path.join(__dirname, 'static');
const files = fs.readdirSync(staticDir);
files.forEach(item => {
const fullPath = path.join(staticDir, item);
let destPath;
if (item.endsWith('.css') && !item.endsWith('.min.css')) {
destPath = path.join(staticDir, item.replace('.css', '.min.css'));
try {
const source = fs.readFileSync(fullPath, 'utf8');
const result = new CleanCSS().minify(source);
fs.writeFileSync(destPath, result.styles, 'utf8');
console.log(`Compressed CSS: ${item} -> ${path.basename(destPath)}`);
} catch (err) {
console.error(`Error compressing ${item}:`, err);
}
} else if (item.endsWith('.js') && !item.endsWith('.min.js')) {
destPath = path.join(staticDir, item.replace('.js', '.min.js'));
try {
const source = fs.readFileSync(fullPath, 'utf8');
const result = UglifyJS.minify(source);
if (result.error) {
throw result.error;
}
fs.writeFileSync(destPath, result.code, 'utf8');
console.log(`Compressed JS: ${item} -> ${path.basename(destPath)}`);
} catch (err) {
console.error(`Error compressing ${item}:`, err);
}
}
});
console.log('Build process finished.');

View File

@@ -1,118 +1,120 @@
var logger = require('winston'); const KeyGenerator = require('./key_generator.js');
var KeyGenerator = require('./key_generator.js');
// handles creating new and requesting existing documents // handles creating new and requesting existing documents
var DocumentHandler = function(options) { class DocumentHandler {
if (!options) { constructor(options) {
options = {}; if (!options) {
} options = {};
this.store = options.store; }
this.maxLength = options.maxLength || 50000; this.store = options.store;
this.keyLength = options.keyLength || 10; this.logger = options.logger;
this.createKey = options.createKey || ''; this.maxLength = options.maxLength || 50000;
this.keyGenerator = new KeyGenerator(); this.keyLength = options.keyLength || 10;
this.createKey = options.createKey || '';
this.keyGenerator = new KeyGenerator();
if (this.createKey !== '') { if (this.createKey !== '') {
logger.info("Creation-Key:", this.createKey); this.logger.info("Creation-Key is configured.");
} }
}; }
// handles existing documents // handles existing documents
DocumentHandler.prototype.handleGet = function(key, res) { handleGet(key, res) {
this.store.get(key, function(ret) { this.store.get(key, (ret) => {
if (ret) { if (ret) {
logger.verbose('Open paste:', key); this.logger.verbose(`Open paste: ${key}`);
res.writeHead(200, {'content-type': 'application/json'}); res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({key: key, data: ret.replace(/\t/g, ' ')})); res.end(JSON.stringify({ key: key, data: ret.replace(/\t/g, ' ') }));
} else { } else {
logger.verbose('Paste not found:', key); this.logger.verbose(`Paste not found: ${key}`);
res.writeHead(404, {'content-type': 'application/json'}); res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({message: 'Paste not found.'})); res.end(JSON.stringify({ message: 'Paste not found.' }));
} }
}); });
}; }
// handles exisiting documents (raw) // handles existing documents (raw)
DocumentHandler.prototype.handleRawGet = function(key, res) { handleRawGet(key, res) {
this.store.get(key, function(ret) { this.store.get(key, (ret) => {
if (ret) { if (ret) {
logger.verbose('Open paste:', key); this.logger.verbose(`Open raw paste: ${key}`);
res.writeHead(200, {'content-type': 'text/plain'}); res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
res.end(ret); res.end(ret);
} else { } else {
logger.verbose('Paste not found:', key); this.logger.verbose(`Paste not found: ${key}`);
res.writeHead(404, {'content-type': 'application/json'}); res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({message: 'Paste not found.'})); res.end(JSON.stringify({ message: 'Paste not found.' }));
} }
}); });
}; }
// handles creating new documents // handles creating new documents
DocumentHandler.prototype.handlePost = function(req, res) { handlePost(req, res) {
var _this = this; let buffer = '';
var buffer = ''; let cancelled = false;
var cancelled = false;
req.on('data', function(data) {
if (cancelled) return;
buffer += data.toString();
if (_this.maxLength && buffer.length > _this.maxLength) {
cancelled = true;
logger.warn('Paste exeeds maximum length.');
res.writeHead(400, {'content-type': 'application/json'});
res.end(JSON.stringify({message: 'Paste exceeds maximum length.'}));
}
});
req.on('end', function() {
if (cancelled) return;
if (_this.createKey !== '') { req.on('data', (data) => {
if (!buffer.startsWith(_this.createKey)) { if (cancelled) return;
logger.warn('Error adding new paste: wrong key'); buffer += data.toString();
res.writeHead(400, {'content-type': 'application/json'}); if (this.maxLength && buffer.length > this.maxLength) {
res.end(JSON.stringify({message: 'Error adding new paste: wrong key'})); cancelled = true;
return; this.logger.warn('Paste exceeds maximum length.');
} res.writeHead(400, { 'content-type': 'application/json' });
buffer = buffer.substring(_this.createKey.length); res.end(JSON.stringify({ message: 'Paste exceeds maximum length.' }));
} }
});
_this.chooseKey(function(key) { req.on('end', () => {
_this.store.set(key, buffer, function(success) { if (cancelled) return;
if (success) {
logger.verbose('New paste:', key);
res.writeHead(200, {'content-type': 'application/json'});
res.end(JSON.stringify({key: key}));
} else {
logger.warn('Error adding new paste.');
res.writeHead(500, {'content-type': 'application/json'});
res.end(JSON.stringify({message: 'Error adding new paste.'}));
}
});
});
});
req.on('error', function(error) {
logger.error('Connection error: ' + error.message);
res.writeHead(500, {'content-type': 'application/json'});
res.end(JSON.stringify({message: 'Connection error.'}));
});
};
// creates new keys until one is not taken if (this.createKey !== '') {
DocumentHandler.prototype.chooseKey = function(callback) { if (!buffer.startsWith(this.createKey)) {
var key = this.acceptableKey(); this.logger.warn('Error adding new paste: wrong key');
var _this = this; res.writeHead(403, { 'content-type': 'application/json' });
this.store.get(key, function(success) { res.end(JSON.stringify({ message: 'Error adding new paste: wrong key' }));
if (success) { return;
_this.chooseKey(callback); }
} else { buffer = buffer.substring(this.createKey.length);
callback(key); }
}
});
};
// creates a new key using the key-generator this.chooseKey((key) => {
DocumentHandler.prototype.acceptableKey = function() { this.store.set(key, buffer, (success) => {
return this.keyGenerator.createKey(this.keyLength); if (success) {
}; this.logger.verbose(`New paste: ${key}`);
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ key: key }));
} else {
this.logger.warn('Error adding new paste.');
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Error adding new paste.' }));
}
});
});
});
module.exports = DocumentHandler; req.on('error', (error) => {
this.logger.error(`Connection error: ${error.message}`);
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Connection error.' }));
});
}
// creates new keys until one is not taken
chooseKey(callback) {
const key = this.acceptableKey();
this.store.get(key, (success) => {
if (success) {
this.chooseKey(callback);
} else {
callback(key);
}
});
}
// creates a new key using the key-generator
acceptableKey() {
return this.keyGenerator.createKey(this.keyLength);
}
}
module.exports = DocumentHandler;

View File

@@ -1,38 +1,75 @@
var fs = require('fs'); const fs = require('fs');
var crypto = require('crypto'); const path = require('path');
var logger = require('winston'); // A simple key regex to prevent path traversal
const validKeyRegex = /^[a-zA-Z0-9]+$/;
// handles saving and retrieving all documents // handles saving and retrieving all documents
var FileDocumentStore = function(options) { class FileDocumentStore {
this.basePath = options.path || './data'; constructor(options) {
logger.info('Path to data: ' + this.basePath); this.basePath = options.path || './data';
}; this.logger = options.logger;
this.logger.info('Path to data: ' + this.basePath);
// Create directory if it does not exist
if (!fs.existsSync(this.basePath)) {
fs.mkdirSync(this.basePath, { recursive: true, mode: '700' });
}
}
// saves a new file to the filesystem // saves a new file to the filesystem
FileDocumentStore.prototype.set = function(key, data, callback) { set(key, data, callback) {
var _this = this; if (!validKeyRegex.test(key)) {
fs.mkdir(this.basePath, '700', function(err) { this.logger.warn(`Invalid key provided for set: ${key}`);
fs.writeFile(_this.basePath + '/' + key, data, 'utf8', function(err) { return callback(false);
if (err) { }
callback(false);
} else {
callback(true);
}
});
});
};
// gets an exisiting file from the filesystem const resolvedBasePath = path.resolve(this.basePath);
FileDocumentStore.prototype.get = function(key, callback) { const filePath = path.resolve(resolvedBasePath, key);
var _this = this;
fs.readFile(this.basePath + '/' + key, 'utf8', function(err, data) {
if (err) {
callback(false);
} else {
callback(data);
}
});
};
module.exports = FileDocumentStore; // Security check to ensure we are not writing outside of the data directory
if (path.dirname(filePath) !== resolvedBasePath) {
this.logger.error(`Path traversal attempt detected for key: ${key}`);
return callback(false);
}
fs.writeFile(filePath, data, 'utf8', (err) => {
if (err) {
this.logger.error('Error writing file:', err);
callback(false);
} else {
callback(true);
}
});
}
// gets an existing file from the filesystem
get(key, callback) {
if (!validKeyRegex.test(key)) {
this.logger.warn(`Invalid key provided for get: ${key}`);
return callback(false);
}
const resolvedBasePath = path.resolve(this.basePath);
const filePath = path.resolve(resolvedBasePath, key);
// Security check to ensure we are not reading outside of the data directory
if (path.dirname(filePath) !== resolvedBasePath) {
this.logger.error(`Path traversal attempt detected for key: ${key}`);
return callback(false);
}
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
// Don't log error if file just doesn't exist
if (err.code !== 'ENOENT') {
this.logger.error('Error reading file:', err);
}
callback(false);
} else {
callback(data);
}
});
}
}
module.exports = FileDocumentStore;

View File

@@ -1,15 +1,18 @@
var KeyGenerator = function() { const crypto = require('crypto');
this.keyspace = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
};
KeyGenerator.prototype.createKey = function(keyLength) { class KeyGenerator {
var key = ''; constructor() {
var index; this.keyspace = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (var i = 0; i < keyLength; i++) { }
index = Math.floor(Math.random() * this.keyspace.length);
key += this.keyspace.charAt(index);
}
return key;
};
module.exports = KeyGenerator; createKey(keyLength) {
const buffer = crypto.randomBytes(keyLength);
let key = '';
for (let i = 0; i < buffer.length; i++) {
key += this.keyspace.charAt(buffer[i] % this.keyspace.length);
}
return key;
}
}
module.exports = KeyGenerator;

1122
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,8 @@
{ {
"name": "unknownBIN", "name": "unknownBIN",
"version": "1.0.0", "version": "2.0.0",
"private": true, "private": true,
"description": "unknownBIN is an open-source Pastebin software written in node.js.", "description": "A secure and modern open-source Pastebin software written in node.js.",
"keywords": [
"paste",
"pastebin",
"haste",
"hastebin",
"code",
"syntax highlighting"
],
"author": { "author": {
"name": "MrUnknownDE", "name": "MrUnknownDE",
"email": "me@mrunk.de", "email": "me@mrunk.de",
@@ -18,12 +10,14 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"clean-css": "~3.4.19", "clean-css": "^5.3.3",
"express": "~4.14.0", "express": "^4.19.2",
"uglify-js": "~2.7.0", "helmet": "^7.1.0",
"winston": "~2.2.0" "uglify-js": "^3.17.4",
"winston": "^3.13.0"
}, },
"scripts": { "scripts": {
"start": "node ./server.js" "start": "node ./server.js",
"build": "node ./build.js"
} }
} }

134
server.js
View File

@@ -1,81 +1,97 @@
var http = require('http'); const fs = require('fs');
var url = require('url'); const path = require('path');
var fs = require('fs');
var express = require('express'); const express = require('express');
var logger = require('winston'); const helmet = require('helmet');
const winston = require('winston');
var DocumentHandler = require('./lib/document_handler.js'); const DocumentHandler = require('./lib/document_handler.js');
var FileStorage = require('./lib/file_storage.js'); const FileStorage = require('./lib/file_storage.js');
// load configuration // load configuration
var config = JSON.parse(fs.readFileSync(__dirname + '/config.json', 'utf8')); let config;
try {
config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8'));
} catch (err) {
console.error('Error reading config.json:', err);
process.exit(1);
}
config.port = process.env.PORT || config.port || 8080; config.port = process.env.PORT || config.port || 8080;
config.host = process.env.HOST || config.host || 'localhost'; config.host = process.env.HOST || config.host || '0.0.0.0';
// logger-setup // logger-setup
logger.remove(logger.transports.Console); const logger = winston.createLogger({
logger.add(logger.transports.Console, {colorize: true, level: 'verbose'}); level: 'verbose',
logger.info('Welcome to Hastebin Plus!'); format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
transports: [
new winston.transports.Console()
]
});
logger.info('Welcome to unknownBIN!');
// init file-storage // init file-storage
var fileStorage = new FileStorage(config.dataPath); const fileStorage = new FileStorage({ path: config.dataPath, logger: logger });
// load static documents into file-storage // load static documents into file-storage
for (var name in config.documents) { if (config.documents) {
var path = config.documents[name]; for (const name in config.documents) {
var data = fs.readFileSync(path, 'utf8'); const docPath = config.documents[name];
if (data) { try {
fileStorage.set(name, data, function(success) {}); const data = fs.readFileSync(docPath, 'utf8');
logger.verbose('Created document: ' + name + " ==> " + path); fileStorage.set(name, data, (success) => {
} else { if (success) {
logger.warn('Unable to find document: ' + name + " ==> " + path); logger.verbose(`Loaded document: ${name} from ${docPath}`);
} } else {
logger.warn(`Failed to store document: ${name}`);
}
});
} catch (err) {
logger.warn(`Unable to find or read document: ${name} at ${docPath}`);
}
}
} }
// configure the document handler // configure the document handler
var documentHandler = new DocumentHandler({ const documentHandler = new DocumentHandler({
store: fileStorage, store: fileStorage,
maxLength: config.maxLength, maxLength: config.maxLength,
keyLength: config.keyLength, keyLength: config.keyLength,
createKey: config.createKey createKey: config.createKey,
logger: logger
}); });
// compress static assets
var cssCompressor = require('clean-css');
var jsCompressor = require('uglify-js');
var files = fs.readdirSync(__dirname + '/static');
for (var i = 0; i < files.length; i++) {
var item = files[i];
var dest = "";
if ((item.indexOf('.css') === item.length - 4) && (item.indexOf('.min.css') === -1)) {
dest = item.substring(0, item.length - 4) + '.min.css';
fs.writeFileSync(__dirname + '/static/' + dest, new cssCompressor().minify(fs.readFileSync(__dirname + '/static/' + item, 'utf8')).styles, 'utf8');
logger.verbose('Compressed: ' + item + ' ==> ' + dest);
} else if ((item.indexOf('.js') === item.length - 3) && (item.indexOf('.min.js') === -1)) {
dest = item.substring(0, item.length - 3) + '.min.js';
fs.writeFileSync(__dirname + '/static/' + dest, jsCompressor.minify(__dirname + '/static/' + item).code, 'utf8');
logger.verbose('Compressed: ' + item + ' ==> ' + dest);
}
}
// setup routes and request-handling // setup routes and request-handling
var app = express(); const app = express();
app.get('/raw/:id', function(req, res) { // Use helmet for basic security headers
return documentHandler.handleRawGet(req.params.id, res); app.use(helmet());
app.get('/raw/:id', (req, res) => {
return documentHandler.handleRawGet(req.params.id, res);
}); });
app.post('/documents', function(req, res) { app.post('/documents', (req, res) => {
return documentHandler.handlePost(req, res); return documentHandler.handlePost(req, res);
}); });
app.get('/documents/:id', function(req, res) { app.get('/documents/:id', (req, res) => {
return documentHandler.handleGet(req.params.id, res); return documentHandler.handleGet(req.params.id, res);
});
app.use(express.static('static'));
app.get('/:id', function(req, res, next) {
res.sendFile(__dirname + '/static/index.html');
}); });
app.listen(config.port, config.host); app.use(express.static(path.join(__dirname, 'static')));
logger.info('Listening on ' + config.host + ':' + config.port);
app.get('/:id', (req, res, next) => {
res.sendFile(path.join(__dirname, '/static/index.html'));
});
app.get('/', (req, res, next) => {
res.sendFile(path.join(__dirname, '/static/index.html'));
});
app.listen(config.port, config.host, () => {
logger.info(`Listening on ${config.host}:${config.port}`);
});

View File

@@ -4,20 +4,51 @@ html {
body { body {
background: #002B36; background: #002B36;
height: 90%; color: #839496;
font-family: sans-serif;
margin: 0; margin: 0;
padding: 1em; padding: 0;
display: flex;
flex-direction: column;
height: 100%;
}
#tools {
background: #00212b;
padding: 8px;
font-size: 14px;
flex-shrink: 0;
}
#tools .function {
display: inline-block;
padding: 4px 8px;
margin: 0 4px;
color: #839496;
border-radius: 3px;
text-decoration: none;
}
#tools .function.enabled {
cursor: pointer;
color: #93a1a1;
}
#tools .function.enabled:hover {
background: #073642;
color: #eee8d5;
} }
#content { #content {
height: 100%; flex-grow: 1;
padding: 0; padding: 1em;
overflow-y: auto;
} }
textarea { textarea {
background: transparent; background: transparent;
border: 0; border: 0;
color: #fff; color: #eee8d5;
font-family: monospace; font-family: monospace;
font-size: 1em; font-size: 1em;
height: 100%; height: 100%;
@@ -34,6 +65,7 @@ textarea {
outline: none; outline: none;
padding: 0 0 4em 0; padding: 0 0 4em 0;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word;
} }
#code code { #code code {
@@ -60,77 +92,4 @@ pre .line::before {
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
#tools {
background: #08323c;
bottom: 0;
font-size: 0;
left: 0;
position: fixed;
width: 100%;
}
#tools .function {
background: url(function-icons.png);
display: inline-block;
height: 37px;
position: relative;
width: 32px;
}
#tools .link embed {
vertical-align: bottom;
}
#tools .function.enabled:hover {
cursor: pointer;
}
#tools .function.save {
background-position: -5px top;
}
#tools .function.enabled.save {
background-position: -5px center;
}
#tools .function.enabled.save:hover {
background-position: -5px bottom;
}
#tools .function.new {
background-position: -42px top;
}
#tools .function.enabled.new {
background-position: -42px center;
}
#tools .function.enabled.new:hover {
background-position: -42px bottom;
}
#tools .function.duplicate {
background-position: -79px top;
}
#tools .function.enabled.duplicate {
background-position: -79px center;
}
#tools .function.enabled.duplicate:hover {
background-position: -79px bottom;
}
#tools .function.raw {
background-position: -116px top;
}
#tools .function.enabled.raw {
background-position: -116px center;
}
#tools .function.enabled.raw:hover {
background-position: -116px bottom;
}

View File

@@ -1,6 +1,5 @@
// represents the haste-application const Haste = function() {
var haste = function() { this.appName = "unknownBIN";
this.appName = "Hastebin Plus";
this.$textarea = $('textarea'); this.$textarea = $('textarea');
this.$box = $('#code'); this.$box = $('#code');
this.$code = $('#code code'); this.$code = $('#code code');
@@ -9,26 +8,26 @@ var haste = function() {
}; };
// set title (browser window) // set title (browser window)
haste.prototype.setTitle = function(ext) { Haste.prototype.setTitle = function(ext) {
var title = ext ? this.appName + ' - ' + ext : this.appName; const title = ext ? `${this.appName} - ${ext}` : this.appName;
document.title = title; document.title = title;
}; };
// show the light key // show the light key
haste.prototype.lightKey = function() { Haste.prototype.lightKey = function() {
this.configureKey(['new', 'save']); this.configureKey(['new', 'save']);
}; };
// show the full key // show the full key
haste.prototype.fullKey = function() { Haste.prototype.fullKey = function() {
this.configureKey(['new', 'duplicate', 'raw']); this.configureKey(['new', 'duplicate', 'raw']);
}; };
// enable certain keys // enable certain keys
haste.prototype.configureKey = function(enable) { Haste.prototype.configureKey = function(enable) {
$('#tools .function').each(function() { $('#tools .function').each(function() {
var $this = $(this); const $this = $(this);
for (var i = 0; i < enable.length; i++) { for (let i = 0; i < enable.length; i++) {
if ($this.hasClass(enable[i])) { if ($this.hasClass(enable[i])) {
$this.addClass('enabled'); $this.addClass('enabled');
return true; return true;
@@ -39,9 +38,9 @@ haste.prototype.configureKey = function(enable) {
}; };
// setup a new, blank document // setup a new, blank document
haste.prototype.newDocument = function(hideHistory) { Haste.prototype.newDocument = function(hideHistory) {
this.$box.hide(); this.$box.hide();
this.doc = new haste_document(); this.doc = new HasteDocument();
if (!hideHistory) { if (!hideHistory) {
window.history.pushState(null, this.appName, '/'); window.history.pushState(null, this.appName, '/');
} }
@@ -53,14 +52,14 @@ haste.prototype.newDocument = function(hideHistory) {
}; };
// load an existing document // load an existing document
haste.prototype.loadDocument = function(key) { Haste.prototype.loadDocument = function(key) {
var _this = this; const _this = this;
_this.doc = new haste_document(); _this.doc = new HasteDocument();
_this.doc.load(key, function(ret) { _this.doc.load(key, function(ret) {
if (ret) { if (ret) {
_this.$code.html(ret.value); _this.$code.html(ret.value);
_this.setTitle(ret.key); _this.setTitle(ret.key);
window.history.pushState(null, _this.appName + '-' + ret.key, '/' + ret.key); window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`);
_this.fullKey(); _this.fullKey();
_this.$textarea.val('').hide(); _this.$textarea.val('').hide();
_this.$box.show(); _this.$box.show();
@@ -71,22 +70,22 @@ haste.prototype.loadDocument = function(key) {
}; };
// duplicate the current document // duplicate the current document
haste.prototype.duplicateDocument = function() { Haste.prototype.duplicateDocument = function() {
if (this.doc.locked) { if (this.doc.locked) {
var currentData = this.doc.data; const currentData = this.doc.data;
this.newDocument(); this.newDocument();
this.$textarea.val(currentData); this.$textarea.val(currentData);
} }
}; };
// lock the current document // lock the current document
haste.prototype.lockDocument = function() { Haste.prototype.lockDocument = function() {
var _this = this; const _this = this;
this.doc.save(this.$textarea.val(), function(err, ret) { this.doc.save(this.$textarea.val(), function(err, ret) {
if (!err && ret) { if (!err && ret) {
_this.$code.html(ret.value.trim().replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n")); _this.$code.html(ret.value.trim().replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n"));
_this.setTitle(ret.key); _this.setTitle(ret.key);
window.history.pushState(null, _this.appName + '-' + ret.key, '/' + ret.key); window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`);
_this.fullKey(); _this.fullKey();
_this.$textarea.val('').hide(); _this.$textarea.val('').hide();
_this.$box.show(); _this.$box.show();
@@ -95,8 +94,8 @@ haste.prototype.lockDocument = function() {
}; };
// configure buttons and their shortcuts // configure buttons and their shortcuts
haste.prototype.configureButtons = function() { Haste.prototype.configureButtons = function() {
var _this = this; const _this = this;
this.buttons = [ this.buttons = [
{ {
$where: $('#tools .save'), $where: $('#tools .save'),
@@ -133,17 +132,17 @@ haste.prototype.configureButtons = function() {
return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82; return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82;
}, },
action: function() { action: function() {
window.location.href = '/raw/' + _this.doc.key; window.location.href = `/raw/${_this.doc.key}`;
} }
} }
]; ];
for (var i = 0; i < this.buttons.length; i++) { for (let i = 0; i < this.buttons.length; i++) {
this.configureButton(this.buttons[i]); this.configureButton(this.buttons[i]);
} }
}; };
// handles the button-click // handles the button-click
haste.prototype.configureButton = function(options) { Haste.prototype.configureButton = function(options) {
options.$where.click(function(evt) { options.$where.click(function(evt) {
evt.preventDefault(); evt.preventDefault();
if (!options.clickDisabled && $(this).hasClass('enabled')) { if (!options.clickDisabled && $(this).hasClass('enabled')) {
@@ -153,11 +152,11 @@ haste.prototype.configureButton = function(options) {
}; };
// enables the configured shortcuts // enables the configured shortcuts
haste.prototype.configureShortcuts = function() { Haste.prototype.configureShortcuts = function() {
var _this = this; const _this = this;
$(document.body).keydown(function(evt) { $(document.body).keydown(function(evt) {
var button; let button;
for (var i = 0; i < _this.buttons.length; i++) { for (let i = 0; i < _this.buttons.length; i++) {
button = _this.buttons[i]; button = _this.buttons[i];
if (button.shortcut && button.shortcut(evt)) { if (button.shortcut && button.shortcut(evt)) {
evt.preventDefault(); evt.preventDefault();
@@ -169,12 +168,12 @@ haste.prototype.configureShortcuts = function() {
}; };
// represents a single document // represents a single document
var haste_document = function() { const HasteDocument = function() {
this.locked = false; this.locked = false;
}; };
// escape HTML-characters // escape HTML-characters
haste_document.prototype.htmlEscape = function(s) { HasteDocument.prototype.htmlEscape = function(s) {
return s return s
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
@@ -183,40 +182,39 @@ haste_document.prototype.htmlEscape = function(s) {
}; };
// load a document from the server // load a document from the server
haste_document.prototype.load = function(key, callback) { HasteDocument.prototype.load = function(key, callback) {
var _this = this; const _this = this;
$.ajax('/documents/' + key, { $.ajax(`/documents/${key}`, {
type: 'get', type: 'get',
dataType: 'json', dataType: 'json',
success: function(res) { success: function(res) {
_this.locked = true; _this.locked = true;
_this.key = key; _this.key = key;
_this.data = res.data; _this.data = res.data;
high = hljs.highlightAuto(res.data).value; const high = hljs.highlightAuto(res.data).value;
callback({ callback({
value: high.replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n"), value: high.replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n"),
key: key, key: key,
}); });
}, },
error: function(err) { error: function() {
callback(false); callback(false);
} }
}); });
}; };
// sends the document to the server // sends the document to the server
haste_document.prototype.save = function(data, callback) { HasteDocument.prototype.save = function(data, callback) {
if (this.locked) return false; if (this.locked) return false;
this.data = data; this.data = data;
var _this = this;
$.ajax('/documents', { $.ajax('/documents', {
type: 'post', type: 'post',
data: data.trim(), data: data,
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'text/plain; charset=utf-8',
success: function(res) { success: function(res) {
new haste().loadDocument(res.key); new Haste().loadDocument(res.key);
}, },
error: function(res) { error: function(res) {
try { try {
@@ -234,16 +232,16 @@ $(function() {
// allow usage of tabs // allow usage of tabs
if (evt.keyCode === 9) { if (evt.keyCode === 9) {
evt.preventDefault(); evt.preventDefault();
var myValue = ' '; const myValue = ' ';
if (document.selection) { if (document.selection) {
this.focus(); this.focus();
sel = document.selection.createRange(); let sel = document.selection.createRange();
sel.text = myValue; sel.text = myValue;
this.focus(); this.focus();
} else if (this.selectionStart || this.selectionStart == '0') { } else if (this.selectionStart || this.selectionStart == '0') {
var startPos = this.selectionStart; const startPos = this.selectionStart;
var endPos = this.selectionEnd; const endPos = this.selectionEnd;
var scrollTop = this.scrollTop; const scrollTop = this.scrollTop;
this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos, this.value.length); this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos, this.value.length);
this.focus(); this.focus();
this.selectionStart = startPos + myValue.length; this.selectionStart = startPos + myValue.length;
@@ -256,8 +254,8 @@ $(function() {
} }
}); });
var app = new haste(); const app = new Haste();
var path = window.location.pathname; const path = window.location.pathname;
if (path === '/') { if (path === '/') {
app.newDocument(true); app.newDocument(true);
} else { } else {
@@ -265,4 +263,4 @@ $(function() {
} }
}); });
hljs.initHighlightingOnLoad(); hljs.initHighlightingOnLoad();

1
static/application.min.css vendored Normal file
View File

@@ -0,0 +1 @@
html{height:100%}body{background:#002b36;color:#839496;font-family:sans-serif;margin:0;padding:0;display:flex;flex-direction:column;height:100%}#tools{background:#00212b;padding:8px;font-size:14px;flex-shrink:0}#tools .function{display:inline-block;padding:4px 8px;margin:0 4px;color:#839496;border-radius:3px;text-decoration:none}#tools .function.enabled{cursor:pointer;color:#93a1a1}#tools .function.enabled:hover{background:#073642;color:#eee8d5}#content{flex-grow:1;padding:1em;overflow-y:auto}textarea{background:0 0;border:0;color:#eee8d5;font-family:monospace;font-size:1em;height:100%;outline:0;padding:0;resize:none;width:100%}#code{border:0;font-size:1em;margin:0;outline:0;padding:0 0 4em 0;white-space:pre-wrap;word-wrap:break-word}#code code{background:0 0!important;padding:0}pre{counter-reset:line-numbering}pre .line::before{color:#4c6a71;content:counter(line-numbering);counter-increment:line-numbering;display:inline-block;margin-right:1em;text-align:right;width:1.5em!important;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}

1
static/application.min.js vendored Normal file
View File

@@ -0,0 +1 @@
let Haste=function(){this.appName="unknownBIN",this.$textarea=$("textarea"),this.$box=$("#code"),this.$code=$("#code code"),this.configureShortcuts(),this.configureButtons()},HasteDocument=(Haste.prototype.setTitle=function(t){t=t?this.appName+" - "+t:this.appName;document.title=t},Haste.prototype.lightKey=function(){this.configureKey(["new","save"])},Haste.prototype.fullKey=function(){this.configureKey(["new","duplicate","raw"])},Haste.prototype.configureKey=function(o){$("#tools .function").each(function(){var e=$(this);for(let t=0;t<o.length;t++)if(e.hasClass(o[t]))return e.addClass("enabled"),!0;e.removeClass("enabled")})},Haste.prototype.newDocument=function(t){this.$box.hide(),this.doc=new HasteDocument,t||window.history.pushState(null,this.appName,"/"),this.setTitle(),this.lightKey(),this.$textarea.val("").show("fast",function(){this.focus()})},Haste.prototype.loadDocument=function(t){let e=this;e.doc=new HasteDocument,e.doc.load(t,function(t){t?(e.$code.html(t.value),e.setTitle(t.key),window.history.pushState(null,e.appName+"-"+t.key,"/"+t.key),e.fullKey(),e.$textarea.val("").hide(),e.$box.show()):e.newDocument()})},Haste.prototype.duplicateDocument=function(){var t;this.doc.locked&&(t=this.doc.data,this.newDocument(),this.$textarea.val(t))},Haste.prototype.lockDocument=function(){let o=this;this.doc.save(this.$textarea.val(),function(t,e){!t&&e&&(o.$code.html(e.value.trim().replace(/.+/g,'<span class="line">$&</span>').replace(/^\s*[\r\n]/gm,'<span class="line"></span>\n')),o.setTitle(e.key),window.history.pushState(null,o.appName+"-"+e.key,"/"+e.key),o.fullKey(),o.$textarea.val("").hide(),o.$box.show())})},Haste.prototype.configureButtons=function(){let e=this;this.buttons=[{$where:$("#tools .save"),shortcut:function(t){return t.ctrlKey&&83===t.keyCode},action:function(){""!==e.$textarea.val().replace(/^\s+|\s+$/g,"")&&e.lockDocument()}},{$where:$("#tools .new"),shortcut:function(t){return t.ctrlKey&&32===t.keyCode},action:function(){e.newDocument(!e.doc.key)}},{$where:$("#tools .duplicate"),shortcut:function(t){return e.doc.locked&&t.ctrlKey&&68===t.keyCode},action:function(){e.duplicateDocument()}},{$where:$("#tools .raw"),shortcut:function(t){return t.ctrlKey&&t.shiftKey&&82===t.keyCode},action:function(){window.location.href="/raw/"+e.doc.key}}];for(let t=0;t<this.buttons.length;t++)this.configureButton(this.buttons[t])},Haste.prototype.configureButton=function(e){e.$where.click(function(t){t.preventDefault(),!e.clickDisabled&&$(this).hasClass("enabled")&&e.action()})},Haste.prototype.configureShortcuts=function(){let n=this;$(document.body).keydown(function(e){var o;for(let t=0;t<n.buttons.length;t++)if((o=n.buttons[t]).shortcut&&o.shortcut(e))return e.preventDefault(),void o.action()})},function(){this.locked=!1});HasteDocument.prototype.htmlEscape=function(t){return t.replace(/&/g,"&amp;").replace(/>/g,"&gt;").replace(/</g,"&lt;").replace(/"/g,"&quot;")},HasteDocument.prototype.load=function(e,o){let n=this;$.ajax("/documents/"+e,{type:"get",dataType:"json",success:function(t){n.locked=!0,n.key=e,n.data=t.data;t=hljs.highlightAuto(t.data).value;o({value:t.replace(/.+/g,'<span class="line">$&</span>').replace(/^\s*[\r\n]/gm,'<span class="line"></span>\n'),key:e})},error:function(){o(!1)}})},HasteDocument.prototype.save=function(t,e){if(this.locked)return!1;this.data=t,$.ajax("/documents",{type:"post",data:t,dataType:"json",contentType:"text/plain; charset=utf-8",success:function(t){(new Haste).loadDocument(t.key)},error:function(t){try{e($.parseJSON(t.responseText))}catch(t){e({message:"Something went wrong!"})}}})},$(function(){$("textarea").keydown(function(t){var e,o,n;9===t.keyCode&&(t.preventDefault(),t=" ",document.selection?(this.focus(),document.selection.createRange().text=t,this.focus()):this.selectionStart||"0"==this.selectionStart?(e=this.selectionStart,o=this.selectionEnd,n=this.scrollTop,this.value=this.value.substring(0,e)+t+this.value.substring(o,this.value.length),this.focus(),this.selectionStart=e+t.length,this.selectionEnd=e+t.length,this.scrollTop=n):(this.value+=t,this.focus()))});var t=new Haste,e=window.location.pathname;"/"===e?t.newDocument(!0):t.loadDocument(e.substring(1,e.length))}),hljs.initHighlightingOnLoad();

1
static/highlight.min.css vendored Normal file
View File

@@ -0,0 +1 @@
.hljs{display:block;overflow-x:auto;padding:.5em;background:#002b36;color:#839496}.hljs-comment,.hljs-quote{color:#586e75}.hljs-addition,.hljs-keyword,.hljs-selector-tag{color:#859900}.hljs-doctag,.hljs-literal,.hljs-meta .hljs-meta-string,.hljs-number,.hljs-regexp,.hljs-string{color:#2aa198}.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title{color:#268bd2}.hljs-attr,.hljs-attribute,.hljs-class .hljs-title,.hljs-template-variable,.hljs-type,.hljs-variable{color:#b58900}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-symbol{color:#cb4b16}.hljs-built_in,.hljs-deletion{color:#dc322f}.hljs-formula{background:#073642}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Hastebin Plus</title> <title>unknownBIN</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="application.min.css"/> <link rel="stylesheet" type="text/css" href="application.min.css"/>
<link rel="stylesheet" type="text/css" href="highlight.min.css"/> <link rel="stylesheet" type="text/css" href="highlight.min.css"/>
@@ -11,15 +11,15 @@
<script src="application.min.js"></script> <script src="application.min.js"></script>
</head> </head>
<body> <body>
<div id="tools">
<div class="save function" title="Save [Ctrl + S]">Save</div>
<div class="new function" title="New [Ctrl + Space]">New</div>
<div class="duplicate function" title="Duplicate [Ctrl + D]">Duplicate</div>
<div class="raw function" title="Raw [Ctrl + Shift + R]">Raw</div>
</div>
<div id="content"> <div id="content">
<pre id="code" style="display:none;" tabindex="0"><code></code></pre> <pre id="code" style="display:none;" tabindex="0"><code></code></pre>
<textarea spellcheck="false" style="display:none;"></textarea> <textarea spellcheck="false" style="display:none;"></textarea>
</div> </div>
<div id="tools">
<div class="save function" title="Save [Ctrl + S]"></div>
<div class="new function" title="New [Ctrl + Space]"></div>
<div class="duplicate function" title="Duplicate [Ctrl + D]"></div>
<div class="raw function" title="Raw [Ctrl + Shift + R]"></div>
</div>
</body> </body>
</html> </html>