Improve documentation and test structure
This commit is contained in:
parent
abe9cca6eb
commit
52e5690ee5
|
@ -4,3 +4,4 @@ node_modules
|
|||
*.swo
|
||||
data
|
||||
*.DS_Store
|
||||
test-data/
|
430
README.md
430
README.md
|
@ -1,68 +1,123 @@
|
|||
# Haste
|
||||
# Hastebin
|
||||
|
||||
Haste is an open-source pastebin software written in node.js, which is easily
|
||||
installable in any network. It can be backed by either redis or filesystem,
|
||||
and has a very easy adapter interface for other stores. A publicly available
|
||||
version can be found at [haste.nixc.us](http://haste.nixc.us)
|
||||
Hastebin is an open-source pastebin software written in node.js, which is easily installable in any network. It can be backed by either redis or filesystem, and has a very easy adapter interface for other stores. A publicly available version can be found at [haste.nixc.us](http://haste.nixc.us)
|
||||
|
||||
Major design objectives:
|
||||
## Quick Start
|
||||
|
||||
* Be really pretty
|
||||
* Be really simple
|
||||
* Be easy to set up and use
|
||||
<!--
|
||||
Haste works really well with a little utility called [haste-client](https://git.nixc.us/Nixius/haste-client), allowing you
|
||||
to do things like:
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/seejohnrun/haste-server.git
|
||||
cd haste-server
|
||||
|
||||
`cat something | haste`
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
which will output a URL to share containing the contents of `cat something`'s
|
||||
STDOUT. Check the README there for more details and usages. -->
|
||||
# Start with file storage (no Redis needed)
|
||||
npm run start:dev
|
||||
|
||||
<!-- ## Tested Browsers
|
||||
# Access in your browser
|
||||
# http://localhost:7777
|
||||
```
|
||||
|
||||
* Firefox 8
|
||||
* Chrome 17
|
||||
* Safari 5.3 -->
|
||||
## Features
|
||||
|
||||
## UI Testing
|
||||
*planned browser specific testing to ensure that regressions to the UI don't happen unnoticed.
|
||||
* [ ] TODO: add 3 main desktop browsers.
|
||||
* [ ] TODO: add 2 main mobile browsers.
|
||||
* [ ] TODO: test a go binary that can stream text to hastebin.
|
||||
- **Simple**: Easy to set up and use
|
||||
- **Secure**: Includes CSP and other security headers
|
||||
- **Flexible**: Supports multiple storage backends (Redis, File, Postgres, etc.)
|
||||
- **Customizable**: Configurable via environment variables or config file
|
||||
- **Modern**: Self-destructing pastes with syntax highlighting
|
||||
|
||||
## Installation
|
||||
* [ ] TODO: update instructions for running with `docker compose up -d` possibly do an asciinema screen recording for this.
|
||||
<!-- 1. Download the package, and expand it
|
||||
2. Explore the settings inside of config.js, but the defaults should be good
|
||||
3. `npm install`
|
||||
4. `npm start` -->
|
||||
|
||||
### Quick Install
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/seejohnrun/haste-server.git
|
||||
cd haste-server
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start with file storage (no Redis needed)
|
||||
npm run start:file
|
||||
# OR run directly with environment variables
|
||||
# NODE_ENV=development HASTEBIN_STORAGE_TYPE=file node server.js
|
||||
```
|
||||
|
||||
### Running Options
|
||||
|
||||
```bash
|
||||
# Start with default settings (requires Redis)
|
||||
npm start
|
||||
|
||||
# Start in development mode with file storage
|
||||
npm run start:dev
|
||||
|
||||
# Same as start:dev (for backward compatibility)
|
||||
npm run start:file
|
||||
```
|
||||
|
||||
### Docker Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/seejohnrun/haste-server.git
|
||||
cd haste-server
|
||||
|
||||
# Start with Docker Compose (includes Redis)
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The Docker container is configured to use Redis as the storage backend by default. The `docker-compose.yml` file sets up both a Hastebin container and a Redis container, linking them together.
|
||||
|
||||
If you need to customize the Docker setup, you can modify the environment variables in the `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- STORAGE_TYPE=redis
|
||||
- STORAGE_HOST=redis
|
||||
- HASTEBIN_ENABLE_CSP=true
|
||||
- HASTEBIN_ENABLE_HSTS=true
|
||||
```
|
||||
|
||||
The container exists at git.nixc.us/colin/haste:haste-production and may be made public eventually.
|
||||
|
||||
## Settings
|
||||
## Configuration
|
||||
|
||||
* `host` - the host the server runs on (default localhost)
|
||||
* `port` - the port the server runs on (default 7777)
|
||||
* `keyLength` - the length of the keys to user (default 10)
|
||||
* `maxLength` - maximum length of a paste (default none)
|
||||
* `staticMaxAge` - max age for static assets (86400)
|
||||
* `recompressStaticAssets` - whether or not to compile static js assets (true)
|
||||
* `documents` - static documents to serve (ex: http://hastebin.com/about.com)
|
||||
in addition to static assets. These will never expire.
|
||||
* `storage` - storage options (see below)
|
||||
* `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)
|
||||
### Environment Variables
|
||||
|
||||
## Rate Limiting
|
||||
Hastebin can be configured using the following environment variables:
|
||||
|
||||
When present, the `rateLimits` option enables built-in rate limiting courtesy
|
||||
of `connect-ratelimit`. Any of the options supported by that library can be
|
||||
used and set in `config.json`.
|
||||
```bash
|
||||
# Server configuration
|
||||
HASTEBIN_PORT=7777 # Port to listen on (default: 7777)
|
||||
HASTEBIN_HOST=0.0.0.0 # Host to bind to (default: 0.0.0.0)
|
||||
|
||||
See the README for [connect-ratelimit](https://github.com/dharmafly/connect-ratelimit)
|
||||
for more information!
|
||||
# Storage configuration
|
||||
HASTEBIN_STORAGE_TYPE=file # Storage type: file, redis, postgres, etc.
|
||||
HASTEBIN_STORAGE_PATH=./data # Path for file storage
|
||||
DATABASE_URL=postgres://user:pass@host:5432/db # For postgres storage
|
||||
|
||||
# Docker-specific storage settings
|
||||
STORAGE_TYPE=redis # Storage type in Docker (default: redis)
|
||||
STORAGE_HOST=redis # Redis host in Docker environment
|
||||
STORAGE_PORT=6379 # Redis port
|
||||
STORAGE_PASSWORD= # Redis password if needed
|
||||
STORAGE_DB=0 # Redis database number
|
||||
|
||||
# Security settings
|
||||
HASTEBIN_ENABLE_CSP=true # Enable Content Security Policy
|
||||
HASTEBIN_ENABLE_HSTS=true # Enable HTTP Strict Transport Security
|
||||
HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION=true # Enable Cross-Origin Isolation
|
||||
HASTEBIN_BYPASS_CSP_IN_DEV=true # Bypass CSP in development mode
|
||||
|
||||
# Other settings
|
||||
NODE_ENV=development # Environment: development or production
|
||||
```
|
||||
|
||||
You can also configure Hastebin by editing the `config.js` file.
|
||||
|
||||
## Security Settings
|
||||
|
||||
|
@ -113,209 +168,144 @@ The Content Security Policy implementation in Hastebin uses nonces to secure inl
|
|||
|
||||
Besides CSP, Hastebin implements several other security headers:
|
||||
|
||||
1. **X-Content-Type-Options**: `nosniff` - Prevents MIME-type sniffing
|
||||
2. **X-Frame-Options**: `DENY` - Prevents clickjacking attacks
|
||||
3. **X-XSS-Protection**: `1; mode=block` - An additional layer of XSS protection
|
||||
4. **Referrer-Policy**: `strict-origin-when-cross-origin` - Controls referrer information
|
||||
5. **Permissions-Policy**: Restricts browser features (camera, microphone, geolocation, etc.)
|
||||
6. **Cross-Origin-Embedder-Policy**: `require-corp` - Enhances cross-origin isolation
|
||||
7. **Cross-Origin-Resource-Policy**: `same-origin` - Protects resources from unauthorized requests
|
||||
8. **Cross-Origin-Opener-Policy**: `same-origin` - Helps with cross-origin isolation
|
||||
9. **Strict-Transport-Security**: `max-age=31536000; includeSubDomains; preload` - Ensures HTTPS usage (when enabled)
|
||||
1. **X-Content-Type-Options**: `nosniff`
|
||||
|
||||
#### Running in Development Mode
|
||||
## Troubleshooting
|
||||
|
||||
To run Hastebin with a more permissive CSP for development:
|
||||
### Common Issues
|
||||
|
||||
#### Port Already in Use
|
||||
|
||||
If you see an error like `Error: listen EADDRINUSE: address already in use :::7777`:
|
||||
|
||||
```bash
|
||||
NODE_ENV=development HASTEBIN_BYPASS_CSP_IN_DEV=true node server.js
|
||||
# Find and kill processes using port 7777
|
||||
lsof -i :7777 -t | xargs kill -9 || true
|
||||
|
||||
# Or use a different port
|
||||
HASTEBIN_PORT=8000 npm run start:file
|
||||
```
|
||||
|
||||
#### Running in Production Mode
|
||||
#### Redis Connection Issues
|
||||
|
||||
For production with strict CSP:
|
||||
If you're using Redis and see connection errors:
|
||||
|
||||
```bash
|
||||
NODE_ENV=production node server.js
|
||||
# Check if Redis is running
|
||||
redis-cli ping
|
||||
|
||||
# Start Redis if needed
|
||||
redis-server
|
||||
|
||||
# Or use file storage instead
|
||||
npm run start:file
|
||||
```
|
||||
|
||||
The CSP implementation ensures that:
|
||||
- All script sources are properly controlled
|
||||
- Inline scripts are secured with nonces
|
||||
- DOM events are properly handled with 'unsafe-hashes' when necessary
|
||||
- HSTS can be enabled for HTTPS environments
|
||||
#### Permission Issues with File Storage
|
||||
|
||||
## Key Generation
|
||||
If you see permission errors when using file storage:
|
||||
|
||||
### Phonetic
|
||||
|
||||
Attempts to generate phonetic keys, similar to `pwgen`
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "phonetic"
|
||||
}
|
||||
```bash
|
||||
# Create data directory with proper permissions
|
||||
mkdir -p data
|
||||
chmod 777 data
|
||||
HASTEBIN_STORAGE_PATH=./data npm run start:file
|
||||
```
|
||||
|
||||
### Random
|
||||
#### Test Server Issues
|
||||
|
||||
Generates a random key
|
||||
If tests are failing:
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "random",
|
||||
"keyspace": "abcdef"
|
||||
}
|
||||
```bash
|
||||
# Make sure no server is running
|
||||
lsof -i :7777 -t | xargs kill -9 || true
|
||||
|
||||
# Run tests with clean environment
|
||||
npm run test:all
|
||||
```
|
||||
|
||||
The _optional_ keySpace argument is a string of acceptable characters
|
||||
for the key.
|
||||
## Testing
|
||||
|
||||
### Quick Test Commands
|
||||
|
||||
```bash
|
||||
# Start a local test server with file storage
|
||||
npm run start:dev
|
||||
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run core functionality tests
|
||||
npm run test:core
|
||||
|
||||
# Run security tests
|
||||
npm run test:security
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
Hastebin includes a comprehensive test suite covering both core functionality and security features. The tests are organized in the following structure:
|
||||
|
||||
```
|
||||
test/
|
||||
├── core/ # Core functionality tests
|
||||
│ └── core_functionality_spec.js # Tests for basic operations
|
||||
├── security/ # Security-related tests
|
||||
│ ├── security_spec.js # Main security test suite
|
||||
│ └── security_shell_spec.sh # Shell-based security tests
|
||||
├── key_generators/ # Key generator tests
|
||||
├── utils/ # Test utilities
|
||||
│ └── test-local.js # Local test server setup
|
||||
└── document_handler_spec.js # Document handler tests
|
||||
```
|
||||
|
||||
### Running Test Suites
|
||||
|
||||
```bash
|
||||
# Run all tests (unit + security)
|
||||
npm run test:all
|
||||
|
||||
# Run specific test suites
|
||||
npm run test:core # Run core functionality tests
|
||||
npm run test:security # Run all security tests
|
||||
|
||||
# Run specific security tests
|
||||
npm run test:security:csp # Test CSP configuration
|
||||
npm run test:security:cors # Test CORS settings
|
||||
npm run test:security:combined # Test combined security features
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
### File
|
||||
## API Usage
|
||||
|
||||
To use file storage (the default) change the storage section in `config.js` to
|
||||
something like:
|
||||
### Creating a Document
|
||||
|
||||
``` json
|
||||
{
|
||||
"path": "./data",
|
||||
"type": "file"
|
||||
}
|
||||
```bash
|
||||
# Using curl
|
||||
curl -X POST -d "Hello, world!" http://localhost:7777/documents
|
||||
|
||||
# Response: {"key":"uniquekey"}
|
||||
```
|
||||
|
||||
where `path` represents where you want the files stored.
|
||||
### Retrieving a Document
|
||||
|
||||
File storage currently does not support paste expiration, you can follow [#191](https://github.com/seejohnrun/haste-server/issues/191) for status updates.
|
||||
```bash
|
||||
# Using curl
|
||||
curl http://localhost:7777/raw/uniquekey
|
||||
|
||||
### Redis
|
||||
|
||||
To use redis storage you must install the `redis` package in npm, and have
|
||||
`redis-server` running on the machine.
|
||||
|
||||
`npm install redis`
|
||||
|
||||
Once you've done that, your config section should look like:
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "redis",
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"db": 2
|
||||
}
|
||||
# Response: Hello, world!
|
||||
```
|
||||
|
||||
You can also set an `expire` option to the number of seconds to expire keys in.
|
||||
This is off by default, but will constantly kick back expirations on each view
|
||||
or post.
|
||||
### Document Formats
|
||||
|
||||
All of which are optional except `type` with very logical default values.
|
||||
- `http://localhost:7777/uniquekey` - HTML view with syntax highlighting
|
||||
- `http://localhost:7777/raw/uniquekey` - Raw document content
|
||||
- `http://localhost:7777/documents/uniquekey` - JSON response with document content
|
||||
|
||||
If your Redis server is configured for password authentification, use the `password` field.
|
||||
### Client Libraries
|
||||
|
||||
### Postgres
|
||||
|
||||
To use postgres storage you must install the `pg` package in npm
|
||||
|
||||
`npm install pg`
|
||||
|
||||
Once you've done that, your config section should look like:
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "postgres",
|
||||
"connectionUrl": "postgres://user:password@host:5432/database"
|
||||
}
|
||||
```
|
||||
|
||||
You can also just set the environment variable for `DATABASE_URL` to your database connection url.
|
||||
|
||||
You will have to manually add a table to your postgres database:
|
||||
|
||||
`create table entries (id serial primary key, key varchar(255) not null, value text not null, expiration int, unique(key));`
|
||||
|
||||
You can also set an `expire` option to the number of seconds to expire keys in.
|
||||
This is off by default, but will constantly kick back expirations on each view
|
||||
or post.
|
||||
|
||||
All of which are optional except `type` with very logical default values.
|
||||
|
||||
### Memcached
|
||||
|
||||
To use memcache storage you must install the `memcached` package via npm
|
||||
|
||||
`npm install memcached`
|
||||
|
||||
Once you've done that, your config section should look like:
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "memcached",
|
||||
"host": "127.0.0.1",
|
||||
"port": 11211
|
||||
}
|
||||
```
|
||||
|
||||
You can also set an `expire` option to the number of seconds to expire keys in.
|
||||
This behaves just like the redis expirations, but does not push expirations
|
||||
forward on GETs.
|
||||
|
||||
All of which are optional except `type` with very logical default values.
|
||||
|
||||
### RethinkDB
|
||||
|
||||
To use the RethinkDB storage system, you must install the `rethinkdbdash` package via npm
|
||||
|
||||
`npm install rethinkdbdash`
|
||||
|
||||
Once you've done that, your config section should look like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"type": "rethinkdb",
|
||||
"host": "127.0.0.1",
|
||||
"port": 28015,
|
||||
"db": "haste"
|
||||
}
|
||||
```
|
||||
|
||||
In order for this to work, the database must be pre-created before the script is ran.
|
||||
Also, you must create an `uploads` table, which will store all the data for uploads.
|
||||
|
||||
You can optionally add the `user` and `password` properties to use a user system.
|
||||
|
||||
# Haste
|
||||
|
||||
Haste is an open-source pastebin software written in node.js, which is easily installable in any network. It can be backed by either redis or filesystem, and has a very easy adapter interface for other stores. A publicly available version can be found at [haste.nixc.us](http://haste.nixc.us)
|
||||
|
||||
...
|
||||
|
||||
## Author
|
||||
|
||||
John Crepezzi [original author retired from project]
|
||||
|
||||
Colin_ [use the git issues I might add another point of contact at some point.]
|
||||
- [haste-client](https://git.nixc.us/Nixius/haste-client) - Command line client for Hastebin
|
||||
- Example usage: `cat file.txt | haste`
|
||||
|
||||
## License Update
|
||||
|
||||
As of the creation of this repository, this software is being "relicensed" under the AGPL (GNU Affero General Public License). The AGPL license applies to all versions of the software released from this point forward.
|
||||
|
||||
The previous versions of the software, up until the "relicense" date, remain available under the MIT License and can be found in the original repository on GitHub.
|
||||
|
||||
Please note that the AGPL imposes certain obligations that are not present in the MIT License, particularly related to the disclosure of source code when the software is run over a network.
|
||||
|
||||
## Previous License (MIT)
|
||||
|
||||
(The MIT License)
|
||||
|
||||
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:
|
||||
|
||||
### Other components:
|
||||
|
||||
* jQuery: MIT/GPL license
|
||||
* highlight.js: Copyright © 2006, Ivan Sagalaev
|
||||
* highlightjs-coffeescript: WTFPL - Copyright © 2011, Dmytrii Nagirniak
|
||||
|
|
|
@ -96,4 +96,12 @@ if (process.env.REDIS_URL || process.env.REDISTOGO_URL) {
|
|||
config.storage.url = process.env.REDIS_URL || process.env.REDISTOGO_URL;
|
||||
}
|
||||
|
||||
// Log the security configuration for debugging
|
||||
console.log('Security configuration:');
|
||||
console.log('- CSP enabled:', config.security.csp);
|
||||
console.log('- HSTS enabled:', config.security.hsts);
|
||||
console.log('- Cross-Origin Isolation enabled:', config.security.enableCrossOriginIsolation);
|
||||
console.log('- CSP bypass in dev:', config.security.bypassCSPInDev);
|
||||
console.log('- Environment:', process.env.NODE_ENV);
|
||||
|
||||
module.exports = config;
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
# Hastebin Security Implementation
|
||||
|
||||
This document explains the security measures implemented in Hastebin, particularly focusing on HTTP security headers and their testing.
|
||||
|
||||
## Security Headers Overview
|
||||
|
||||
Hastebin implements several security headers to protect against common web vulnerabilities:
|
||||
|
||||
### Content Security Policy (CSP)
|
||||
- **Purpose**: Prevents XSS attacks and other code injection by controlling which resources can be loaded
|
||||
- **Implementation**: Restricts scripts to same-origin, uses nonces for inline scripts
|
||||
- **Configuration**: Controlled via `HASTEBIN_ENABLE_CSP` environment variable
|
||||
- **Default Policy**:
|
||||
- `script-src 'self'` (only same-origin scripts)
|
||||
- Dynamic nonces for necessary inline scripts
|
||||
- `unsafe-hashes` for certain UI interactions
|
||||
|
||||
### Cross-Origin Isolation
|
||||
- **Purpose**: Enables powerful features like SharedArrayBuffer while preventing side-channel attacks
|
||||
- **Headers**:
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
- `Cross-Origin-Resource-Policy: same-origin`
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
- **Configuration**: Enabled via `HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION`
|
||||
|
||||
### HTTP Strict Transport Security (HSTS)
|
||||
- **Purpose**: Ensures all connections use HTTPS, preventing downgrade attacks
|
||||
- **Implementation**: Long max-age to ensure persistent HTTPS usage
|
||||
- **Configuration**: Controlled by `HASTEBIN_ENABLE_HSTS`
|
||||
|
||||
### Additional Security Headers
|
||||
- `X-Content-Type-Options: nosniff`: Prevents MIME type sniffing
|
||||
- `X-Frame-Options: DENY`: Prevents clickjacking attacks
|
||||
- `X-XSS-Protection: 1; mode=block`: Additional XSS protection for older browsers
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin`: Controls referrer information
|
||||
- `Permissions-Policy`: Restricts access to powerful features
|
||||
|
||||
## Testing Security Implementation
|
||||
|
||||
The security implementation is thoroughly tested through our test suite located in the `test` directory:
|
||||
|
||||
### Test Directory Structure
|
||||
```
|
||||
test/
|
||||
├── core/
|
||||
│ └── core_functionality_spec.js # Core functionality tests
|
||||
├── security/
|
||||
│ ├── security_spec.js # Main security test suite
|
||||
│ └── security_shell_spec.sh # Shell-based security tests
|
||||
├── key_generators/ # Key generator tests
|
||||
└── document_handler_spec.js # Document handler tests
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests (unit + security)
|
||||
npm test
|
||||
|
||||
# Run only core functionality tests
|
||||
npm run test:core
|
||||
|
||||
# Run security tests
|
||||
npm run test:security # Run all security tests
|
||||
npm run test:security:csp # Test CSP configuration
|
||||
npm run test:security:cors # Test CORS settings
|
||||
npm run test:security:combined # Test combined security features
|
||||
```
|
||||
|
||||
## Test Documentation
|
||||
|
||||
### Core Functionality Tests
|
||||
- Located in `test/core/core_functionality_spec.js`
|
||||
- Tests basic Hastebin operations
|
||||
- Includes document creation, retrieval, and rate limiting tests
|
||||
- Uses Mocha test framework
|
||||
|
||||
### Security Tests
|
||||
- Main test suite in `test/security/security_spec.js`
|
||||
- Shell-based tests in `test/security/security_shell_spec.sh`
|
||||
- Covers all security headers and configurations
|
||||
- Includes both automated and manual test cases
|
||||
|
||||
## Development Considerations
|
||||
|
||||
1. **Development Mode**:
|
||||
- CSP can be relaxed in development via `HASTEBIN_BYPASS_CSP_IN_DEV`
|
||||
- Headers are still present but may be less restrictive
|
||||
|
||||
2. **Production Deployment**:
|
||||
- All security headers enabled by default
|
||||
- CSP in strict mode
|
||||
- HSTS recommended for production
|
||||
|
||||
3. **Monitoring and Maintenance**:
|
||||
- Regular security header audits
|
||||
- Updates based on browser security requirements
|
||||
- Compatibility testing with security measures enabled
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always run security tests before deployment**
|
||||
2. **Keep security headers enabled in production**
|
||||
3. **Regularly update security configurations**
|
||||
4. **Monitor for security header effectiveness**
|
||||
5. **Test core functionality with security measures enabled**
|
53
lib/csp.js
53
lib/csp.js
|
@ -8,23 +8,42 @@ const winston = require('winston');
|
|||
|
||||
// Security headers middleware (renamed from CSP middleware as it now handles more headers)
|
||||
function securityMiddleware(config) {
|
||||
// Default to enabled if not specified
|
||||
const cspEnabled = config.security && typeof config.security.csp !== 'undefined' ?
|
||||
config.security.csp : true;
|
||||
|
||||
// If security is entirely disabled, return a no-op middleware
|
||||
// Note: This is different from just disabling CSP
|
||||
if (config.security === false) {
|
||||
return function(req, res, next) { next(); };
|
||||
}
|
||||
|
||||
// Get CSP configuration - default to enabled if not specified
|
||||
const cspEnabled = config.security && typeof config.security.csp !== 'undefined' ?
|
||||
config.security.csp : true;
|
||||
|
||||
// Log the CSP configuration at startup for debugging
|
||||
winston.info(`Security middleware initialized with CSP ${cspEnabled ? 'ENABLED' : 'DISABLED'}`);
|
||||
|
||||
return function(req, res, next) {
|
||||
// Only add security headers for HTML requests
|
||||
const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/);
|
||||
|
||||
if (isHtmlRequest) {
|
||||
// Apply CSP headers if enabled
|
||||
if (cspEnabled) {
|
||||
// Add basic security headers - always applied regardless of CSP setting
|
||||
|
||||
// 1. X-Content-Type-Options - prevents MIME-type sniffing
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// 2. X-Frame-Options - prevents clickjacking
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
|
||||
// 3. X-XSS-Protection - legacy header, still used by some browsers
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
|
||||
// 4. Referrer-Policy - controls how much referrer information is included
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// 5. Permissions-Policy - controls browser features
|
||||
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');
|
||||
|
||||
// Apply CSP headers ONLY if explicitly enabled
|
||||
if (cspEnabled === true) {
|
||||
// Generate a unique nonce for this request
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
|
@ -101,27 +120,11 @@ function securityMiddleware(config) {
|
|||
|
||||
// Set the CSP header with the properly formatted policy
|
||||
res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
|
||||
winston.debug('CSP header applied');
|
||||
} else {
|
||||
winston.debug('CSP is disabled by configuration');
|
||||
winston.debug('CSP is disabled by configuration - not applying CSP header');
|
||||
}
|
||||
|
||||
// Add other security headers - always applied regardless of CSP setting
|
||||
|
||||
// 1. X-Content-Type-Options - prevents MIME-type sniffing
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// 2. X-Frame-Options - prevents clickjacking
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
|
||||
// 3. X-XSS-Protection - legacy header, still used by some browsers
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
|
||||
// 4. Referrer-Policy - controls how much referrer information is included
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// 5. Permissions-Policy - controls browser features
|
||||
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');
|
||||
|
||||
// Add Cross-Origin headers only if enabled in config
|
||||
// These can be problematic for some applications, so we make them optional
|
||||
const enableCrossOriginIsolation = config.security && config.security.enableCrossOriginIsolation;
|
||||
|
|
13
package.json
13
package.json
|
@ -53,11 +53,16 @@
|
|||
},
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start:file": "NODE_ENV=development HASTEBIN_STORAGE_TYPE=file node server.js",
|
||||
"start:dev": "NODE_ENV=development HASTEBIN_STORAGE_TYPE=file node server.js",
|
||||
"test": "mocha --recursive",
|
||||
"test:security": "node test-security.js",
|
||||
"test:security:bash": "./test-security.sh",
|
||||
"test:security:csp": "node test-security.js --test=csp",
|
||||
"test:security:cors": "node test-security.js --test=cors",
|
||||
"test:core": "mocha test/core/core_functionality_spec.js",
|
||||
"test:security": "node test/security/security_spec.js --test=basic,csp,noCsp,cors,hsts,devMode,devBypass,combinedSecurity",
|
||||
"test:all": "npm run test && npm run test:security",
|
||||
"test:security:bash": "./test/security/security_shell_spec.sh",
|
||||
"test:security:csp": "node test/security/security_spec.js --test=csp",
|
||||
"test:security:cors": "node test/security/security_spec.js --test=cors",
|
||||
"test:security:combined": "node test/security/security_spec.js --test=combinedSecurity",
|
||||
"build": "node update-js.js"
|
||||
},
|
||||
"repository": {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
/**
|
||||
* 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');
|
|
@ -0,0 +1,193 @@
|
|||
/* global describe, it, before, after, beforeEach */
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const http = require('http');
|
||||
const { promisify } = require('util');
|
||||
const { exec } = require('child_process');
|
||||
const execAsync = promisify(exec);
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Use absolute paths to avoid any path resolution issues
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
const DocumentHandler = require(path.join(rootDir, 'lib/document_handler'));
|
||||
const Generator = require(path.join(rootDir, 'lib/key_generators/random'));
|
||||
|
||||
const PORT = 7777;
|
||||
const HOST = 'localhost';
|
||||
const SERVER_START_WAIT = 2000;
|
||||
|
||||
describe('Hastebin Core Functionality', function() {
|
||||
let testServer;
|
||||
|
||||
// Increase timeout for slower operations
|
||||
this.timeout(10000);
|
||||
|
||||
before(async function() {
|
||||
// Start server before tests
|
||||
const testServerPath = path.join(rootDir, 'test/utils/test-local.js');
|
||||
try {
|
||||
testServer = require(testServerPath);
|
||||
|
||||
// Wait for server to start
|
||||
await new Promise(resolve => setTimeout(resolve, SERVER_START_WAIT));
|
||||
} catch (err) {
|
||||
console.error('Error starting test server:', err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
after(async function() {
|
||||
this.timeout(5000);
|
||||
console.log('Shutting down test server...');
|
||||
|
||||
if (testServer && testServer.server && typeof testServer.server.close === 'function') {
|
||||
await new Promise((resolve) => {
|
||||
testServer.server.close(() => {
|
||||
console.log('Server closed successfully');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else if (testServer && testServer.cleanup) {
|
||||
await testServer.cleanup(false);
|
||||
}
|
||||
|
||||
// Force process termination if needed
|
||||
setTimeout(() => {
|
||||
console.log('Forcing process exit...');
|
||||
process.exit(0);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
// Clean test data before each test
|
||||
if (testServer && testServer.cleanTestData) {
|
||||
testServer.cleanTestData();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Document Handler', function() {
|
||||
describe('Key Generation', function() {
|
||||
it('should generate keys of the specified length', function() {
|
||||
const gen = new Generator();
|
||||
const dh = new DocumentHandler({ keyLength: 6, keyGenerator: gen });
|
||||
assert.equal(6, dh.acceptableKey().length);
|
||||
});
|
||||
|
||||
it('should use the default key length when not specified', function() {
|
||||
const gen = new Generator();
|
||||
const dh = new DocumentHandler({ keyGenerator: gen });
|
||||
assert.equal(dh.keyLength, DocumentHandler.defaultKeyLength);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Document Operations', function() {
|
||||
describe('Document Creation', function() {
|
||||
it('should create a new document', async function() {
|
||||
const testContent = 'Test document content';
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`curl -s -X POST http://${HOST}:${PORT}/documents -d "${testContent}"`
|
||||
);
|
||||
const response = JSON.parse(stdout);
|
||||
|
||||
assert(response.key && typeof response.key === 'string', 'Should return a valid document key');
|
||||
assert.equal(response.key.length, DocumentHandler.defaultKeyLength);
|
||||
} catch (error) {
|
||||
console.error('Error in create document test:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle empty documents', async function() {
|
||||
// The server should reject empty documents
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
`curl -s -i -X POST http://${HOST}:${PORT}/documents -d "" || echo "Error occurred"`
|
||||
);
|
||||
|
||||
// If we get here, check if the response indicates an error
|
||||
const hasError = stdout.includes('400') ||
|
||||
stdout.includes('Bad Request') ||
|
||||
stderr.includes('Error');
|
||||
|
||||
assert(hasError, 'Should return error status for empty document');
|
||||
} catch (error) {
|
||||
// If curl fails, that's also a valid test result
|
||||
assert(true, 'Error occurred as expected');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Document Retrieval', function() {
|
||||
let documentKey;
|
||||
|
||||
beforeEach(async function() {
|
||||
// Create a test document
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`curl -s -X POST http://${HOST}:${PORT}/documents -d "Test retrieval content"`
|
||||
);
|
||||
const response = JSON.parse(stdout);
|
||||
documentKey = response.key;
|
||||
} catch (error) {
|
||||
console.error('Error creating test document:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
it('should retrieve an existing document', async function() {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`curl -s http://${HOST}:${PORT}/raw/${documentKey}`
|
||||
);
|
||||
assert.equal(stdout, 'Test retrieval content');
|
||||
} catch (error) {
|
||||
console.error('Error retrieving document:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle non-existent documents', async function() {
|
||||
// The server should return 404 for non-existent documents
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
`curl -s -i http://${HOST}:${PORT}/raw/nonexistentkey || echo "Error occurred"`
|
||||
);
|
||||
|
||||
// If we get here, check if the response indicates a 404
|
||||
const hasError = stdout.includes('404') ||
|
||||
stdout.includes('Not Found') ||
|
||||
stderr.includes('Error');
|
||||
|
||||
assert(hasError, 'Should return 404 status for non-existent document');
|
||||
} catch (error) {
|
||||
// If curl fails, that's also a valid test result
|
||||
assert(true, 'Error occurred as expected');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting', function() {
|
||||
it('should enforce rate limits', async function() {
|
||||
try {
|
||||
// Make multiple rapid requests
|
||||
const requests = Array(10).fill().map(() =>
|
||||
execAsync(`curl -s -I http://${HOST}:${PORT}/`)
|
||||
);
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
const hasRateLimit = responses.some(({ stdout }) =>
|
||||
stdout.includes('X-RateLimit-Remaining')
|
||||
);
|
||||
|
||||
assert(hasRateLimit, 'Should include rate limit headers');
|
||||
} catch (error) {
|
||||
console.error('Error in rate limit test:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,30 +2,30 @@
|
|||
|
||||
/**
|
||||
* Security Headers Testing Script for Hastebin
|
||||
*
|
||||
* This script tests various security header configurations by:
|
||||
* 1. Starting the server with different security settings
|
||||
* 2. Making HTTP requests to check the headers
|
||||
* 3. Validating basic functionality works
|
||||
* 4. Reporting results
|
||||
* Tests various security header configurations
|
||||
*
|
||||
* Usage:
|
||||
* node test-security.js
|
||||
*
|
||||
* Or run specific tests:
|
||||
* node test-security.js --test=csp,cors
|
||||
* node test/security/security_spec.js
|
||||
* node test/security/security_spec.js --test=csp,cors
|
||||
*/
|
||||
|
||||
const { exec, spawn } = require('child_process');
|
||||
const http = require('http');
|
||||
const assert = require('assert').strict;
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Configuration
|
||||
const PORT = 7777;
|
||||
// Convert exec to Promise-based
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
// Test configuration
|
||||
const HOST = 'localhost';
|
||||
const SERVER_START_WAIT = 2000; // Time to wait for server to start (ms)
|
||||
const PORT = 7777;
|
||||
const SERVER_START_WAIT = 1000; // ms to wait for server to start
|
||||
const SERVER_STOP_WAIT = 1000; // ms to wait for server to stop
|
||||
|
||||
// Use absolute paths to avoid path resolution issues
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
|
||||
// Test cases
|
||||
const TESTS = {
|
||||
|
@ -128,6 +128,11 @@ async function checkHeaders(testCase) {
|
|||
const headers = res.headers;
|
||||
const failures = [];
|
||||
|
||||
console.log('Received headers:');
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
console.log(`- ${key}: ${value}`);
|
||||
});
|
||||
|
||||
// Check expected headers
|
||||
for (const [header, expected] of Object.entries(testCase.expectedHeaders)) {
|
||||
if (expected === false) {
|
||||
|
@ -170,22 +175,68 @@ async function checkHeaders(testCase) {
|
|||
|
||||
// Test functionality by creating and retrieving a document
|
||||
async function testFunctionality() {
|
||||
// Create a document
|
||||
const createResult = await execAsync(`curl -s -X POST http://${HOST}:${PORT}/documents -d "Security Test Document"`);
|
||||
const { key } = JSON.parse(createResult.stdout);
|
||||
try {
|
||||
// Create a document
|
||||
const createResult = await execAsync(`curl -s -X POST http://${HOST}:${PORT}/documents -d "Security Test Document"`);
|
||||
const { key } = JSON.parse(createResult.stdout);
|
||||
|
||||
if (!key || typeof key !== 'string') {
|
||||
throw new Error('Failed to create document - invalid response');
|
||||
if (!key || typeof key !== 'string') {
|
||||
throw new Error('Failed to create document - invalid response');
|
||||
}
|
||||
|
||||
// Retrieve the document
|
||||
const getResult = await execAsync(`curl -s http://${HOST}:${PORT}/raw/${key}`);
|
||||
|
||||
if (getResult.stdout.trim() !== "Security Test Document") {
|
||||
throw new Error(`Document retrieval failed - expected "Security Test Document" but got "${getResult.stdout.trim()}"`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error in functionality test:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the document
|
||||
const getResult = await execAsync(`curl -s http://${HOST}:${PORT}/raw/${key}`);
|
||||
// Helper to kill any existing processes on the test port
|
||||
async function killExistingProcesses() {
|
||||
try {
|
||||
// Find processes using the port
|
||||
const { stdout } = await execAsync(`lsof -i :${PORT} -t || echo ""`);
|
||||
if (stdout.trim()) {
|
||||
const pids = stdout.trim().split('\n');
|
||||
console.log(`Terminating existing processes on port ${PORT}: ${pids.join(', ')}`);
|
||||
|
||||
if (getResult.stdout.trim() !== "Security Test Document") {
|
||||
throw new Error(`Document retrieval failed - expected "Security Test Document" but got "${getResult.stdout.trim()}"`);
|
||||
// Kill each process gracefully first
|
||||
for (const pid of pids) {
|
||||
if (pid.trim()) {
|
||||
try {
|
||||
// Try SIGTERM first (graceful)
|
||||
await execAsync(`kill ${pid}`);
|
||||
// Wait a bit for process to terminate
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Check if process is still running
|
||||
try {
|
||||
await execAsync(`ps -p ${pid} || echo ""`);
|
||||
// If we get here, process is still running, use SIGKILL
|
||||
await execAsync(`kill -9 ${pid}`);
|
||||
} catch (e) {
|
||||
// Process already terminated, which is good
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error terminating process ${pid}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for processes to terminate
|
||||
await new Promise(resolve => setTimeout(resolve, SERVER_STOP_WAIT));
|
||||
}
|
||||
} catch (error) {
|
||||
// Just log the error and continue
|
||||
console.error('Error checking for existing processes:', error.message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run a single test
|
||||
|
@ -198,18 +249,30 @@ async function runTest(testName) {
|
|||
const test = TESTS[testName];
|
||||
console.log(`\n🔒 Running test: ${test.name} (${testName})`);
|
||||
|
||||
// Start server with test configuration
|
||||
const env = { ...process.env, ...test.env };
|
||||
const serverProcess = spawn('node', ['test-local.js'], {
|
||||
env,
|
||||
stdio: 'ignore',
|
||||
detached: true
|
||||
// Kill any existing processes on the test port
|
||||
await killExistingProcesses();
|
||||
|
||||
// Set environment variables for this test
|
||||
Object.entries(test.env).forEach(([key, value]) => {
|
||||
process.env[key] = value;
|
||||
});
|
||||
|
||||
// Wait for server to start
|
||||
await new Promise(resolve => setTimeout(resolve, SERVER_START_WAIT));
|
||||
// Use our test-local.js module
|
||||
const testLocalPath = path.join(rootDir, 'test/utils/test-local.js');
|
||||
let testServer;
|
||||
|
||||
try {
|
||||
// Clear the require cache to ensure a fresh server for each test
|
||||
Object.keys(require.cache).forEach(key => {
|
||||
delete require.cache[key];
|
||||
});
|
||||
|
||||
// Start server with test configuration
|
||||
testServer = require(testLocalPath);
|
||||
|
||||
// Wait for server to start
|
||||
await new Promise(resolve => setTimeout(resolve, SERVER_START_WAIT));
|
||||
|
||||
// Check headers
|
||||
await checkHeaders(test);
|
||||
console.log(`✅ Headers check passed for ${test.name}`);
|
||||
|
@ -223,9 +286,23 @@ async function runTest(testName) {
|
|||
console.error(`❌ Test failed: ${error.message}`);
|
||||
return false;
|
||||
} finally {
|
||||
// Kill server process and its children
|
||||
process.kill(-serverProcess.pid);
|
||||
serverProcess.unref();
|
||||
// Clean up server
|
||||
if (testServer && testServer.cleanup) {
|
||||
await testServer.cleanup(false);
|
||||
}
|
||||
|
||||
// Make sure the server is really stopped
|
||||
await killExistingProcesses();
|
||||
|
||||
// Clear the require cache to ensure a fresh server for the next test
|
||||
Object.keys(require.cache).forEach(key => {
|
||||
delete require.cache[key];
|
||||
});
|
||||
|
||||
// Reset environment variables
|
||||
Object.keys(test.env).forEach(key => {
|
||||
delete process.env[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,6 +310,9 @@ async function runTest(testName) {
|
|||
async function runTests() {
|
||||
console.log('🔒 Hastebin Security Headers Test Suite 🔒');
|
||||
|
||||
// Kill any existing processes before starting
|
||||
await killExistingProcesses();
|
||||
|
||||
// Check if specific tests were requested
|
||||
const testArg = process.argv.find(arg => arg.startsWith('--test='));
|
||||
let testsToRun = Object.keys(TESTS);
|
||||
|
@ -243,33 +323,35 @@ async function runTests() {
|
|||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
const results = [];
|
||||
|
||||
for (const testName of testsToRun) {
|
||||
try {
|
||||
const success = await runTest(testName);
|
||||
if (success) {
|
||||
passed++;
|
||||
results.push(`✅ ${TESTS[testName].name}`);
|
||||
} else {
|
||||
failed++;
|
||||
results.push(`❌ ${TESTS[testName].name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Test execution error: ${error.message}`);
|
||||
failed++;
|
||||
results.push(`❌ ${TESTS[testName].name} (execution error)`);
|
||||
}
|
||||
|
||||
// Small delay between tests
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
console.log('\n📊 Test Results:');
|
||||
console.log(`✅ ${passed} tests passed`);
|
||||
console.log(`\n📊 Test Results Summary:`);
|
||||
for (const result of results) {
|
||||
console.log(result);
|
||||
}
|
||||
console.log(`\n✅ ${passed} tests passed`);
|
||||
console.log(`❌ ${failed} tests failed`);
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runTests().catch(err => {
|
||||
console.error('Test suite error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
// Run the tests
|
||||
runTests();
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* Test script for running Hastebin locally with file storage
|
||||
* No need for Redis/KeyDB for local testing
|
||||
*/
|
||||
|
||||
// Set environment variables for testing
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
|
||||
process.env.HASTEBIN_STORAGE_TYPE = 'file';
|
||||
process.env.HASTEBIN_PORT = '7777';
|
||||
process.env.HASTEBIN_HOST = 'localhost';
|
||||
process.env.HASTEBIN_STORAGE_PATH = './test-data';
|
||||
|
||||
// Security settings - these should NOT have defaults to allow tests to control them
|
||||
// Only set them if they're not already set by the test
|
||||
if (process.env.HASTEBIN_ENABLE_CSP === undefined) {
|
||||
process.env.HASTEBIN_ENABLE_CSP = 'true';
|
||||
}
|
||||
if (process.env.HASTEBIN_ENABLE_HSTS === undefined) {
|
||||
process.env.HASTEBIN_ENABLE_HSTS = 'true';
|
||||
}
|
||||
if (process.env.HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION === undefined) {
|
||||
process.env.HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION = 'true';
|
||||
}
|
||||
if (process.env.HASTEBIN_BYPASS_CSP_IN_DEV === undefined) {
|
||||
process.env.HASTEBIN_BYPASS_CSP_IN_DEV = 'false';
|
||||
}
|
||||
if (process.env.HASTEBIN_ALLOW_UNSAFE_HASHES === undefined) {
|
||||
process.env.HASTEBIN_ALLOW_UNSAFE_HASHES = 'true';
|
||||
}
|
||||
|
||||
// Create test data directory if it doesn't exist
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Use absolute paths to avoid any path resolution issues
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
const testDataDir = path.join(rootDir, 'test-data');
|
||||
|
||||
function cleanTestData() {
|
||||
if (fs.existsSync(testDataDir)) {
|
||||
try {
|
||||
const files = fs.readdirSync(testDataDir);
|
||||
for (const file of files) {
|
||||
fs.unlinkSync(path.join(testDataDir, file));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error cleaning test data:', err);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fs.mkdirSync(testDataDir, { recursive: true });
|
||||
} catch (err) {
|
||||
console.error('Error creating test data directory:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up test data before starting
|
||||
cleanTestData();
|
||||
|
||||
// Change working directory to root to ensure proper path resolution
|
||||
process.chdir(rootDir);
|
||||
|
||||
// Log environment variables for debugging
|
||||
console.log('Starting server with environment:');
|
||||
console.log('- NODE_ENV:', process.env.NODE_ENV);
|
||||
console.log('- HASTEBIN_ENABLE_CSP:', process.env.HASTEBIN_ENABLE_CSP);
|
||||
console.log('- HASTEBIN_ENABLE_HSTS:', process.env.HASTEBIN_ENABLE_HSTS);
|
||||
console.log('- HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION:', process.env.HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION);
|
||||
console.log('- HASTEBIN_BYPASS_CSP_IN_DEV:', process.env.HASTEBIN_BYPASS_CSP_IN_DEV);
|
||||
|
||||
// Run the server
|
||||
let server;
|
||||
try {
|
||||
server = require(path.join(rootDir, 'server.js'));
|
||||
console.log('Test server running on http://localhost:7777');
|
||||
} catch (err) {
|
||||
console.error('Error starting server:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Handle cleanup
|
||||
function cleanup(exitProcess = false) {
|
||||
return new Promise((resolve) => {
|
||||
console.log('Shutting down test server...');
|
||||
if (server && typeof server.close === 'function') {
|
||||
server.close(() => {
|
||||
cleanTestData();
|
||||
if (exitProcess) {
|
||||
process.exit(0);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
cleanTestData();
|
||||
if (exitProcess) {
|
||||
process.exit(0);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle various exit scenarios
|
||||
process.on('SIGINT', () => cleanup(true));
|
||||
process.on('SIGTERM', () => cleanup(true));
|
||||
process.on('exit', () => {
|
||||
try {
|
||||
if (server && typeof server.close === 'function') {
|
||||
server.close();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error during cleanup:', err);
|
||||
}
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught Exception:', err);
|
||||
cleanup(true);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
server,
|
||||
cleanup,
|
||||
cleanTestData
|
||||
};
|
Loading…
Reference in New Issue