Fix button alignment and visibility issues, implement CSP improvements with nonce support

This commit is contained in:
Leopere 2025-03-01 17:49:24 -05:00
parent 627cb5cc25
commit 6e7b63a408
16 changed files with 2423 additions and 644 deletions

View File

@ -53,6 +53,7 @@ The container exists at git.nixc.us/colin/haste:haste-production and may be made
* `logging` - logging preferences
* `keyGenerator` - key generator options (see below)
* `rateLimits` - settings for rate limiting (see below)
* `security` - settings for Content Security Policy and other security features (see below)
## Rate Limiting
@ -63,6 +64,37 @@ used and set in `config.json`.
See the README for [connect-ratelimit](https://github.com/dharmafly/connect-ratelimit)
for more information!
## Security Settings
The `security` section in the configuration allows you to control various security features, particularly the Content Security Policy (CSP):
```json
{
"security": {
"csp": true, // Enable/disable CSP entirely
"hsts": false, // Enable HTTP Strict Transport Security
"scriptSources": [], // Additional allowed script sources
"bypassCSPInDev": false, // Use permissive CSP in development mode
"allowUnsafeHashes": true // Allow 'unsafe-hashes' in production for event handlers
}
}
```
### Content Security Policy Options
* `csp` - Enable or disable Content Security Policy headers (default: true)
* `hsts` - Enable HTTP Strict Transport Security headers (default: false)
* `scriptSources` - Additional script sources to allow - comma-separated list in env vars
* `bypassCSPInDev` - In development mode (NODE_ENV=development), use a more permissive CSP that includes 'unsafe-inline' (default: false)
* `allowUnsafeHashes` - Allow 'unsafe-hashes' in production mode for DOM event handlers (default: true)
You can set these options through environment variables:
* `HASTEBIN_ENABLE_CSP` - Enable/disable CSP (true/false)
* `HASTEBIN_ENABLE_HSTS` - Enable/disable HSTS (true/false)
* `HASTEBIN_SCRIPT_SOURCES` - Additional script sources (comma-separated)
* `HASTEBIN_BYPASS_CSP_IN_DEV` - Allow unsafe-inline in development (true/false)
* `HASTEBIN_ALLOW_UNSAFE_HASHES` - Allow unsafe-hashes in production (true/false)
## Key Generation
### Phonetic
@ -230,7 +262,7 @@ Please note that the AGPL imposes certain obligations that are not present in th
Copyright © 2011-2012 John Crepezzi
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:
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:
### Other components:

100
config.js
View File

@ -1,46 +1,94 @@
{
// Modern Hastebin configuration with KeyDB as default storage
const config = {
// Server settings
host: process.env.HASTEBIN_HOST || "0.0.0.0",
port: parseInt(process.env.HASTEBIN_PORT, 10) || 7777,
"host": "0.0.0.0",
"port": 7777,
// Document settings
keyLength: parseInt(process.env.HASTEBIN_KEY_LENGTH, 10) || 10,
maxLength: parseInt(process.env.HASTEBIN_MAX_LENGTH, 10) || 400000,
"keyLength": 10,
// Static file settings
staticMaxAge: parseInt(process.env.HASTEBIN_STATIC_MAX_AGE, 10) || 86400,
recompressStaticAssets: process.env.HASTEBIN_RECOMPRESS_ASSETS ?
(process.env.HASTEBIN_RECOMPRESS_ASSETS.toLowerCase() === 'true') : true,
"maxLength": 400000,
// Security settings
security: {
// Enable Content Security Policy
csp: process.env.HASTEBIN_ENABLE_CSP ?
(process.env.HASTEBIN_ENABLE_CSP.toLowerCase() === 'true') : true,
"staticMaxAge": 86400,
// Enable HTTP Strict Transport Security (only enable in production with HTTPS)
hsts: process.env.HASTEBIN_ENABLE_HSTS ?
(process.env.HASTEBIN_ENABLE_HSTS.toLowerCase() === 'true') : false,
"recompressStaticAssets": true,
// Additional script sources (empty by default since we now host jQuery locally)
scriptSources: process.env.HASTEBIN_SCRIPT_SOURCES ?
process.env.HASTEBIN_SCRIPT_SOURCES.split(',') : [],
"logging": [
// Allow bypassing strict CSP in development mode for testing (default: false)
// This adds unsafe-inline to the policy when NODE_ENV=development
bypassCSPInDev: process.env.HASTEBIN_BYPASS_CSP_IN_DEV ?
(process.env.HASTEBIN_BYPASS_CSP_IN_DEV.toLowerCase() === 'true') : false,
// Allow unsafe-hashes in production for event handlers (default: true)
// This adds 'unsafe-hashes' to the policy for DOM event handlers
allowUnsafeHashes: process.env.HASTEBIN_ALLOW_UNSAFE_HASHES ?
(process.env.HASTEBIN_ALLOW_UNSAFE_HASHES.toLowerCase() === 'true') : true
},
// Logging configuration
logging: [
{
"level": "verbose",
"type": "Console",
"colorize": true
level: process.env.HASTEBIN_LOG_LEVEL || "verbose",
type: process.env.HASTEBIN_LOG_TYPE || "Console",
colorize: process.env.HASTEBIN_LOG_COLORIZE ?
(process.env.HASTEBIN_LOG_COLORIZE.toLowerCase() === 'true') : true,
json: process.env.HASTEBIN_LOG_JSON ?
(process.env.HASTEBIN_LOG_JSON.toLowerCase() === 'true') : false
}
],
"keyGenerator": {
"type": "phonetic"
// Key generator configuration
keyGenerator: {
type: process.env.HASTEBIN_KEY_GENERATOR_TYPE || "phonetic"
},
"rateLimits": {
"categories": {
"normal": {
"totalRequests": 500,
"every": 60000
// Rate limiting configuration
rateLimits: {
categories: {
normal: {
totalRequests: parseInt(process.env.HASTEBIN_RATE_LIMIT_REQUESTS, 10) || 500,
every: parseInt(process.env.HASTEBIN_RATE_LIMIT_WINDOW, 10) || 60000
}
}
},
"storage": {
"type": "memcached",
"host": "127.0.0.1",
"port": 11211,
"expire": 2592000
// Storage configuration - KeyDB as default
storage: {
type: process.env.HASTEBIN_STORAGE_TYPE || "redis",
host: process.env.HASTEBIN_STORAGE_HOST || "redis",
port: parseInt(process.env.HASTEBIN_STORAGE_PORT, 10) || 6379,
password: process.env.HASTEBIN_STORAGE_PASSWORD || "",
db: parseInt(process.env.HASTEBIN_STORAGE_DB, 10) || 0,
expire: parseInt(process.env.HASTEBIN_STORAGE_EXPIRE, 10) || 7776000,
connectionTimeout: parseInt(process.env.HASTEBIN_STORAGE_TIMEOUT, 10) || 5000
},
"documents": {
"about": "./about.md"
}
// Static documents
documents: {
about: process.env.HASTEBIN_ABOUT_DOCUMENT || "./about.md"
},
// CORS settings
allowedOrigins: process.env.HASTEBIN_ALLOWED_ORIGINS ?
process.env.HASTEBIN_ALLOWED_ORIGINS.split(',') : ['*']
};
// Support for backwards compatibility
if (process.env.REDIS_URL || process.env.REDISTOGO_URL) {
config.storage.url = process.env.REDIS_URL || process.env.REDISTOGO_URL;
}
module.exports = config;

View File

@ -1,4 +1,3 @@
version: '3.9'
services:
redis:
image: eqalpha/keydb

163
lib/csp.js Normal file
View File

@ -0,0 +1,163 @@
/**
* Content Security Policy middleware
* Generates nonces for inline scripts and sets appropriate CSP headers
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const winston = require('winston');
// Keep a cache of file hashes
const fileHashes = {};
// Generate a SHA-256 hash for a file or string
function generateHash(content) {
const hash = crypto.createHash('sha256');
hash.update(content);
return `'sha256-${hash.digest('base64')}'`;
}
// Calculate hash for a file and cache it
function getFileHash(filePath) {
if (!fileHashes[filePath]) {
try {
const content = fs.readFileSync(filePath, 'utf8');
fileHashes[filePath] = generateHash(content);
} catch (err) {
console.error(`Error generating hash for ${filePath}:`, err);
return null;
}
}
return fileHashes[filePath];
}
// CSP middleware
function cspMiddleware(config) {
// Default to enabled if not specified
const enabled = config.security && typeof config.security.csp !== 'undefined' ?
config.security.csp : true;
// If CSP is disabled, return a no-op middleware
if (!enabled) {
return function(req, res, next) { next(); };
}
// Calculate hashes for static JS files once at startup
const staticDir = path.join(process.cwd(), 'static');
let appJsHash, highlightJsHash, jqueryJsHash;
try {
// Only generate hashes if the files exist
if (fs.existsSync(path.join(staticDir, 'application.min.js'))) {
appJsHash = getFileHash(path.join(staticDir, 'application.min.js'));
winston.debug('Generated hash for application.min.js');
}
if (fs.existsSync(path.join(staticDir, 'highlight.min.js'))) {
highlightJsHash = getFileHash(path.join(staticDir, 'highlight.min.js'));
winston.debug('Generated hash for highlight.min.js');
}
if (fs.existsSync(path.join(staticDir, 'jquery.min.js'))) {
jqueryJsHash = getFileHash(path.join(staticDir, 'jquery.min.js'));
winston.debug('Generated hash for jquery.min.js');
}
} catch (err) {
winston.error('Error generating file hashes:', err);
}
return function(req, res, next) {
// Only add CSP headers for HTML requests
const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/);
if (isHtmlRequest) {
// Generate a unique nonce for this request
const nonce = crypto.randomBytes(16).toString('base64');
// Store nonce in request object for use in HTML template
req.cspNonce = nonce;
// Build the base script sources list - now only self-hosted content
const baseScriptSources = ["'self'"];
// Add nonce
baseScriptSources.push(`'nonce-${nonce}'`);
// Add static file hashes if available
if (appJsHash) baseScriptSources.push(appJsHash);
if (highlightJsHash) baseScriptSources.push(highlightJsHash);
if (jqueryJsHash) baseScriptSources.push(jqueryJsHash);
// Create the policy - adjust based on environment
let cspDirectives;
// Development mode with potential bypass option
if (process.env.NODE_ENV === 'development') {
// Check if we should bypass strict CSP in development
const bypassCSPInDev = config.security && config.security.bypassCSPInDev;
if (bypassCSPInDev) {
// Very permissive policy with unsafe-inline for development testing
cspDirectives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"connect-src 'self'",
"font-src 'self'"
];
winston.debug('Using permissive development CSP policy with unsafe-inline (bypass enabled)');
} else {
// Standard development mode - still using nonces
cspDirectives = [
"default-src 'self'",
`script-src ${baseScriptSources.join(' ')}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"connect-src 'self'",
"font-src 'self'"
];
winston.debug('Using development CSP policy with nonces (bypass disabled)');
}
} else {
// Production mode - always strict policy with nonces and hashes
cspDirectives = [
"default-src 'self'",
`script-src ${baseScriptSources.join(' ')}${config.security.allowUnsafeHashes ? " 'unsafe-hashes'" : ""}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"connect-src 'self'",
"font-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'none'",
"object-src 'none'"
];
winston.debug(`Using strict production CSP policy with nonces, hashes${config.security.allowUnsafeHashes ? ", and unsafe-hashes" : ""}`);
}
// Set the CSP header with the properly formatted policy
res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
// Add other security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'no-referrer');
// If configured, add HSTS header
if (config.security && config.security.hsts) {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
}
next();
};
}
// Export the middleware
module.exports = cspMiddleware;

View File

@ -1,84 +1,170 @@
var redis = require('redis');
var winston = require('winston');
// For storing in redis
// For storing in redis or KeyDB (Redis API compatible)
// options[type] = redis
// options[host] - The host to connect to (default localhost)
// options[port] - The port to connect to (default 5379)
// options[port] - The port to connect to (default 6379)
// options[db] - The db to use (default 0)
// options[password] - The password to use (if any)
// options[expire] - The time to live for each key set (default never)
// options[connectionTimeout] - Connection timeout in ms (default 5000)
var RedisDocumentStore = function(options, client) {
this.expire = options.expire;
if (client) {
winston.info('using predefined redis client');
RedisDocumentStore.client = client;
// Check if we need to promisify the client (for redis-url 0.1.0)
if (!RedisDocumentStore.client.connect && !RedisDocumentStore.isLegacyClient) {
winston.info('using legacy redis client from redis-url');
RedisDocumentStore.isLegacyClient = true;
}
} else if (!RedisDocumentStore.client) {
winston.info('configuring redis');
winston.info('configuring redis/keydb client');
RedisDocumentStore.connect(options);
}
};
// Create a connection according to config
RedisDocumentStore.connect = function(options) {
RedisDocumentStore.connect = async function(options) {
var host = options.host || '127.0.0.1';
var port = options.port || 6379;
var index = options.db || 0;
RedisDocumentStore.client = redis.createClient(port, host);
// authenticate if password is provided
var connectionTimeout = options.connectionTimeout || 5000;
const redisOptions = {
socket: {
host: host,
port: port,
connectTimeout: connectionTimeout
},
database: index
};
// Add password if provided
if (options.password) {
RedisDocumentStore.client.auth(options.password);
redisOptions.password = options.password;
}
// Add URL if provided (overrides other options)
if (options.url) {
delete redisOptions.socket;
delete redisOptions.database;
redisOptions.url = options.url;
}
try {
RedisDocumentStore.client = redis.createClient(redisOptions);
// Set up error handling
RedisDocumentStore.client.on('error', (err) => {
winston.error('Redis/KeyDB error', { error: err });
});
// Connect the client
await RedisDocumentStore.client.connect();
winston.info('connected to redis/keydb on ' + host + ':' + port + '/' + index);
}
catch (err) {
winston.error('error connecting to redis/keydb', { error: err });
process.exit(1);
}
RedisDocumentStore.client.select(index, function(err) {
if (err) {
winston.error(
'error connecting to redis index ' + index,
{ error: err }
);
process.exit(1);
}
else {
winston.info('connected to redis on ' + host + ':' + port + '/' + index);
}
});
};
// Save file in a key
RedisDocumentStore.prototype.set = function(key, data, callback, skipExpire) {
RedisDocumentStore.prototype.set = async function(key, data, callback, skipExpire) {
var _this = this;
RedisDocumentStore.client.set(key, data, function(err) {
if (err) {
callback(false);
try {
// Handle legacy client (redis-url 0.1.0)
if (RedisDocumentStore.isLegacyClient) {
RedisDocumentStore.client.set(key, data, function(err) {
if (err) {
winston.error('error setting key', { error: err });
callback(false);
return;
}
if (!skipExpire) {
_this.setExpiration(key);
}
callback(true);
});
return;
}
else {
if (!skipExpire) {
_this.setExpiration(key);
}
callback(true);
// Modern redis 4.x client
await RedisDocumentStore.client.set(key, data);
if (!skipExpire) {
_this.setExpiration(key);
}
});
callback(true);
}
catch (err) {
winston.error('error setting key', { error: err });
callback(false);
}
};
// Expire a key in expire time if set
RedisDocumentStore.prototype.setExpiration = function(key) {
RedisDocumentStore.prototype.setExpiration = async function(key) {
if (this.expire) {
RedisDocumentStore.client.expire(key, this.expire, function(err) {
if (err) {
winston.error('failed to set expiry on key: ' + key);
try {
if (RedisDocumentStore.isLegacyClient) {
RedisDocumentStore.client.expire(key, this.expire, function(err) {
if (err) {
winston.error('failed to set expiry on key: ' + key, { error: err });
}
});
return;
}
});
await RedisDocumentStore.client.expire(key, this.expire);
}
catch (err) {
winston.error('failed to set expiry on key: ' + key, { error: err });
}
}
};
// Get a file from a key
RedisDocumentStore.prototype.get = function(key, callback, skipExpire) {
RedisDocumentStore.prototype.get = async function(key, callback, skipExpire) {
var _this = this;
RedisDocumentStore.client.get(key, function(err, reply) {
if (!err && !skipExpire) {
try {
// Handle legacy client (redis-url 0.1.0)
if (RedisDocumentStore.isLegacyClient) {
RedisDocumentStore.client.get(key, function(err, reply) {
if (err) {
winston.error('error getting key', { error: err });
callback(false);
return;
}
if (reply && !skipExpire) {
_this.setExpiration(key);
}
callback(reply);
});
return;
}
// Modern redis 4.x client
const reply = await RedisDocumentStore.client.get(key);
if (reply && !skipExpire) {
_this.setExpiration(key);
}
callback(err ? false : reply);
});
callback(reply);
}
catch (err) {
winston.error('error getting key', { error: err });
callback(false);
}
};
module.exports = RedisDocumentStore;

75
lib/template_handler.js Normal file
View File

@ -0,0 +1,75 @@
/**
* Template Handler Middleware
* Handles injecting CSP nonces into HTML templates
*/
const fs = require('fs');
const path = require('path');
const winston = require('winston');
// Cache for HTML templates
const templateCache = {};
/**
* Template handler middleware
* Preprocesses HTML templates and injects the CSP nonce
*/
function templateHandlerMiddleware(staticDir) {
// Load the index.html template at startup to avoid reading from disk on each request
try {
const indexPath = path.join(staticDir, 'index.html');
templateCache[indexPath] = fs.readFileSync(indexPath, 'utf8');
winston.debug('Loaded template: ' + indexPath);
} catch (err) {
winston.error('Failed to load index.html template at startup:', err);
}
return function(req, res, next) {
// Only process specific URLs
if (req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/)) {
const indexPath = path.join(staticDir, 'index.html');
// If template is not in cache, try to load it
if (!templateCache[indexPath]) {
try {
templateCache[indexPath] = fs.readFileSync(indexPath, 'utf8');
winston.debug('Loaded template on demand: ' + indexPath);
} catch (err) {
winston.error('Error reading index.html template:', err);
return next();
}
}
// Get the CSP nonce from the request (set by CSP middleware)
const nonce = req.cspNonce || '';
// Add debug log
console.log('Template handler processing with nonce:', nonce);
// Process the template - replace all nonce placeholders
let html;
try {
html = templateCache[indexPath].replace(/\{\{cspNonce\}\}/g, nonce);
} catch (err) {
winston.error('Error processing template:', err);
return next();
}
// Set response headers for HTML content
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Content-Length', Buffer.byteLength(html));
// Send the response
res.statusCode = 200;
res.end(html);
// Don't proceed to other middleware
return;
}
// Continue to next middleware for non-index requests
next();
};
}
module.exports = templateHandlerMiddleware;

2193
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,44 @@
{
"name": "haste",
"version": "0.1.1",
"version": "0.2.0",
"private": true,
"description": "Self Destructing Pastebin Server",
"description": "Modern Self Destructing Pastebin Server with KeyDB support",
"keywords": [
"paste",
"pastebin"
"pastebin",
"code sharing"
],
"author": {
"name": "Colin_",
"email": "hastebin@c.nixc.us",
"url": "https://colinknapp.com/"
},
"main": "haste",
"contributors": [
{
"name": "Hastebin",
"url": "https://git.nixc.us/Nixius/hastebin"
}
],
"main": "server.js",
"dependencies": {
"connect-ratelimit": "0.0.7",
"connect-route": "0.1.5",
"connect": "3.4.1",
"st": "1.1.0",
"winston": "0.6.2",
"connect": "^3.7.0",
"st": "^2.0.0",
"winston": "^3.8.2",
"redis-url": "0.1.0",
"redis": "0.8.1",
"uglify-js": "3.1.6",
"busboy": "0.2.4",
"pg": "4.1.1"
"redis": "^4.6.6",
"uglify-js": "^3.17.4",
"busboy": "^1.6.0",
"pg": "^8.11.0"
},
"devDependencies": {
"mocha": "^4.0.1"
"mocha": "^10.2.0"
},
"bundledDependencies": [],
"engines": {
"node": "8.1.4",
"npm": "5.2.0"
"node": ">=18",
"npm": ">=9"
},
"bin": {
"haste-server": "./server.js"
@ -47,5 +54,10 @@
"scripts": {
"start": "node server.js",
"test": "mocha --recursive"
}
},
"repository": {
"type": "git",
"url": "https://git.nixc.us/Nixius/hastebin"
},
"license": "AGPL-3.0-only"
}

View File

@ -1,5 +1,7 @@
var http = require('http');
var fs = require('fs');
var crypto = require('crypto');
var path = require('path');
var uglify = require('uglify-js');
var winston = require('winston');
@ -9,27 +11,31 @@ var connect_st = require('st');
var connect_rate_limit = require('connect-ratelimit');
var DocumentHandler = require('./lib/document_handler');
var cspMiddleware = require('./lib/csp');
var templateHandler = require('./lib/template_handler');
// Load the configuration and set some defaults
var config = JSON.parse(fs.readFileSync('./config.js', 'utf8'));
config.port = process.env.PORT || config.port || 7777;
config.host = process.env.HOST || config.host || 'localhost';
// Load the configuration
var config = require('./config.js');
// Set up the logger
if (config.logging) {
try {
winston.remove(winston.transports.Console);
} catch(e) {
/* was not present */
}
// Reset default logger
winston.clear();
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);
}
// Create format based on config
const logFormat = winston.format.combine(
config.logging[0].json ? winston.format.json() : winston.format.simple(),
config.logging[0].colorize ? winston.format.colorize() : winston.format.uncolorize()
);
// Configure logger
winston.configure({
level: config.logging[0].level || 'info',
format: logFormat,
transports: [
new winston.transports.Console()
]
});
}
// build the store from the config on-demand - so that we don't load it
@ -43,12 +49,21 @@ if (!config.storage.type) {
var Store, preferredStore;
if (process.env.REDISTOGO_URL && config.storage.type === 'redis') {
var redisClient = require('redis-url').connect(process.env.REDISTOGO_URL);
// Support Redis URL format for KeyDB/Redis
if ((process.env.REDIS_URL || process.env.REDISTOGO_URL) &&
(config.storage.type === 'redis' || config.storage.type === 'keydb')) {
// redis-url 0.1.0 uses a different API than newer versions
var redisUrl = require('redis-url');
var redisClient = redisUrl.connect(process.env.REDIS_URL || process.env.REDISTOGO_URL);
Store = require('./lib/document_stores/redis');
preferredStore = new Store(config.storage, redisClient);
}
else {
// If storage is set to keydb, use the redis driver (KeyDB is Redis API compatible)
if (config.storage.type === 'keydb') {
config.storage.type = 'redis';
winston.info('Using redis driver for KeyDB storage');
}
Store = require('./lib/document_stores/' + config.storage.type);
preferredStore = new Store(config.storage);
}
@ -69,18 +84,18 @@ if (config.recompressStaticAssets) {
}
// Send the static documents into the preferred store, skipping expirations
var path, data;
var docPath, 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 });
docPath = config.documents[name];
data = fs.readFileSync(docPath, 'utf8');
winston.info('loading static document', { name: name, path: docPath });
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 });
winston.warn('failed to load static document', { name: name, path: docPath });
}
}
@ -100,6 +115,33 @@ var documentHandler = new DocumentHandler({
var app = connect();
// Add CSP middleware early in the chain
app.use(cspMiddleware(config));
// Add CORS support
app.use(function(req, res, next) {
// Get origin or fallback to *
var origin = req.headers.origin;
// Check if the origin is allowed
if (config.allowedOrigins && config.allowedOrigins.length > 0) {
if (config.allowedOrigins.includes('*') || (origin && config.allowedOrigins.includes(origin))) {
res.setHeader('Access-Control-Allow-Origin', origin || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
}
// Handle preflight requests
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
next();
});
// Rate limit all requests
if (config.rateLimits) {
config.rateLimits.end = true;
@ -126,7 +168,7 @@ app.use(route(function(router) {
});
}));
// Otherwise, try to match static files
// Static file handling - this needs to come before the template handler
app.use(connect_st({
path: __dirname + '/static',
content: { maxAge: config.staticMaxAge },
@ -134,6 +176,9 @@ app.use(connect_st({
index: false
}));
// Add the template handler for index.html - only handles the main URLs
app.use(templateHandler(require('path').join(__dirname, 'static')));
// Then we can loop back - and everything else should be a token,
// so route it back to /
app.use(route(function(router) {
@ -143,7 +188,7 @@ app.use(route(function(router) {
});
}));
// And match index
// And match index as a fallback
app.use(connect_st({
path: __dirname + '/static',
content: { maxAge: config.staticMaxAge },

View File

@ -69,26 +69,34 @@ textarea {
#box2 {
background: #08323c;
font-size: 0px;
padding: 0px 5px;
}
#box1 a.logo, #box1 a.logo:visited {
display: inline-block;
background: url(logo.png);
width: 126px;
height: 42px;
}
#box1 a.logo:hover {
background-position: 0 bottom;
padding: 0;
height: 37px;
position: relative;
min-width: 160px;
display: flex;
align-items: center;
}
#box2 .function {
background: url(function-icons.png);
width: 32px;
background: url(function-icons.png) no-repeat;
height: 37px;
display: inline-block;
position: relative;
border: none;
padding: 0;
margin: 0;
background-color: transparent;
text-indent: -9999px;
overflow: hidden;
display: inline-block;
}
#box2 .button-picture {
background-color: transparent;
border: none;
padding: 0;
margin: 0;
text-indent: -9999px;
overflow: hidden;
}
#box2 .link embed {
@ -130,26 +138,63 @@ textarea {
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 button.function {
flex-shrink: 0;
}
#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 button.function + button.function {
margin-left: 0;
}
#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.save {
width: 37px;
background-position: -4px top;
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
#box2 .function.enabled.save {
background-position: -4px center;
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
#box2 .function.enabled.save:hover {
background-position: -4px 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 {
width: 37px;
background-position: -41px top;
}
#box2 .function.enabled.new {
background-position: -41px center;
}
#box2 .function.enabled.new:hover {
background-position: -41px 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; }
#box2 .function.duplicate {
width: 37px;
background-position: -78px top;
}
#box2 .function.enabled.duplicate {
background-position: -78px center;
}
#box2 .function.enabled.duplicate:hover {
background-position: -78px bottom;
}
#box2 .function.raw {
width: 37px;
background-position: -115px top;
}
#box2 .function.enabled.raw {
background-position: -115px center;
}
#box2 .function.enabled.raw:hover {
background-position: -115px bottom;
}
#messages {
position:fixed;
@ -170,3 +215,14 @@ textarea {
background:rgba(102,8,0,0.8);
}
#box1 a.logo, #box1 a.logo:visited {
display: inline-block;
background: url(logo.png);
width: 126px;
height: 42px;
}
#box1 a.logo:hover {
background-position: 0 bottom;
}

