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)
.idea
HastebinPlus.iml
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
# IDE (Microsoft Visual Studio)
obj/
Microsoft.NodejsTools.WebRole.dll
.ntvs_analysis.dat
*.njsproj
*.sln
*.suo
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Application
data
node_modules
*.min.css
*.min.js
!jquery.min.js
!highlight.min.js
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# NodeJS
npm-debug.log
# Runtime data
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
Hastebin Plus is an open-source Pastebin software written in node.js, which is easily installable in any network.
It bases upon [haste](https://github.com/seejohnrun/haste-server) and got enhanced in matters of **Design, Speed and Simplicity**.
# unknownBIN
unknownBIN is a secure and modern open-source Pastebin software written in node.js.
It is a fork of the original Hastebin and Hastebin Plus, modernized for security and performance.
## Features
* 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
* Duplicate & edit pastes
* Raw paste-view
* Secure, unpredictable paste IDs
* Modernized backend with security enhancements
## 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: `sudo apt-get install git nodejs`
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)
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`
3. Change into the directory: `cd unknownbin`
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
1. Pull changes from this repository: `git pull`
1. Pull changes from the repository: `git pull`
2. Install new dependencies: `npm install`
3. Re-build static assets: `npm run build`
4. Restart the application.
## Settings
| 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` |
| `keyLength` | The length of the pastes' key | `10` |
| `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 |
### Default Config
@@ -43,23 +47,6 @@ It bases upon [haste](https://github.com/seejohnrun/haste-server) and got enhanc
"maxLength": 500000,
"createKey": "",
"documents": {
"about": "./README.md",
"javaTest": "./documents/test.java"
"about": "./README.md"
}
}
```
## 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');
var KeyGenerator = require('./key_generator.js');
const KeyGenerator = require('./key_generator.js');
// handles creating new and requesting existing documents
var DocumentHandler = function(options) {
if (!options) {
options = {};
}
this.store = options.store;
this.maxLength = options.maxLength || 50000;
this.keyLength = options.keyLength || 10;
this.createKey = options.createKey || '';
this.keyGenerator = new KeyGenerator();
class DocumentHandler {
constructor(options) {
if (!options) {
options = {};
}
this.store = options.store;
this.logger = options.logger;
this.maxLength = options.maxLength || 50000;
this.keyLength = options.keyLength || 10;
this.createKey = options.createKey || '';
this.keyGenerator = new KeyGenerator();
if (this.createKey !== '') {
logger.info("Creation-Key:", this.createKey);
}
};
if (this.createKey !== '') {
this.logger.info("Creation-Key is configured.");
}
}
// handles existing documents
DocumentHandler.prototype.handleGet = function(key, res) {
this.store.get(key, function(ret) {
if (ret) {
logger.verbose('Open paste:', key);
res.writeHead(200, {'content-type': 'application/json'});
res.end(JSON.stringify({key: key, data: ret.replace(/\t/g, ' ')}));
} else {
logger.verbose('Paste not found:', key);
res.writeHead(404, {'content-type': 'application/json'});
res.end(JSON.stringify({message: 'Paste not found.'}));
}
});
};
// handles existing documents
handleGet(key, res) {
this.store.get(key, (ret) => {
if (ret) {
this.logger.verbose(`Open paste: ${key}`);
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ key: key, data: ret.replace(/\t/g, ' ') }));
} else {
this.logger.verbose(`Paste not found: ${key}`);
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Paste not found.' }));
}
});
}
// handles exisiting documents (raw)
DocumentHandler.prototype.handleRawGet = function(key, res) {
this.store.get(key, function(ret) {
if (ret) {
logger.verbose('Open paste:', key);
res.writeHead(200, {'content-type': 'text/plain'});
res.end(ret);
} else {
logger.verbose('Paste not found:', key);
res.writeHead(404, {'content-type': 'application/json'});
res.end(JSON.stringify({message: 'Paste not found.'}));
}
});
};
// handles existing documents (raw)
handleRawGet(key, res) {
this.store.get(key, (ret) => {
if (ret) {
this.logger.verbose(`Open raw paste: ${key}`);
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
res.end(ret);
} else {
this.logger.verbose(`Paste not found: ${key}`);
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Paste not found.' }));
}
});
}
// handles creating new documents
DocumentHandler.prototype.handlePost = function(req, res) {
var _this = this;
var buffer = '';
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;
// handles creating new documents
handlePost(req, res) {
let buffer = '';
let cancelled = false;
if (_this.createKey !== '') {
if (!buffer.startsWith(_this.createKey)) {
logger.warn('Error adding new paste: wrong key');
res.writeHead(400, {'content-type': 'application/json'});
res.end(JSON.stringify({message: 'Error adding new paste: wrong key'}));
return;
}
buffer = buffer.substring(_this.createKey.length);
}
req.on('data', (data) => {
if (cancelled) return;
buffer += data.toString();
if (this.maxLength && buffer.length > this.maxLength) {
cancelled = true;
this.logger.warn('Paste exceeds maximum length.');
res.writeHead(400, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Paste exceeds maximum length.' }));
}
});
_this.chooseKey(function(key) {
_this.store.set(key, buffer, function(success) {
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.'}));
});
};
req.on('end', () => {
if (cancelled) return;
// creates new keys until one is not taken
DocumentHandler.prototype.chooseKey = function(callback) {
var key = this.acceptableKey();
var _this = this;
this.store.get(key, function(success) {
if (success) {
_this.chooseKey(callback);
} else {
callback(key);
}
});
};
if (this.createKey !== '') {
if (!buffer.startsWith(this.createKey)) {
this.logger.warn('Error adding new paste: wrong key');
res.writeHead(403, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Error adding new paste: wrong key' }));
return;
}
buffer = buffer.substring(this.createKey.length);
}
// creates a new key using the key-generator
DocumentHandler.prototype.acceptableKey = function() {
return this.keyGenerator.createKey(this.keyLength);
};
this.chooseKey((key) => {
this.store.set(key, buffer, (success) => {
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');
var crypto = require('crypto');
const fs = require('fs');
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
var FileDocumentStore = function(options) {
this.basePath = options.path || './data';
logger.info('Path to data: ' + this.basePath);
};
class FileDocumentStore {
constructor(options) {
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
FileDocumentStore.prototype.set = function(key, data, callback) {
var _this = this;
fs.mkdir(this.basePath, '700', function(err) {
fs.writeFile(_this.basePath + '/' + key, data, 'utf8', function(err) {
if (err) {
callback(false);
} else {
callback(true);
}
});
});
};
// saves a new file to the filesystem
set(key, data, callback) {
if (!validKeyRegex.test(key)) {
this.logger.warn(`Invalid key provided for set: ${key}`);
return callback(false);
}
// gets an exisiting file from the filesystem
FileDocumentStore.prototype.get = function(key, callback) {
var _this = this;
fs.readFile(this.basePath + '/' + key, 'utf8', function(err, data) {
if (err) {
callback(false);
} else {
callback(data);
}
});
};
const resolvedBasePath = path.resolve(this.basePath);
const filePath = path.resolve(resolvedBasePath, key);
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() {
this.keyspace = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
};
const crypto = require('crypto');
KeyGenerator.prototype.createKey = function(keyLength) {
var key = '';
var index;
for (var i = 0; i < keyLength; i++) {
index = Math.floor(Math.random() * this.keyspace.length);
key += this.keyspace.charAt(index);
}
return key;
};
class KeyGenerator {
constructor() {
this.keyspace = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
}
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",
"version": "1.0.0",
"version": "2.0.0",
"private": true,
"description": "unknownBIN is an open-source Pastebin software written in node.js.",
"keywords": [
"paste",
"pastebin",
"haste",
"hastebin",
"code",
"syntax highlighting"
],
"description": "A secure and modern open-source Pastebin software written in node.js.",
"author": {
"name": "MrUnknownDE",
"email": "me@mrunk.de",
@@ -18,12 +10,14 @@
},
"license": "MIT",
"dependencies": {
"clean-css": "~3.4.19",
"express": "~4.14.0",
"uglify-js": "~2.7.0",
"winston": "~2.2.0"
"clean-css": "^5.3.3",
"express": "^4.19.2",
"helmet": "^7.1.0",
"uglify-js": "^3.17.4",
"winston": "^3.13.0"
},
"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');
var url = require('url');
var fs = require('fs');
const fs = require('fs');
const path = require('path');
var express = require('express');
var logger = require('winston');
const express = require('express');
const helmet = require('helmet');
const winston = require('winston');
var DocumentHandler = require('./lib/document_handler.js');
var FileStorage = require('./lib/file_storage.js');
const DocumentHandler = require('./lib/document_handler.js');
const FileStorage = require('./lib/file_storage.js');
// 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.host = process.env.HOST || config.host || 'localhost';
config.host = process.env.HOST || config.host || '0.0.0.0';
// logger-setup
logger.remove(logger.transports.Console);
logger.add(logger.transports.Console, {colorize: true, level: 'verbose'});
logger.info('Welcome to Hastebin Plus!');
const logger = winston.createLogger({
level: 'verbose',
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
transports: [
new winston.transports.Console()
]
});
logger.info('Welcome to unknownBIN!');
// init file-storage
var fileStorage = new FileStorage(config.dataPath);
const fileStorage = new FileStorage({ path: config.dataPath, logger: logger });
// load static documents into file-storage
for (var name in config.documents) {
var path = config.documents[name];
var data = fs.readFileSync(path, 'utf8');
if (data) {
fileStorage.set(name, data, function(success) {});
logger.verbose('Created document: ' + name + " ==> " + path);
} else {
logger.warn('Unable to find document: ' + name + " ==> " + path);
}
if (config.documents) {
for (const name in config.documents) {
const docPath = config.documents[name];
try {
const data = fs.readFileSync(docPath, 'utf8');
fileStorage.set(name, data, (success) => {
if (success) {
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
var documentHandler = new DocumentHandler({
store: fileStorage,
maxLength: config.maxLength,
keyLength: config.keyLength,
createKey: config.createKey
const documentHandler = new DocumentHandler({
store: fileStorage,
maxLength: config.maxLength,
keyLength: config.keyLength,
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
var app = express();
const app = express();
app.get('/raw/:id', function(req, res) {
return documentHandler.handleRawGet(req.params.id, res);
// Use helmet for basic security headers
app.use(helmet());
app.get('/raw/:id', (req, res) => {
return documentHandler.handleRawGet(req.params.id, res);
});
app.post('/documents', function(req, res) {
return documentHandler.handlePost(req, res);
app.post('/documents', (req, res) => {
return documentHandler.handlePost(req, res);
});
app.get('/documents/:id', function(req, 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.get('/documents/:id', (req, res) => {
return documentHandler.handleGet(req.params.id, res);
});
app.listen(config.port, config.host);
logger.info('Listening on ' + config.host + ':' + config.port);
app.use(express.static(path.join(__dirname, 'static')));
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 {
background: #002B36;
height: 90%;
color: #839496;
font-family: sans-serif;
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 {
height: 100%;
padding: 0;
flex-grow: 1;
padding: 1em;
overflow-y: auto;
}
textarea {
background: transparent;
border: 0;
color: #fff;
color: #eee8d5;
font-family: monospace;
font-size: 1em;
height: 100%;
@@ -34,6 +65,7 @@ textarea {
outline: none;
padding: 0 0 4em 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#code code {
@@ -60,77 +92,4 @@ pre .line::before {
-moz-user-select: none;
-ms-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
var haste = function() {
this.appName = "Hastebin Plus";
const Haste = function() {
this.appName = "unknownBIN";
this.$textarea = $('textarea');
this.$box = $('#code');
this.$code = $('#code code');
@@ -9,26 +8,26 @@ var haste = function() {
};
// set title (browser window)
haste.prototype.setTitle = function(ext) {
var title = ext ? this.appName + ' - ' + ext : this.appName;
Haste.prototype.setTitle = function(ext) {
const title = ext ? `${this.appName} - ${ext}` : this.appName;
document.title = title;
};
// show the light key
haste.prototype.lightKey = function() {
Haste.prototype.lightKey = function() {
this.configureKey(['new', 'save']);
};
// show the full key
haste.prototype.fullKey = function() {
Haste.prototype.fullKey = function() {
this.configureKey(['new', 'duplicate', 'raw']);
};
// enable certain keys
haste.prototype.configureKey = function(enable) {
Haste.prototype.configureKey = function(enable) {
$('#tools .function').each(function() {
var $this = $(this);
for (var i = 0; i < enable.length; i++) {
const $this = $(this);
for (let i = 0; i < enable.length; i++) {
if ($this.hasClass(enable[i])) {
$this.addClass('enabled');
return true;
@@ -39,9 +38,9 @@ haste.prototype.configureKey = function(enable) {
};
// setup a new, blank document
haste.prototype.newDocument = function(hideHistory) {
Haste.prototype.newDocument = function(hideHistory) {
this.$box.hide();
this.doc = new haste_document();
this.doc = new HasteDocument();
if (!hideHistory) {
window.history.pushState(null, this.appName, '/');
}
@@ -53,14 +52,14 @@ haste.prototype.newDocument = function(hideHistory) {
};
// load an existing document
haste.prototype.loadDocument = function(key) {
var _this = this;
_this.doc = new haste_document();
Haste.prototype.loadDocument = function(key) {
const _this = this;
_this.doc = new HasteDocument();
_this.doc.load(key, function(ret) {
if (ret) {
_this.$code.html(ret.value);
_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.$textarea.val('').hide();
_this.$box.show();
@@ -71,22 +70,22 @@ haste.prototype.loadDocument = function(key) {
};
// duplicate the current document
haste.prototype.duplicateDocument = function() {
Haste.prototype.duplicateDocument = function() {
if (this.doc.locked) {
var currentData = this.doc.data;
const currentData = this.doc.data;
this.newDocument();
this.$textarea.val(currentData);
}
};
// lock the current document
haste.prototype.lockDocument = function() {
var _this = this;
Haste.prototype.lockDocument = function() {
const _this = this;
this.doc.save(this.$textarea.val(), function(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.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.$textarea.val('').hide();
_this.$box.show();
@@ -95,8 +94,8 @@ haste.prototype.lockDocument = function() {
};
// configure buttons and their shortcuts
haste.prototype.configureButtons = function() {
var _this = this;
Haste.prototype.configureButtons = function() {
const _this = this;
this.buttons = [
{
$where: $('#tools .save'),
@@ -133,17 +132,17 @@ haste.prototype.configureButtons = function() {
return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82;
},
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]);
}
};
// handles the button-click
haste.prototype.configureButton = function(options) {
Haste.prototype.configureButton = function(options) {
options.$where.click(function(evt) {
evt.preventDefault();
if (!options.clickDisabled && $(this).hasClass('enabled')) {
@@ -153,11 +152,11 @@ haste.prototype.configureButton = function(options) {
};
// enables the configured shortcuts
haste.prototype.configureShortcuts = function() {
var _this = this;
Haste.prototype.configureShortcuts = function() {
const _this = this;
$(document.body).keydown(function(evt) {
var button;
for (var i = 0; i < _this.buttons.length; i++) {
let button;
for (let i = 0; i < _this.buttons.length; i++) {
button = _this.buttons[i];
if (button.shortcut && button.shortcut(evt)) {
evt.preventDefault();
@@ -169,12 +168,12 @@ haste.prototype.configureShortcuts = function() {
};
// represents a single document
var haste_document = function() {
const HasteDocument = function() {
this.locked = false;
};
// escape HTML-characters
haste_document.prototype.htmlEscape = function(s) {
HasteDocument.prototype.htmlEscape = function(s) {
return s
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
@@ -183,40 +182,39 @@ haste_document.prototype.htmlEscape = function(s) {
};
// load a document from the server
haste_document.prototype.load = function(key, callback) {
var _this = this;
$.ajax('/documents/' + key, {
HasteDocument.prototype.load = function(key, callback) {
const _this = this;
$.ajax(`/documents/${key}`, {
type: 'get',
dataType: 'json',
success: function(res) {
_this.locked = true;
_this.key = key;
_this.data = res.data;
high = hljs.highlightAuto(res.data).value;
const high = hljs.highlightAuto(res.data).value;
callback({
value: high.replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n"),
key: key,
});
},
error: function(err) {
error: function() {
callback(false);
}
});
};
// sends the document to the server
haste_document.prototype.save = function(data, callback) {
HasteDocument.prototype.save = function(data, callback) {
if (this.locked) return false;
this.data = data;
var _this = this;
$.ajax('/documents', {
type: 'post',
data: data.trim(),
data: data,
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'text/plain; charset=utf-8',
success: function(res) {
new haste().loadDocument(res.key);
new Haste().loadDocument(res.key);
},
error: function(res) {
try {
@@ -234,16 +232,16 @@ $(function() {
// allow usage of tabs
if (evt.keyCode === 9) {
evt.preventDefault();
var myValue = ' ';
const myValue = ' ';
if (document.selection) {
this.focus();
sel = document.selection.createRange();
let sel = document.selection.createRange();
sel.text = myValue;
this.focus();
} else if (this.selectionStart || this.selectionStart == '0') {
var startPos = this.selectionStart;
var endPos = this.selectionEnd;
var scrollTop = this.scrollTop;
const startPos = this.selectionStart;
const endPos = this.selectionEnd;
const scrollTop = this.scrollTop;
this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos, this.value.length);
this.focus();
this.selectionStart = startPos + myValue.length;
@@ -256,8 +254,8 @@ $(function() {
}
});
var app = new haste();
var path = window.location.pathname;
const app = new Haste();
const path = window.location.pathname;
if (path === '/') {
app.newDocument(true);
} 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>
<html>
<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">
<link rel="stylesheet" type="text/css" href="application.min.css"/>
<link rel="stylesheet" type="text/css" href="highlight.min.css"/>
@@ -11,15 +11,15 @@
<script src="application.min.js"></script>
</head>
<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">
<pre id="code" style="display:none;" tabindex="0"><code></code></pre>
<textarea spellcheck="false" style="display:none;"></textarea>
</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>
</html>
</html>