View File

@ -126,7 +126,7 @@ haste.prototype.lightKey = function() {
// Show the full key
haste.prototype.fullKey = function() {
this.configureKey(['new', 'duplicate', 'twitter', 'raw']);
this.configureKey(['new', 'save', 'duplicate', 'raw']);
};
// Set the key up for certain things to be enabled
@ -264,12 +264,15 @@ haste.prototype.configureButtons = function() {
{
$where: $('#box2 .save'),
label: 'Save',
shortcutDescription: 'control + s',
shortcut: function(evt) {
return evt.ctrlKey && (evt.keyCode === 83);
return evt.ctrlKey && evt.keyCode === 83;
},
shortcutDescription: 'control + s',
action: function() {
if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') {
if (_this.doc.locked) {
_this.duplicate();
}
else {
_this.lockDocument();
}
}
@ -293,7 +296,7 @@ haste.prototype.configureButtons = function() {
},
shortcutDescription: 'control + d',
action: function() {
_this.duplicateDocument();
_this.duplicate();
}
},
{
@ -306,17 +309,6 @@ haste.prototype.configureButtons = function() {
action: function() {
window.location.href = '/raw/' + _this.doc.key;
}
},
{
$where: $('#box2 .twitter'),
label: 'Twitter',
shortcut: function(evt) {
return _this.options.twitter && _this.doc.locked && evt.shiftKey && evt.ctrlKey && evt.keyCode == 84;
},
shortcutDescription: 'control + shift + t',
action: function() {
window.open('https://twitter.com/share?url=' + encodeURI(window.location.href));
}
}
];
for (var i = 0; i < this.buttons.length; i++) {

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html>
<head>
@ -7,13 +8,13 @@
<link rel="stylesheet" type="text/css" href="solarized_dark.css"/>
<link rel="stylesheet" type="text/css" href="application.css"/>
<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 type="text/javascript" nonce="{{cspNonce}}" src="jquery.min.js"></script>
<script type="text/javascript" nonce="{{cspNonce}}" src="highlight.min.js"></script>
<script type="text/javascript" nonce="{{cspNonce}}" src="application.min.js"></script>
<meta name="robots" content="noindex,nofollow"/>
<script type="text/javascript">
<script type="text/javascript" nonce="{{cspNonce}}">
var app = null;
// Handle pops
var handlePop = function(evt) {
@ -31,8 +32,12 @@
}, 1000);
// Construct app and load initial path
$(function() {
app = new haste('hastebin', { twitter: true });
app = new haste('hastebin', { twitter: false });
handlePop({ target: window });
// Ensure textarea gets focus after a short delay to let the UI initialize
setTimeout(function() {
$('textarea').focus();
}, 100);
});
</script>
@ -51,7 +56,6 @@
<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>

4
static/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

12
test-local.js Normal file
View File

@ -0,0 +1,12 @@
/**
* Test script for running Hastebin locally with file storage
* No need for Redis/KeyDB for local testing
*/
// Set environment variables for testing
process.env.HASTEBIN_STORAGE_TYPE = 'file';
process.env.HASTEBIN_PORT = '7777';
process.env.HASTEBIN_HOST = 'localhost';
// Run the server
require('./server.js');

20
update-js.js Normal file
View File

@ -0,0 +1,20 @@
/**
* Script to recompress static JavaScript files after changes
*/
const fs = require('fs');
const uglify = require('uglify-js');
// Read the application.js file
const applicationJs = fs.readFileSync('./static/application.js', 'utf8');
// Minify it
const minified = uglify.minify(applicationJs);
if (minified.error) {
console.error('Error minifying JavaScript:', minified.error);
process.exit(1);
}
// Write the minified file
fs.writeFileSync('./static/application.min.js', minified.code, 'utf8');
console.log('Successfully compressed application.js into application.min.js');