Compare commits
No commits in common. 'gh-pages' and 'master' have entirely different histories.
50 changed files with 4335 additions and 385 deletions
@ -0,0 +1,8 @@ |
|||||||
|
Dockerfile |
||||||
|
.git |
||||||
|
npm-debug.log |
||||||
|
node_modules |
||||||
|
*.swp |
||||||
|
*.swo |
||||||
|
data |
||||||
|
*.DS_Store |
@ -0,0 +1,2 @@ |
|||||||
|
**/*.min.js |
||||||
|
config.js |
@ -0,0 +1,25 @@ |
|||||||
|
{ |
||||||
|
"env": { |
||||||
|
"es6": true, |
||||||
|
"node": true |
||||||
|
}, |
||||||
|
"extends": "eslint:recommended", |
||||||
|
"rules": { |
||||||
|
"indent": [ |
||||||
|
"error", |
||||||
|
2 |
||||||
|
], |
||||||
|
"linebreak-style": [ |
||||||
|
"error", |
||||||
|
"unix" |
||||||
|
], |
||||||
|
"quotes": [ |
||||||
|
"error", |
||||||
|
"single" |
||||||
|
], |
||||||
|
"semi": [ |
||||||
|
"error", |
||||||
|
"always" |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
* @toptal/site-acquisition-eng |
@ -0,0 +1,30 @@ |
|||||||
|
name: Close inactive issues and PRs |
||||||
|
on: |
||||||
|
workflow_dispatch: |
||||||
|
schedule: |
||||||
|
- cron: "30 1 * * *" |
||||||
|
|
||||||
|
jobs: |
||||||
|
close-stale: |
||||||
|
runs-on: ubuntu-latest |
||||||
|
permissions: |
||||||
|
issues: write |
||||||
|
pull-requests: write |
||||||
|
steps: |
||||||
|
- uses: actions/stale@v3 |
||||||
|
with: |
||||||
|
days-before-stale: 30 |
||||||
|
days-before-close: 14 |
||||||
|
stale-issue-label: "stale" |
||||||
|
stale-pr-label: "stale" |
||||||
|
|
||||||
|
exempt-issue-labels: backlog,triage,nostale |
||||||
|
exempt-pr-labels: backlog,triage,nostale |
||||||
|
|
||||||
|
stale-pr-message: "This PR is stale because it has been open for 30 days with no activity." |
||||||
|
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale." |
||||||
|
|
||||||
|
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." |
||||||
|
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." |
||||||
|
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }} |
@ -0,0 +1,7 @@ |
|||||||
|
npm-debug.log |
||||||
|
node_modules |
||||||
|
*.swp |
||||||
|
*.swo |
||||||
|
data |
||||||
|
*.DS_Store |
||||||
|
docker-compose.override.yml |
@ -0,0 +1,68 @@ |
|||||||
|
FROM node:14.8.0-stretch |
||||||
|
|
||||||
|
RUN mkdir -p /usr/src/app && \ |
||||||
|
chown node:node /usr/src/app |
||||||
|
|
||||||
|
USER node:node |
||||||
|
|
||||||
|
WORKDIR /usr/src/app |
||||||
|
|
||||||
|
COPY --chown=node:node . . |
||||||
|
|
||||||
|
RUN npm install && \ |
||||||
|
npm install redis@0.8.1 && \ |
||||||
|
npm install pg@4.1.1 && \ |
||||||
|
npm install memcached@2.2.2 && \ |
||||||
|
npm install aws-sdk@2.738.0 && \ |
||||||
|
npm install rethinkdbdash@2.3.31 |
||||||
|
|
||||||
|
ENV STORAGE_TYPE=memcached \ |
||||||
|
STORAGE_HOST=127.0.0.1 \ |
||||||
|
STORAGE_PORT=11211\ |
||||||
|
STORAGE_EXPIRE_SECONDS=2592000\ |
||||||
|
STORAGE_DB=2 \ |
||||||
|
STORAGE_AWS_BUCKET= \ |
||||||
|
STORAGE_AWS_REGION= \ |
||||||
|
STORAGE_USENAME= \ |
||||||
|
STORAGE_PASSWORD= \ |
||||||
|
STORAGE_FILEPATH= |
||||||
|
|
||||||
|
ENV LOGGING_LEVEL=verbose \ |
||||||
|
LOGGING_TYPE=Console \ |
||||||
|
LOGGING_COLORIZE=true |
||||||
|
|
||||||
|
ENV HOST=0.0.0.0\ |
||||||
|
PORT=7777\ |
||||||
|
KEY_LENGTH=10\ |
||||||
|
MAX_LENGTH=400000\ |
||||||
|
STATIC_MAX_AGE=86400\ |
||||||
|
RECOMPRESS_STATIC_ASSETS=true |
||||||
|
|
||||||
|
ENV KEYGENERATOR_TYPE=phonetic \ |
||||||
|
KEYGENERATOR_KEYSPACE= |
||||||
|
|
||||||
|
ENV RATELIMITS_NORMAL_TOTAL_REQUESTS=500\ |
||||||
|
RATELIMITS_NORMAL_EVERY_MILLISECONDS=60000 \ |
||||||
|
RATELIMITS_WHITELIST_TOTAL_REQUESTS= \ |
||||||
|
RATELIMITS_WHITELIST_EVERY_MILLISECONDS= \ |
||||||
|
# comma separated list for the whitelisted \ |
||||||
|
RATELIMITS_WHITELIST=example1.whitelist,example2.whitelist \ |
||||||
|
\ |
||||||
|
RATELIMITS_BLACKLIST_TOTAL_REQUESTS= \ |
||||||
|
RATELIMITS_BLACKLIST_EVERY_MILLISECONDS= \ |
||||||
|
# comma separated list for the blacklisted \ |
||||||
|
RATELIMITS_BLACKLIST=example1.blacklist,example2.blacklist |
||||||
|
ENV DOCUMENTS=about=./about.md |
||||||
|
|
||||||
|
EXPOSE ${PORT} |
||||||
|
STOPSIGNAL SIGINT |
||||||
|
ENTRYPOINT [ "bash", "docker-entrypoint.sh" ] |
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s \ |
||||||
|
--retries=3 CMD [ "sh", "-c", "echo -n 'curl localhost:7777... '; \ |
||||||
|
(\ |
||||||
|
curl -sf localhost:7777 > /dev/null\ |
||||||
|
) && echo OK || (\ |
||||||
|
echo Fail && exit 2\ |
||||||
|
)"] |
||||||
|
CMD ["npm", "start"] |
@ -0,0 +1,389 @@ |
|||||||
|
# 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 [hastebin.com](http://hastebin.com) |
||||||
|
|
||||||
|
Major design objectives: |
||||||
|
|
||||||
|
* 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://github.com/seejohnrun/haste-client), allowing you |
||||||
|
to do things like: |
||||||
|
|
||||||
|
`cat something | haste` |
||||||
|
|
||||||
|
which will output a URL to share containing the contents of `cat something`'s |
||||||
|
STDOUT. Check the README there for more details and usages. |
||||||
|
|
||||||
|
## Tested Browsers |
||||||
|
|
||||||
|
* Firefox 8 |
||||||
|
* Chrome 17 |
||||||
|
* Safari 5.3 |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
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` (you may specify an optional `<config-path>` as well) |
||||||
|
|
||||||
|
## Settings |
||||||
|
|
||||||
|
* `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 400000) |
||||||
|
* `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) |
||||||
|
|
||||||
|
## Rate Limiting |
||||||
|
|
||||||
|
When present, the `rateLimits` option enables built-in rate limiting courtesy |
||||||
|
of `connect-ratelimit`. Any of the options supported by that library can be |
||||||
|
used and set in `config.js`. |
||||||
|
|
||||||
|
See the README for [connect-ratelimit](https://github.com/dharmafly/connect-ratelimit) |
||||||
|
for more information! |
||||||
|
|
||||||
|
## Key Generation |
||||||
|
|
||||||
|
### Phonetic |
||||||
|
|
||||||
|
Attempts to generate phonetic keys, similar to `pwgen` |
||||||
|
|
||||||
|
``` json |
||||||
|
{ |
||||||
|
"type": "phonetic" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Random |
||||||
|
|
||||||
|
Generates a random key |
||||||
|
|
||||||
|
``` json |
||||||
|
{ |
||||||
|
"type": "random", |
||||||
|
"keyspace": "abcdef" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
The _optional_ keySpace argument is a string of acceptable characters |
||||||
|
for the key. |
||||||
|
|
||||||
|
## Storage |
||||||
|
|
||||||
|
### File |
||||||
|
|
||||||
|
To use file storage (the default) change the storage section in `config.js` to |
||||||
|
something like: |
||||||
|
|
||||||
|
``` json |
||||||
|
{ |
||||||
|
"path": "./data", |
||||||
|
"type": "file" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
where `path` represents where you want the files stored. |
||||||
|
|
||||||
|
File storage currently does not support paste expiration, you can follow [#191](https://github.com/seejohnrun/haste-server/issues/191) for status updates. |
||||||
|
|
||||||
|
### 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 |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
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. |
||||||
|
|
||||||
|
If your Redis server is configured for password authentification, use the `password` field. |
||||||
|
|
||||||
|
### 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. |
||||||
|
|
||||||
|
### MongoDB |
||||||
|
|
||||||
|
To use mongodb storage you must install the 'mongodb' package in npm |
||||||
|
|
||||||
|
`npm install mongodb` |
||||||
|
|
||||||
|
Once you've done that, your config section should look like: |
||||||
|
|
||||||
|
``` json |
||||||
|
{ |
||||||
|
"type": "mongo", |
||||||
|
"connectionUrl": "mongodb://localhost:27017/database" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
You can also just set the environment variable for `DATABASE_URL` to your database connection url. |
||||||
|
|
||||||
|
Unlike with postgres you do NOT have to create the table in your mongo database prior to running. |
||||||
|
|
||||||
|
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. |
||||||
|
|
||||||
|
### 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. |
||||||
|
|
||||||
|
### Google Datastore |
||||||
|
|
||||||
|
To use the Google Datastore storage system, you must install the `@google-cloud/datastore` package via npm |
||||||
|
|
||||||
|
`npm install @google-cloud/datastore` |
||||||
|
|
||||||
|
Once you've done that, your config section should look like this: |
||||||
|
|
||||||
|
``` json |
||||||
|
{ |
||||||
|
"type": "google-datastore" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Authentication is handled automatically by [Google Cloud service account credentials](https://cloud.google.com/docs/authentication/getting-started), by providing authentication details to the GOOGLE_APPLICATION_CREDENTIALS environmental variable. |
||||||
|
|
||||||
|
### Amazon S3 |
||||||
|
|
||||||
|
To use [Amazon S3](https://aws.amazon.com/s3/) as a storage system, you must |
||||||
|
install the `aws-sdk` package via npm: |
||||||
|
|
||||||
|
`npm install aws-sdk` |
||||||
|
|
||||||
|
Once you've done that, your config section should look like this: |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"type": "amazon-s3", |
||||||
|
"bucket": "your-bucket-name", |
||||||
|
"region": "us-east-1" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Authentication is handled automatically by the client. Check |
||||||
|
[Amazon's documentation](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) |
||||||
|
for more information. You will need to grant your role these permissions to |
||||||
|
your bucket: |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"Version": "2012-10-17", |
||||||
|
"Statement": [ |
||||||
|
{ |
||||||
|
"Action": [ |
||||||
|
"s3:GetObject", |
||||||
|
"s3:PutObject" |
||||||
|
], |
||||||
|
"Effect": "Allow", |
||||||
|
"Resource": "arn:aws:s3:::your-bucket-name-goes-here/*" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Docker |
||||||
|
|
||||||
|
### Build image |
||||||
|
|
||||||
|
```bash |
||||||
|
docker build --tag haste-server . |
||||||
|
``` |
||||||
|
|
||||||
|
### Run container |
||||||
|
|
||||||
|
For this example we will run haste-server, and connect it to a redis server |
||||||
|
|
||||||
|
```bash |
||||||
|
docker run --name haste-server-container --env STORAGE_TYPE=redis --env STORAGE_HOST=redis-server --env STORAGE_PORT=6379 haste-server |
||||||
|
``` |
||||||
|
|
||||||
|
### Use docker-compose example |
||||||
|
|
||||||
|
There is an example `docker-compose.yml` which runs haste-server together with memcached |
||||||
|
|
||||||
|
```bash |
||||||
|
docker-compose up |
||||||
|
``` |
||||||
|
|
||||||
|
### Configuration |
||||||
|
|
||||||
|
The docker image is configured using environmental variables as you can see in the example above. |
||||||
|
|
||||||
|
Here is a list of all the environment variables |
||||||
|
|
||||||
|
### Storage |
||||||
|
|
||||||
|
| Name | Default value | Description | |
||||||
|
| :--------------------: | :-----------: | :-----------------------------------------------------------------------------------------------------------: | |
||||||
|
| STORAGE_TYPE | memcached | Type of storage . Accepted values: "memcached","redis","postgres","rethinkdb", "amazon-s3", and "file" | |
||||||
|
| STORAGE_HOST | 127.0.0.1 | Storage host. Applicable for types: memcached, redis, postgres, and rethinkdb | |
||||||
|
| STORAGE_PORT | 11211 | Port on the storage host. Applicable for types: memcached, redis, postgres, and rethinkdb | |
||||||
|
| STORAGE_EXPIRE_SECONDS | 2592000 | Number of seconds to expire keys in. Applicable for types. Redis, postgres, memcached. `expire` option to the | |
||||||
|
| STORAGE_DB | 2 | The name of the database. Applicable for redis, postgres, and rethinkdb | |
||||||
|
| STORAGE_PASSWORD | | Password for database. Applicable for redis, postges, rethinkdb . | |
||||||
|
| STORAGE_USERNAME | | Database username. Applicable for postgres, and rethinkdb | |
||||||
|
| STORAGE_AWS_BUCKET | | Applicable for amazon-s3. This is the name of the S3 bucket | |
||||||
|
| STORAGE_AWS_REGION | | Applicable for amazon-s3. The region in which the bucket is located | |
||||||
|
| STORAGE_FILEPATH | | Path to file to save data to. Applicable for type file | |
||||||
|
|
||||||
|
### Logging |
||||||
|
|
||||||
|
| Name | Default value | Description | |
||||||
|
| :---------------: | :-----------: | :---------: | |
||||||
|
| LOGGING_LEVEL | verbose | | |
||||||
|
| LOGGING_TYPE= | Console | |
||||||
|
| LOGGING_COLORIZE= | true | |
||||||
|
|
||||||
|
### Basics |
||||||
|
|
||||||
|
| Name | Default value | Description | |
||||||
|
| :----------------------: | :--------------: | :---------------------------------------------------------------------------------------: | |
||||||
|
| HOST | 0.0.0.0 | The hostname which the server answers on | |
||||||
|
| PORT | 7777 | The port on which the server is running | |
||||||
|
| KEY_LENGTH | 10 | the length of the keys to user | |
||||||
|
| MAX_LENGTH | 400000 | maximum length of a paste | |
||||||
|
| STATIC_MAX_AGE | 86400 | max age for static assets | |
||||||
|
| RECOMPRESS_STATIC_ASSETS | true | whether or not to compile static js assets | |
||||||
|
| KEYGENERATOR_TYPE | phonetic | Type of key generator. Acceptable values: "phonetic", or "random" | |
||||||
|
| KEYGENERATOR_KEYSPACE | | keySpace argument is a string of acceptable characters | |
||||||
|
| DOCUMENTS | about=./about.md | Comma separated list of static documents to serve. ex: \n about=./about.md,home=./home.md | |
||||||
|
|
||||||
|
### Rate limits |
||||||
|
|
||||||
|
| Name | Default value | Description | |
||||||
|
| :----------------------------------: | :-----------------------------------: | :--------------------------------------------------------------------------------------: | |
||||||
|
| RATELIMITS_NORMAL_TOTAL_REQUESTS | 500 | By default anyone uncategorized will be subject to 500 requests in the defined timespan. | |
||||||
|
| RATELIMITS_NORMAL_EVERY_MILLISECONDS | 60000 | The timespan to allow the total requests for uncategorized users | |
||||||
|
| RATELIMITS_WHITELIST_TOTAL_REQUESTS | | By default client names in the whitelist will not have their requests limited. | |
||||||
|
| RATELIMITS_WHITELIST_EVERY_SECONDS | | By default client names in the whitelist will not have their requests limited. | |
||||||
|
| RATELIMITS_WHITELIST | example1.whitelist,example2.whitelist | Comma separated list of the clients which are in the whitelist pool | |
||||||
|
| RATELIMITS_BLACKLIST_TOTAL_REQUESTS | | By default client names in the blacklist will be subject to 0 requests per hours. | |
||||||
|
| RATELIMITS_BLACKLIST_EVERY_SECONDS | | By default client names in the blacklist will be subject to 0 requests per hours | |
||||||
|
| RATELIMITS_BLACKLIST | example1.blacklist,example2.blacklist | Comma separated list of the clients which are in the blacklistpool. | |
||||||
|
|
||||||
|
## Author |
||||||
|
|
||||||
|
John Crepezzi <john.crepezzi@gmail.com> |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
(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: |
||||||
|
|
||||||
|
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 |
||||||
|
|
||||||
|
### Other components: |
||||||
|
|
||||||
|
* jQuery: MIT/GPL license |
||||||
|
* highlight.js: Copyright © 2006, Ivan Sagalaev |
||||||
|
* highlightjs-coffeescript: WTFPL - Copyright © 2011, Dmytrii Nagirniak |
@ -0,0 +1,61 @@ |
|||||||
|
# Haste |
||||||
|
|
||||||
|
Sharing code is a good thing, and it should be _really_ easy to do it. |
||||||
|
A lot of times, I want to show you something I'm seeing - and that's where we |
||||||
|
use pastebins. |
||||||
|
|
||||||
|
Haste is the prettiest, easiest to use pastebin ever made. |
||||||
|
|
||||||
|
## Basic Usage |
||||||
|
|
||||||
|
Type what you want me to see, click "Save", and then copy the URL. Send that |
||||||
|
URL to someone and they'll see what you see. |
||||||
|
|
||||||
|
To make a new entry, click "New" (or type 'control + n') |
||||||
|
|
||||||
|
## From the Console |
||||||
|
|
||||||
|
Most of the time I want to show you some text, it's coming from my current |
||||||
|
console session. We should make it really easy to take code from the console |
||||||
|
and send it to people. |
||||||
|
|
||||||
|
`cat something | haste` # https://hastebin.com/1238193 |
||||||
|
|
||||||
|
You can even take this a step further, and cut out the last step of copying the |
||||||
|
URL with: |
||||||
|
|
||||||
|
* osx: `cat something | haste | pbcopy` |
||||||
|
* linux: `cat something | haste | xsel -b` |
||||||
|
* windows: check out [WinHaste](https://github.com/ajryan/WinHaste) |
||||||
|
|
||||||
|
After running that, the STDOUT output of `cat something` will show up at a URL |
||||||
|
which has been conveniently copied to your clipboard. |
||||||
|
|
||||||
|
That's all there is to that, and you can install it with `gem install haste` |
||||||
|
right now. |
||||||
|
* osx: you will need to have an up to date version of Xcode |
||||||
|
* linux: you will need to have rubygems and ruby-devel installed |
||||||
|
|
||||||
|
## Duration |
||||||
|
|
||||||
|
Pastes will stay for 30 days from their last view. They may be removed earlier |
||||||
|
and without notice. |
||||||
|
|
||||||
|
## Privacy |
||||||
|
|
||||||
|
While the contents of hastebin.com are not directly crawled by any search robot |
||||||
|
that obeys "robots.txt", there should be no great expectation of privacy. Post |
||||||
|
things at your own risk. Not responsible for any loss of data or removed |
||||||
|
pastes. |
||||||
|
|
||||||
|
## Open Source |
||||||
|
|
||||||
|
Haste can easily be installed behind your network, and it's all open source! |
||||||
|
|
||||||
|
* [haste-client](https://github.com/seejohnrun/haste-client) |
||||||
|
* [haste-server](https://github.com/seejohnrun/haste-server) |
||||||
|
|
||||||
|
## Author |
||||||
|
|
||||||
|
Code by John Crepezzi <john.crepezzi@gmail.com> |
||||||
|
Key Design by Brian Dawson <bridawson@gmail.com> |
@ -0,0 +1,43 @@ |
|||||||
|
{ |
||||||
|
|
||||||
|
"host": "0.0.0.0", |
||||||
|
"port": 7777, |
||||||
|
|
||||||
|
"keyLength": 10, |
||||||
|
|
||||||
|
"maxLength": 400000, |
||||||
|
|
||||||
|
"staticMaxAge": 86400, |
||||||
|
|
||||||
|
"recompressStaticAssets": true, |
||||||
|
|
||||||
|
"logging": [ |
||||||
|
{ |
||||||
|
"level": "verbose", |
||||||
|
"type": "Console", |
||||||
|
"colorize": true |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"keyGenerator": { |
||||||
|
"type": "phonetic" |
||||||
|
}, |
||||||
|
|
||||||
|
"rateLimits": { |
||||||
|
"categories": { |
||||||
|
"normal": { |
||||||
|
"totalRequests": 500, |
||||||
|
"every": 60000 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"storage": { |
||||||
|
"type": "file" |
||||||
|
}, |
||||||
|
|
||||||
|
"documents": { |
||||||
|
"about": "./about.md" |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
version: '3.0' |
||||||
|
services: |
||||||
|
haste-server: |
||||||
|
build: . |
||||||
|
environment: |
||||||
|
- STORAGE_TYPE=memcached |
||||||
|
- STORAGE_HOST=memcached |
||||||
|
- STORAGE_PORT=11211 |
||||||
|
ports: |
||||||
|
- 7777:7777 |
||||||
|
memcached: |
||||||
|
image: memcached:latest |
@ -0,0 +1,108 @@ |
|||||||
|
const { |
||||||
|
HOST, |
||||||
|
PORT, |
||||||
|
KEY_LENGTH, |
||||||
|
MAX_LENGTH, |
||||||
|
STATIC_MAX_AGE, |
||||||
|
RECOMPRESS_STATIC_ASSETS, |
||||||
|
STORAGE_TYPE, |
||||||
|
STORAGE_HOST, |
||||||
|
STORAGE_PORT, |
||||||
|
STORAGE_EXPIRE_SECONDS, |
||||||
|
STORAGE_DB, |
||||||
|
STORAGE_AWS_BUCKET, |
||||||
|
STORAGE_AWS_REGION, |
||||||
|
STORAGE_PASSWORD, |
||||||
|
STORAGE_USERNAME, |
||||||
|
STORAGE_FILEPATH, |
||||||
|
LOGGING_LEVEL, |
||||||
|
LOGGING_TYPE, |
||||||
|
LOGGING_COLORIZE, |
||||||
|
KEYGENERATOR_TYPE, |
||||||
|
KEY_GENERATOR_KEYSPACE, |
||||||
|
RATE_LIMITS_NORMAL_TOTAL_REQUESTS, |
||||||
|
RATE_LIMITS_NORMAL_EVERY_MILLISECONDS, |
||||||
|
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS, |
||||||
|
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS, |
||||||
|
RATE_LIMITS_WHITELIST, |
||||||
|
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS, |
||||||
|
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS, |
||||||
|
RATE_LIMITS_BLACKLIST, |
||||||
|
DOCUMENTS, |
||||||
|
} = process.env; |
||||||
|
|
||||||
|
const config = { |
||||||
|
host: HOST, |
||||||
|
port: Number(PORT), |
||||||
|
|
||||||
|
keyLength: Number(KEY_LENGTH), |
||||||
|
|
||||||
|
maxLength: Number(MAX_LENGTH), |
||||||
|
|
||||||
|
staticMaxAge: Number(STATIC_MAX_AGE), |
||||||
|
|
||||||
|
recompressStaticAssets: RECOMPRESS_STATIC_ASSETS, |
||||||
|
|
||||||
|
logging: [ |
||||||
|
{ |
||||||
|
level: LOGGING_LEVEL, |
||||||
|
type: LOGGING_TYPE, |
||||||
|
colorize: LOGGING_COLORIZE, |
||||||
|
}, |
||||||
|
], |
||||||
|
|
||||||
|
keyGenerator: { |
||||||
|
type: KEYGENERATOR_TYPE, |
||||||
|
keyspace: KEY_GENERATOR_KEYSPACE, |
||||||
|
}, |
||||||
|
|
||||||
|
rateLimits: { |
||||||
|
whitelist: RATE_LIMITS_WHITELIST ? RATE_LIMITS_WHITELIST.split(",") : [], |
||||||
|
blacklist: RATE_LIMITS_BLACKLIST ? RATE_LIMITS_BLACKLIST.split(",") : [], |
||||||
|
categories: { |
||||||
|
normal: { |
||||||
|
totalRequests: RATE_LIMITS_NORMAL_TOTAL_REQUESTS, |
||||||
|
every: RATE_LIMITS_NORMAL_EVERY_MILLISECONDS, |
||||||
|
}, |
||||||
|
whitelist: |
||||||
|
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS || |
||||||
|
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS |
||||||
|
? { |
||||||
|
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS, |
||||||
|
every: RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS, |
||||||
|
} |
||||||
|
: null, |
||||||
|
blacklist: |
||||||
|
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS || |
||||||
|
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS |
||||||
|
? { |
||||||
|
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS, |
||||||
|
every: RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS, |
||||||
|
} |
||||||
|
: null, |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
storage: { |
||||||
|
type: STORAGE_TYPE, |
||||||
|
host: STORAGE_HOST, |
||||||
|
port: Number(STORAGE_PORT), |
||||||
|
expire: Number(STORAGE_EXPIRE_SECONDS), |
||||||
|
bucket: STORAGE_AWS_BUCKET, |
||||||
|
region: STORAGE_AWS_REGION, |
||||||
|
connectionUrl: `postgres://${STORAGE_USERNAME}:${STORAGE_PASSWORD}@${STORAGE_HOST}:${STORAGE_PORT}/${STORAGE_DB}`, |
||||||
|
db: STORAGE_DB, |
||||||
|
user: STORAGE_USERNAME, |
||||||
|
password: STORAGE_PASSWORD, |
||||||
|
path: STORAGE_FILEPATH, |
||||||
|
}, |
||||||
|
|
||||||
|
documents: DOCUMENTS |
||||||
|
? DOCUMENTS.split(",").reduce((acc, item) => { |
||||||
|
const keyAndValueArray = item.replace(/\s/g, "").split("="); |
||||||
|
return { ...acc, [keyAndValueArray[0]]: keyAndValueArray[1] }; |
||||||
|
}, {}) |
||||||
|
: null, |
||||||
|
}; |
||||||
|
|
||||||
|
console.log(JSON.stringify(config)); |
@ -0,0 +1,9 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# We use this file to translate environmental variables to .env files used by the application |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
node ./docker-entrypoint.js > ./config.js |
||||||
|
|
||||||
|
exec "$@" |
@ -1,43 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
<meta http-equiv="X-UA-Compatible" content="chrome=1"> |
|
||||||
<title>Haste-server by seejohnrun</title> |
|
||||||
|
|
||||||
<link rel="stylesheet" href="stylesheets/styles.css"> |
|
||||||
<link rel="stylesheet" href="stylesheets/pygment_trac.css"> |
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> |
|
||||||
<!--[if lt IE 9]> |
|
||||||
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> |
|
||||||
<![endif]--> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
<div class="wrapper"> |
|
||||||
<header> |
|
||||||
<h1>Haste-server</h1> |
|
||||||
<p>open source pastebin written in node.js</p> |
|
||||||
<p class="view"><a href="https://github.com/seejohnrun/haste-server">View the Project on GitHub <small>seejohnrun/haste-server</small></a></p> |
|
||||||
<ul> |
|
||||||
<li><a href="https://github.com/seejohnrun/haste-server/zipball/master">Download <strong>ZIP File</strong></a></li> |
|
||||||
<li><a href="https://github.com/seejohnrun/haste-server/tarball/master">Download <strong>TAR Ball</strong></a></li> |
|
||||||
<li><a href="https://github.com/seejohnrun/haste-server">Fork On <strong>GitHub</strong></a></li> |
|
||||||
</ul> |
|
||||||
</header> |
|
||||||
<section> |
|
||||||
<h1>Pastebin</h1> |
|
||||||
|
|
||||||
<p>haste-server is an open source pastebin. You can see an example at <a href="http://hastebin.com">hastebin.com</a>, and it's easy to install locally.</p> |
|
||||||
|
|
||||||
<h2>GitHub</h2> |
|
||||||
|
|
||||||
<p>Check out the project on <a href="https://github.com/seejohnrun/haste-server">github</a></p> |
|
||||||
</section> |
|
||||||
<footer> |
|
||||||
<p>This project is maintained by <a href="https://github.com/seejohnrun">seejohnrun</a></p> |
|
||||||
<p><small>Hosted on GitHub Pages — Theme by <a href="https://github.com/orderedlist">orderedlist</a></small></p> |
|
||||||
</footer> |
|
||||||
</div> |
|
||||||
<script src="javascripts/scale.fix.js"></script> |
|
||||||
</body> |
|
||||||
</html> |
|
@ -1,17 +0,0 @@ |
|||||||
var metas = document.getElementsByTagName('meta'); |
|
||||||
var i; |
|
||||||
if (navigator.userAgent.match(/iPhone/i)) { |
|
||||||
for (i=0; i<metas.length; i++) { |
|
||||||
if (metas[i].name == "viewport") { |
|
||||||
metas[i].content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0"; |
|
||||||
} |
|
||||||
} |
|
||||||
document.addEventListener("gesturestart", gestureStart, false); |
|
||||||
} |
|
||||||
function gestureStart() { |
|
||||||
for (i=0; i<metas.length; i++) { |
|
||||||
if (metas[i].name == "viewport") { |
|
||||||
metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6"; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,155 @@ |
|||||||
|
var winston = require('winston'); |
||||||
|
var Busboy = require('busboy'); |
||||||
|
|
||||||
|
// For handling serving stored documents
|
||||||
|
|
||||||
|
var DocumentHandler = function(options) { |
||||||
|
if (!options) { |
||||||
|
options = {}; |
||||||
|
} |
||||||
|
this.keyLength = options.keyLength || DocumentHandler.defaultKeyLength; |
||||||
|
this.maxLength = options.maxLength; // none by default
|
||||||
|
this.store = options.store; |
||||||
|
this.keyGenerator = options.keyGenerator; |
||||||
|
}; |
||||||
|
|
||||||
|
DocumentHandler.defaultKeyLength = 10; |
||||||
|
|
||||||
|
// Handle retrieving a document
|
||||||
|
DocumentHandler.prototype.handleGet = function(request, response, config) { |
||||||
|
const key = request.params.id.split('.')[0]; |
||||||
|
const skipExpire = !!config.documents[key]; |
||||||
|
|
||||||
|
this.store.get(key, function(ret) { |
||||||
|
if (ret) { |
||||||
|
winston.verbose('retrieved document', { key: key }); |
||||||
|
response.writeHead(200, { 'content-type': 'application/json' }); |
||||||
|
if (request.method === 'HEAD') { |
||||||
|
response.end(); |
||||||
|
} else { |
||||||
|
response.end(JSON.stringify({ data: ret, key: key })); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
winston.warn('document not found', { key: key }); |
||||||
|
response.writeHead(404, { 'content-type': 'application/json' }); |
||||||
|
if (request.method === 'HEAD') { |
||||||
|
response.end(); |
||||||
|
} else { |
||||||
|
response.end(JSON.stringify({ message: 'Document not found.' })); |
||||||
|
} |
||||||
|
} |
||||||
|
}, skipExpire); |
||||||
|
}; |
||||||
|
|
||||||
|
// Handle retrieving the raw version of a document
|
||||||
|
DocumentHandler.prototype.handleRawGet = function(request, response, config) { |
||||||
|
const key = request.params.id.split('.')[0]; |
||||||
|
const skipExpire = !!config.documents[key]; |
||||||
|
|
||||||
|
this.store.get(key, function(ret) { |
||||||
|
if (ret) { |
||||||
|
winston.verbose('retrieved raw document', { key: key }); |
||||||
|
response.writeHead(200, { 'content-type': 'text/plain; charset=UTF-8' }); |
||||||
|
if (request.method === 'HEAD') { |
||||||
|
response.end(); |
||||||
|
} else { |
||||||
|
response.end(ret); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
winston.warn('raw document not found', { key: key }); |
||||||
|
response.writeHead(404, { 'content-type': 'application/json' }); |
||||||
|
if (request.method === 'HEAD') { |
||||||
|
response.end(); |
||||||
|
} else { |
||||||
|
response.end(JSON.stringify({ message: 'Document not found.' })); |
||||||
|
} |
||||||
|
} |
||||||
|
}, skipExpire); |
||||||
|
}; |
||||||
|
|
||||||
|
// Handle adding a new Document
|
||||||
|
DocumentHandler.prototype.handlePost = function (request, response) { |
||||||
|
var _this = this; |
||||||
|
var buffer = ''; |
||||||
|
var cancelled = false; |
||||||
|
|
||||||
|
// What to do when done
|
||||||
|
var onSuccess = function () { |
||||||
|
// Check length
|
||||||
|
if (_this.maxLength && buffer.length > _this.maxLength) { |
||||||
|
cancelled = true; |
||||||
|
winston.warn('document >maxLength', { maxLength: _this.maxLength }); |
||||||
|
response.writeHead(400, { 'content-type': 'application/json' }); |
||||||
|
response.end( |
||||||
|
JSON.stringify({ message: 'Document exceeds maximum length.' }) |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
// And then save if we should
|
||||||
|
_this.chooseKey(function (key) { |
||||||
|
_this.store.set(key, buffer, function (res) { |
||||||
|
if (res) { |
||||||
|
winston.verbose('added document', { key: key }); |
||||||
|
response.writeHead(200, { 'content-type': 'application/json' }); |
||||||
|
response.end(JSON.stringify({ key: key })); |
||||||
|
} |
||||||
|
else { |
||||||
|
winston.verbose('error adding document'); |
||||||
|
response.writeHead(500, { 'content-type': 'application/json' }); |
||||||
|
response.end(JSON.stringify({ message: 'Error adding document.' })); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// If we should, parse a form to grab the data
|
||||||
|
var ct = request.headers['content-type']; |
||||||
|
if (ct && ct.split(';')[0] === 'multipart/form-data') { |
||||||
|
var busboy = new Busboy({ headers: request.headers }); |
||||||
|
busboy.on('field', function (fieldname, val) { |
||||||
|
if (fieldname === 'data') { |
||||||
|
buffer = val; |
||||||
|
} |
||||||
|
}); |
||||||
|
busboy.on('finish', function () { |
||||||
|
onSuccess(); |
||||||
|
}); |
||||||
|
request.pipe(busboy); |
||||||
|
// Otherwise, use our own and just grab flat data from POST body
|
||||||
|
} else { |
||||||
|
request.on('data', function (data) { |
||||||
|
buffer += data.toString(); |
||||||
|
}); |
||||||
|
request.on('end', function () { |
||||||
|
if (cancelled) { return; } |
||||||
|
onSuccess(); |
||||||
|
}); |
||||||
|
request.on('error', function (error) { |
||||||
|
winston.error('connection error: ' + error.message); |
||||||
|
response.writeHead(500, { 'content-type': 'application/json' }); |
||||||
|
response.end(JSON.stringify({ message: 'Connection error.' })); |
||||||
|
cancelled = true; |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Keep choosing keys until one isn't taken
|
||||||
|
DocumentHandler.prototype.chooseKey = function(callback) { |
||||||
|
var key = this.acceptableKey(); |
||||||
|
var _this = this; |
||||||
|
this.store.get(key, function(ret) { |
||||||
|
if (ret) { |
||||||
|
_this.chooseKey(callback); |
||||||
|
} else { |
||||||
|
callback(key); |
||||||
|
} |
||||||
|
}, true); // Don't bump expirations when key searching
|
||||||
|
}; |
||||||
|
|
||||||
|
DocumentHandler.prototype.acceptableKey = function() { |
||||||
|
return this.keyGenerator.createKey(this.keyLength); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = DocumentHandler; |
@ -0,0 +1,56 @@ |
|||||||
|
/*global require,module,process*/ |
||||||
|
|
||||||
|
var AWS = require('aws-sdk'); |
||||||
|
var winston = require('winston'); |
||||||
|
|
||||||
|
var AmazonS3DocumentStore = function(options) { |
||||||
|
this.expire = options.expire; |
||||||
|
this.bucket = options.bucket; |
||||||
|
this.client = new AWS.S3({region: options.region}); |
||||||
|
}; |
||||||
|
|
||||||
|
AmazonS3DocumentStore.prototype.get = function(key, callback, skipExpire) { |
||||||
|
var _this = this; |
||||||
|
|
||||||
|
var req = { |
||||||
|
Bucket: _this.bucket, |
||||||
|
Key: key |
||||||
|
}; |
||||||
|
|
||||||
|
_this.client.getObject(req, function(err, data) { |
||||||
|
if(err) { |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
else { |
||||||
|
callback(data.Body.toString('utf-8')); |
||||||
|
if (_this.expire && !skipExpire) { |
||||||
|
winston.warn('amazon s3 store cannot set expirations on keys'); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
AmazonS3DocumentStore.prototype.set = function(key, data, callback, skipExpire) { |
||||||
|
var _this = this; |
||||||
|
|
||||||
|
var req = { |
||||||
|
Bucket: _this.bucket, |
||||||
|
Key: key, |
||||||
|
Body: data, |
||||||
|
ContentType: 'text/plain' |
||||||
|
}; |
||||||
|
|
||||||
|
_this.client.putObject(req, function(err, data) { |
||||||
|
if (err) { |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
else { |
||||||
|
callback(true); |
||||||
|
if (_this.expire && !skipExpire) { |
||||||
|
winston.warn('amazon s3 store cannot set expirations on keys'); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = AmazonS3DocumentStore; |
@ -0,0 +1,63 @@ |
|||||||
|
var fs = require('fs'); |
||||||
|
var crypto = require('crypto'); |
||||||
|
|
||||||
|
var winston = require('winston'); |
||||||
|
|
||||||
|
// For storing in files
|
||||||
|
// options[type] = file
|
||||||
|
// options[path] - Where to store
|
||||||
|
|
||||||
|
var FileDocumentStore = function(options) { |
||||||
|
this.basePath = options.path || './data'; |
||||||
|
this.expire = options.expire; |
||||||
|
}; |
||||||
|
|
||||||
|
// Generate md5 of a string
|
||||||
|
FileDocumentStore.md5 = function(str) { |
||||||
|
var md5sum = crypto.createHash('md5'); |
||||||
|
md5sum.update(str); |
||||||
|
return md5sum.digest('hex'); |
||||||
|
}; |
||||||
|
|
||||||
|
// Save data in a file, key as md5 - since we don't know what we could
|
||||||
|
// be passed here
|
||||||
|
FileDocumentStore.prototype.set = function(key, data, callback, skipExpire) { |
||||||
|
try { |
||||||
|
var _this = this; |
||||||
|
fs.mkdir(this.basePath, '700', function() { |
||||||
|
var fn = _this.basePath + '/' + FileDocumentStore.md5(key); |
||||||
|
fs.writeFile(fn, data, 'utf8', function(err) { |
||||||
|
if (err) { |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
else { |
||||||
|
callback(true); |
||||||
|
if (_this.expire && !skipExpire) { |
||||||
|
winston.warn('file store cannot set expirations on keys'); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} catch(err) { |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Get data from a file from key
|
||||||
|
FileDocumentStore.prototype.get = function(key, callback, skipExpire) { |
||||||
|
var _this = this; |
||||||
|
var fn = this.basePath + '/' + FileDocumentStore.md5(key); |
||||||
|
fs.readFile(fn, 'utf8', function(err, data) { |
||||||
|
if (err) { |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
else { |
||||||
|
callback(data); |
||||||
|
if (_this.expire && !skipExpire) { |
||||||
|
winston.warn('file store cannot set expirations on keys'); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = FileDocumentStore; |
@ -0,0 +1,89 @@ |
|||||||
|
/*global require,module,process*/ |
||||||
|
|
||||||
|
const Datastore = require('@google-cloud/datastore'); |
||||||
|
const winston = require('winston'); |
||||||
|
|
||||||
|
class GoogleDatastoreDocumentStore { |
||||||
|
|
||||||
|
// Create a new store with options
|
||||||
|
constructor(options) { |
||||||
|
this.kind = "Haste"; |
||||||
|
this.expire = options.expire; |
||||||
|
this.datastore = new Datastore(); |
||||||
|
} |
||||||
|
|
||||||
|
// Save file in a key
|
||||||
|
set(key, data, callback, skipExpire) { |
||||||
|
var expireTime = (skipExpire || this.expire === undefined) ? null : new Date(Date.now() + this.expire * 1000); |
||||||
|
|
||||||
|
var taskKey = this.datastore.key([this.kind, key]) |
||||||
|
var task = { |
||||||
|
key: taskKey, |
||||||
|
data: [ |
||||||
|
{ |
||||||
|
name: 'value', |
||||||
|
value: data, |
||||||
|
excludeFromIndexes: true |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'expiration', |
||||||
|
value: expireTime |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
|
||||||
|
this.datastore.insert(task).then(() => { |
||||||
|
callback(true); |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
callback(false); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Get a file from a key
|
||||||
|
get(key, callback, skipExpire) { |
||||||
|
var taskKey = this.datastore.key([this.kind, key]) |
||||||
|
|
||||||
|
this.datastore.get(taskKey).then((entity) => { |
||||||
|
if (skipExpire || entity[0]["expiration"] == null) { |
||||||
|
callback(entity[0]["value"]); |
||||||
|
} |
||||||
|
else { |
||||||
|
// check for expiry
|
||||||
|
if (entity[0]["expiration"] < new Date()) { |
||||||
|
winston.info("document expired", {key: key, expiration: entity[0]["expiration"], check: new Date(null)}); |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
else { |
||||||
|
// update expiry
|
||||||
|
var task = { |
||||||
|
key: taskKey, |
||||||
|
data: [ |
||||||
|
{ |
||||||
|
name: 'value', |
||||||
|
value: entity[0]["value"], |
||||||
|
excludeFromIndexes: true |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'expiration', |
||||||
|
value: new Date(Date.now() + this.expire * 1000) |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
this.datastore.update(task).then(() => { |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
winston.error("failed to update expiration", {error: err}); |
||||||
|
}); |
||||||
|
callback(entity[0]["value"]); |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
winston.error("Error retrieving value from Google Datastore", {error: err}); |
||||||
|
callback(false); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = GoogleDatastoreDocumentStore; |
@ -0,0 +1,54 @@ |
|||||||
|
const memcached = require('memcached'); |
||||||
|
const winston = require('winston'); |
||||||
|
|
||||||
|
class MemcachedDocumentStore { |
||||||
|
|
||||||
|
// Create a new store with options
|
||||||
|
constructor(options) { |
||||||
|
this.expire = options.expire; |
||||||
|
|
||||||
|
const host = options.host || '127.0.0.1'; |
||||||
|
const port = options.port || 11211; |
||||||
|
const url = `${host}:${port}`; |
||||||
|
this.connect(url); |
||||||
|
} |
||||||
|
|
||||||
|
// Create a connection
|
||||||
|
connect(url) { |
||||||
|
this.client = new memcached(url); |
||||||
|
|
||||||
|
winston.info(`connecting to memcached on ${url}`); |
||||||
|
|
||||||
|
this.client.on('failure', function(error) { |
||||||
|
winston.info('error connecting to memcached', {error}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Save file in a key
|
||||||
|
set(key, data, callback, skipExpire) { |
||||||
|
this.client.set(key, data, skipExpire ? 0 : this.expire || 0, (error) => { |
||||||
|
callback(!error); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Get a file from a key
|
||||||
|
get(key, callback, skipExpire) { |
||||||
|
this.client.get(key, (error, data) => { |
||||||
|
const value = error ? false : data; |
||||||
|
|
||||||
|
callback(value); |
||||||
|
|
||||||
|
// Update the key so that the expiration is pushed forward
|
||||||
|
if (value && !skipExpire) { |
||||||
|
this.set(key, data, (updateSucceeded) => { |
||||||
|
if (!updateSucceeded) { |
||||||
|
winston.error('failed to update expiration on GET', {key}); |
||||||
|
} |
||||||
|
}, skipExpire); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = MemcachedDocumentStore; |
@ -0,0 +1,88 @@ |
|||||||
|
|
||||||
|
|
||||||
|
var MongoClient = require('mongodb').MongoClient, |
||||||
|
winston = require('winston'); |
||||||
|
|
||||||
|
var MongoDocumentStore = function (options) { |
||||||
|
this.expire = options.expire; |
||||||
|
this.connectionUrl = process.env.DATABASE_URl || options.connectionUrl; |
||||||
|
}; |
||||||
|
|
||||||
|
MongoDocumentStore.prototype.set = function (key, data, callback, skipExpire) { |
||||||
|
var now = Math.floor(new Date().getTime() / 1000), |
||||||
|
that = this; |
||||||
|
|
||||||
|
this.safeConnect(function (err, db) { |
||||||
|
if (err) |
||||||
|
return callback(false); |
||||||
|
|
||||||
|
db.collection('entries').update({ |
||||||
|
'entry_id': key, |
||||||
|
$or: [ |
||||||
|
{ expiration: -1 }, |
||||||
|
{ expiration: { $gt: now } } |
||||||
|
] |
||||||
|
}, { |
||||||
|
'entry_id': key, |
||||||
|
'value': data, |
||||||
|
'expiration': that.expire && !skipExpire ? that.expire + now : -1 |
||||||
|
}, { |
||||||
|
upsert: true |
||||||
|
}, function (err, existing) { |
||||||
|
if (err) { |
||||||
|
winston.error('error persisting value to mongodb', { error: err }); |
||||||
|
return callback(false); |
||||||
|
} |
||||||
|
|
||||||
|
callback(true); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
MongoDocumentStore.prototype.get = function (key, callback, skipExpire) { |
||||||
|
var now = Math.floor(new Date().getTime() / 1000), |
||||||
|
that = this; |
||||||
|
|
||||||
|
this.safeConnect(function (err, db) { |
||||||
|
if (err) |
||||||
|
return callback(false); |
||||||
|
|
||||||
|
db.collection('entries').findOne({ |
||||||
|
'entry_id': key, |
||||||
|
$or: [ |
||||||
|
{ expiration: -1 }, |
||||||
|
{ expiration: { $gt: now } } |
||||||
|
] |
||||||
|
}, function (err, entry) { |
||||||
|
if (err) { |
||||||
|
winston.error('error persisting value to mongodb', { error: err }); |
||||||
|
return callback(false); |
||||||
|
} |
||||||
|
|
||||||
|
callback(entry === null ? false : entry.value); |
||||||
|
|
||||||
|
if (entry !== null && entry.expiration !== -1 && that.expire && !skipExpire) { |
||||||
|
db.collection('entries').update({ |
||||||
|
'entry_id': key |
||||||
|
}, { |
||||||
|
$set: { |
||||||
|
'expiration': that.expire + now |
||||||
|
} |
||||||
|
}, function (err, result) { }); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
MongoDocumentStore.prototype.safeConnect = function (callback) { |
||||||
|
MongoClient.connect(this.connectionUrl, function (err, db) { |
||||||
|
if (err) { |
||||||
|
winston.error('error connecting to mongodb', { error: err }); |
||||||
|
callback(err); |
||||||
|
} else { |
||||||
|
callback(undefined, db); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = MongoDocumentStore; |
@ -0,0 +1,80 @@ |
|||||||
|
/*global require,module,process*/ |
||||||
|
|
||||||
|
var winston = require('winston'); |
||||||
|
const {Pool} = require('pg'); |
||||||
|
|
||||||
|
// create table entries (id serial primary key, key varchar(255) not null, value text not null, expiration int, unique(key));
|
||||||
|
|
||||||
|
// A postgres document store
|
||||||
|
var PostgresDocumentStore = function (options) { |
||||||
|
this.expireJS = parseInt(options.expire, 10); |
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL || options.connectionUrl; |
||||||
|
this.pool = new Pool({connectionString}); |
||||||
|
}; |
||||||
|
|
||||||
|
PostgresDocumentStore.prototype = { |
||||||
|
|
||||||
|
// Set a given key
|
||||||
|
set: function (key, data, callback, skipExpire) { |
||||||
|
var now = Math.floor(new Date().getTime() / 1000); |
||||||
|
var that = this; |
||||||
|
this.safeConnect(function (err, client, done) { |
||||||
|
if (err) { return callback(false); } |
||||||
|
client.query('INSERT INTO entries (key, value, expiration) VALUES ($1, $2, $3)', [ |
||||||
|
key, |
||||||
|
data, |
||||||
|
that.expireJS && !skipExpire ? that.expireJS + now : null |
||||||
|
], function (err) { |
||||||
|
if (err) { |
||||||
|
winston.error('error persisting value to postgres', { error: err }); |
||||||
|
return callback(false); |
||||||
|
} |
||||||
|
callback(true); |
||||||
|
done(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
// Get a given key's data
|
||||||
|
get: function (key, callback, skipExpire) { |
||||||
|
var now = Math.floor(new Date().getTime() / 1000); |
||||||
|
var that = this; |
||||||
|
this.safeConnect(function (err, client, done) { |
||||||
|
if (err) { return callback(false); } |
||||||
|
client.query('SELECT id,value,expiration from entries where KEY = $1 and (expiration IS NULL or expiration > $2)', [key, now], function (err, result) { |
||||||
|
if (err) { |
||||||
|
winston.error('error retrieving value from postgres', { error: err }); |
||||||
|
return callback(false); |
||||||
|
} |
||||||
|
callback(result.rows.length ? result.rows[0].value : false); |
||||||
|
if (result.rows.length && that.expireJS && !skipExpire) { |
||||||
|
client.query('UPDATE entries SET expiration = $1 WHERE ID = $2', [ |
||||||
|
that.expireJS + now, |
||||||
|
result.rows[0].id |
||||||
|
], function (err) { |
||||||
|
if (!err) { |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
// A connection wrapper
|
||||||
|
safeConnect: function (callback) { |
||||||
|
this.pool.connect((error, client, done) => { |
||||||
|
if (error) { |
||||||
|
winston.error('error connecting to postgres', {error}); |
||||||
|
callback(error); |
||||||
|
} else { |
||||||
|
callback(undefined, client, done); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = PostgresDocumentStore; |
@ -0,0 +1,89 @@ |
|||||||
|
var redis = require('redis'); |
||||||
|
var winston = require('winston'); |
||||||
|
|
||||||
|
// For storing in redis
|
||||||
|
// options[type] = redis
|
||||||
|
// options[host] - The host to connect to (default localhost)
|
||||||
|
// options[port] - The port to connect to (default 5379)
|
||||||
|
// options[db] - The db to use (default 0)
|
||||||
|
// options[expire] - The time to live for each key set (default never)
|
||||||
|
|
||||||
|
var RedisDocumentStore = function(options, client) { |
||||||
|
this.expire = options.expire; |
||||||
|
if (client) { |
||||||
|
winston.info('using predefined redis client'); |
||||||
|
RedisDocumentStore.client = client; |
||||||
|
} else if (!RedisDocumentStore.client) { |
||||||
|
winston.info('configuring redis'); |
||||||
|
RedisDocumentStore.connect(options); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Create a connection according to config
|
||||||
|
RedisDocumentStore.connect = 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
|
||||||
|
if (options.password) { |
||||||
|
RedisDocumentStore.client.auth(options.password); |
||||||
|
} |
||||||
|
|
||||||
|
RedisDocumentStore.client.on('error', function(err) { |
||||||
|
winston.error('redis disconnected', err); |
||||||
|
}); |
||||||
|
|
||||||
|
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) { |
||||||
|
var _this = this; |
||||||
|
RedisDocumentStore.client.set(key, data, function(err) { |
||||||
|
if (err) { |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
else { |
||||||
|
if (!skipExpire) { |
||||||
|
_this.setExpiration(key); |
||||||
|
} |
||||||
|
callback(true); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// Expire a key in expire time if set
|
||||||
|
RedisDocumentStore.prototype.setExpiration = function(key) { |
||||||
|
if (this.expire) { |
||||||
|
RedisDocumentStore.client.expire(key, this.expire, function(err) { |
||||||
|
if (err) { |
||||||
|
winston.error('failed to set expiry on key: ' + key); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Get a file from a key
|
||||||
|
RedisDocumentStore.prototype.get = function(key, callback, skipExpire) { |
||||||
|
var _this = this; |
||||||
|
RedisDocumentStore.client.get(key, function(err, reply) { |
||||||
|
if (!err && !skipExpire) { |
||||||
|
_this.setExpiration(key); |
||||||
|
} |
||||||
|
callback(err ? false : reply); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = RedisDocumentStore; |
@ -0,0 +1,46 @@ |
|||||||
|
const crypto = require('crypto'); |
||||||
|
const rethink = require('rethinkdbdash'); |
||||||
|
const winston = require('winston'); |
||||||
|
|
||||||
|
const md5 = (str) => { |
||||||
|
const md5sum = crypto.createHash('md5'); |
||||||
|
md5sum.update(str); |
||||||
|
return md5sum.digest('hex'); |
||||||
|
}; |
||||||
|
|
||||||
|
class RethinkDBStore { |
||||||
|
constructor(options) { |
||||||
|
this.client = rethink({ |
||||||
|
silent: true, |
||||||
|
host: options.host || '127.0.0.1', |
||||||
|
port: options.port || 28015, |
||||||
|
db: options.db || 'haste', |
||||||
|
user: options.user || 'admin', |
||||||
|
password: options.password || '' |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
set(key, data, callback) { |
||||||
|
this.client.table('uploads').insert({ id: md5(key), data: data }).run((error) => { |
||||||
|
if (error) { |
||||||
|
callback(false); |
||||||
|
winston.error('failed to insert to table', error); |
||||||
|
return; |
||||||
|
} |
||||||
|
callback(true); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
get(key, callback) { |
||||||
|
this.client.table('uploads').get(md5(key)).run((error, result) => { |
||||||
|
if (error || !result) { |
||||||
|
callback(false); |
||||||
|
if (error) winston.error('failed to insert to table', error); |
||||||
|
return; |
||||||
|
} |
||||||
|
callback(result.data); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = RethinkDBStore; |
@ -0,0 +1,32 @@ |
|||||||
|
const fs = require('fs'); |
||||||
|
|
||||||
|
module.exports = class DictionaryGenerator { |
||||||
|
|
||||||
|
constructor(options, readyCallback) { |
||||||
|
// Check options format
|
||||||
|
if (!options) throw Error('No options passed to generator'); |
||||||
|
if (!options.path) throw Error('No dictionary path specified in options'); |
||||||
|
|
||||||
|
// Load dictionary
|
||||||
|
fs.readFile(options.path, 'utf8', (err, data) => { |
||||||
|
if (err) throw err; |
||||||
|
|
||||||
|
this.dictionary = data.split(/[\n\r]+/); |
||||||
|
|
||||||
|
if (readyCallback) readyCallback(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Generates a dictionary-based key, of keyLength words
|
||||||
|
createKey(keyLength) { |
||||||
|
let text = ''; |
||||||
|
|
||||||
|
for (let i = 0; i < keyLength; i++) { |
||||||
|
const index = Math.floor(Math.random() * this.dictionary.length); |
||||||
|
text += this.dictionary[index]; |
||||||
|
} |
||||||
|
|
||||||
|
return text; |
||||||
|
} |
||||||
|
|
||||||
|
}; |
@ -0,0 +1,27 @@ |
|||||||
|
// Draws inspiration from pwgen and http://tools.arantius.com/password
|
||||||
|
|
||||||
|
const randOf = (collection) => { |
||||||
|
return () => { |
||||||
|
return collection[Math.floor(Math.random() * collection.length)]; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
// Helper methods to get an random vowel or consonant
|
||||||
|
const randVowel = randOf('aeiou'); |
||||||
|
const randConsonant = randOf('bcdfghjklmnpqrstvwxyz'); |
||||||
|
|
||||||
|
module.exports = class PhoneticKeyGenerator { |
||||||
|
|
||||||
|
// Generate a phonetic key of alternating consonant & vowel
|
||||||
|
createKey(keyLength) { |
||||||
|
let text = ''; |
||||||
|
const start = Math.round(Math.random()); |
||||||
|
|
||||||
|
for (let i = 0; i < keyLength; i++) { |
||||||
|
text += (i % 2 == start) ? randConsonant() : randVowel(); |
||||||
|
} |
||||||
|
|
||||||
|
return text; |
||||||
|
} |
||||||
|
|
||||||
|
}; |
@ -0,0 +1,20 @@ |
|||||||
|
module.exports = class RandomKeyGenerator { |
||||||
|
|
||||||
|
// Initialize a new generator with the given keySpace
|
||||||
|
constructor(options = {}) { |
||||||
|
this.keyspace = options.keyspace || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
||||||
|
} |
||||||
|
|
||||||
|
// Generate a key of the given length
|
||||||
|
createKey(keyLength) { |
||||||
|
var text = ''; |
||||||
|
|
||||||
|
for (var i = 0; i < keyLength; i++) { |
||||||
|
const index = Math.floor(Math.random() * this.keyspace.length); |
||||||
|
text += this.keyspace.charAt(index); |
||||||
|
} |
||||||
|
|
||||||
|
return text; |
||||||
|
} |
||||||
|
|
||||||
|
}; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,47 @@ |
|||||||
|
{ |
||||||
|
"name": "haste", |
||||||
|
"version": "0.1.0", |
||||||
|
"private": true, |
||||||
|
"description": "Private Pastebin Server", |
||||||
|
"keywords": [ |
||||||
|
"paste", |
||||||
|
"pastebin" |
||||||
|
], |
||||||
|
"author": { |
||||||
|
"name": "John Crepezzi", |
||||||
|
"email": "john.crepezzi@gmail.com", |
||||||
|
"url": "http://seejohncode.com/" |
||||||
|
}, |
||||||
|
"main": "haste", |
||||||
|
"dependencies": { |
||||||
|
"busboy": "0.2.4", |
||||||
|
"connect": "^3.7.0", |
||||||
|
"connect-ratelimit": "0.0.7", |
||||||
|
"connect-route": "0.1.5", |
||||||
|
"pg": "^8.0.0", |
||||||
|
"redis": "0.8.1", |
||||||
|
"redis-url": "0.1.0", |
||||||
|
"st": "^2.0.0", |
||||||
|
"uglify-js": "3.1.6", |
||||||
|
"winston": "^2.0.0" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"mocha": "^8.1.3" |
||||||
|
}, |
||||||
|
"bundledDependencies": [], |
||||||
|
"bin": { |
||||||
|
"haste-server": "./server.js" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"server.js", |
||||||
|
"lib", |
||||||
|
"static" |
||||||
|
], |
||||||
|
"directories": { |
||||||
|
"lib": "./lib" |
||||||
|
}, |
||||||
|
"scripts": { |
||||||
|
"start": "node server.js", |
||||||
|
"test": "mocha --recursive" |
||||||
|
} |
||||||
|
} |
@ -1 +0,0 @@ |
|||||||
{"name":"Haste-server","body":"# Pastebin\r\n\r\nhaste-server is an open source pastebin. You can see an example at [hastebin.com](http://hastebin.com), and it's easy to install locally.\r\n\r\n## GitHub\r\n\r\nCheck out the project on [github](https://github.com/seejohnrun/haste-server)","tagline":"open source pastebin written in node.js","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} |
|
@ -0,0 +1,164 @@ |
|||||||
|
var http = require('http'); |
||||||
|
var fs = require('fs'); |
||||||
|
|
||||||
|
var uglify = require('uglify-js'); |
||||||
|
var winston = require('winston'); |
||||||
|
var connect = require('connect'); |
||||||
|
var route = require('connect-route'); |
||||||
|
var connect_st = require('st'); |
||||||
|
var connect_rate_limit = require('connect-ratelimit'); |
||||||
|
|
||||||
|
var DocumentHandler = require('./lib/document_handler'); |
||||||
|
|
||||||
|
// Load the configuration and set some defaults
|
||||||
|
const configPath = process.argv.length <= 2 ? 'config.js' : process.argv[2]; |
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); |
||||||
|
config.port = process.env.PORT || config.port || 7777; |
||||||
|
config.host = process.env.HOST || config.host || 'localhost'; |
||||||
|
|
||||||
|
// Set up the logger
|
||||||
|
if (config.logging) { |
||||||
|
try { |
||||||
|
winston.remove(winston.transports.Console); |
||||||
|
} catch(e) { |
||||||
|
/* was not present */ |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// build the store from the config on-demand - so that we don't load it
|
||||||
|
// for statics
|
||||||
|
if (!config.storage) { |
||||||
|
config.storage = { type: 'file' }; |
||||||
|
} |
||||||
|
if (!config.storage.type) { |
||||||
|
config.storage.type = 'file'; |
||||||
|
} |
||||||
|
|
||||||
|
var Store, preferredStore; |
||||||
|
|
||||||
|
if (process.env.REDISTOGO_URL && config.storage.type === 'redis') { |
||||||
|
var redisClient = require('redis-url').connect(process.env.REDISTOGO_URL); |
||||||
|
Store = require('./lib/document_stores/redis'); |
||||||
|
preferredStore = new Store(config.storage, redisClient); |
||||||
|
} |
||||||
|
else { |
||||||
|
Store = require('./lib/document_stores/' + config.storage.type); |
||||||
|
preferredStore = new Store(config.storage); |
||||||
|
} |
||||||
|
|
||||||
|
// Compress the static javascript assets
|
||||||
|
if (config.recompressStaticAssets) { |
||||||
|
var list = fs.readdirSync('./static'); |
||||||
|
for (var j = 0; j < list.length; j++) { |
||||||
|
var item = list[j]; |
||||||
|
if ((item.indexOf('.js') === item.length - 3) && (item.indexOf('.min.js') === -1)) { |
||||||
|
var dest = item.substring(0, item.length - 3) + '.min' + item.substring(item.length - 3); |
||||||
|
var orig_code = fs.readFileSync('./static/' + item, 'utf8'); |
||||||
|
|
||||||
|
fs.writeFileSync('./static/' + dest, uglify.minify(orig_code).code, 'utf8'); |
||||||
|
winston.info('compressed ' + item + ' into ' + dest); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Send the static documents into the preferred store, skipping expirations
|
||||||
|
var path, data; |
||||||
|
for (var name in config.documents) { |
||||||
|
path = config.documents[name]; |
||||||
|
data = fs.readFileSync(path, 'utf8'); |
||||||
|
winston.info('loading static document', { name: name, path: path }); |
||||||
|
if (data) { |
||||||
|
preferredStore.set(name, data, function(cb) { |
||||||
|
winston.debug('loaded static document', { success: cb }); |
||||||
|
}, true); |
||||||
|
} |
||||||
|
else { |
||||||
|
winston.warn('failed to load static document', { name: name, path: path }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Pick up a key generator
|
||||||
|
var pwOptions = config.keyGenerator || {}; |
||||||
|
pwOptions.type = pwOptions.type || 'random'; |
||||||
|
var gen = require('./lib/key_generators/' + pwOptions.type); |
||||||
|
var keyGenerator = new gen(pwOptions); |
||||||
|
|
||||||
|
// Configure the document handler
|
||||||
|
var documentHandler = new DocumentHandler({ |
||||||
|
store: preferredStore, |
||||||
|
maxLength: config.maxLength, |
||||||
|
keyLength: config.keyLength, |
||||||
|
keyGenerator: keyGenerator |
||||||
|
}); |
||||||
|
|
||||||
|
var app = connect(); |
||||||
|
|
||||||
|
// Rate limit all requests
|
||||||
|
if (config.rateLimits) { |
||||||
|
config.rateLimits.end = true; |
||||||
|
app.use(connect_rate_limit(config.rateLimits)); |
||||||
|
} |
||||||
|
|
||||||
|
// first look at API calls
|
||||||
|
app.use(route(function(router) { |
||||||
|
// get raw documents - support getting with extension
|
||||||
|
|
||||||
|
router.get('/raw/:id', function(request, response) { |
||||||
|
return documentHandler.handleRawGet(request, response, config); |
||||||
|
}); |
||||||
|
|
||||||
|
router.head('/raw/:id', function(request, response) { |
||||||
|
return documentHandler.handleRawGet(request, response, config); |
||||||
|
}); |
||||||
|
|
||||||
|
// add documents
|
||||||
|
|
||||||
|
router.post('/documents', function(request, response) { |
||||||
|
return documentHandler.handlePost(request, response); |
||||||
|
}); |
||||||
|
|
||||||
|
// get documents
|
||||||
|
router.get('/documents/:id', function(request, response) { |
||||||
|
return documentHandler.handleGet(request, response, config); |
||||||
|
}); |
||||||
|
|
||||||
|
router.head('/documents/:id', function(request, response) { |
||||||
|
return documentHandler.handleGet(request, response, config); |
||||||
|
}); |
||||||
|
})); |
||||||
|
|
||||||
|
// Otherwise, try to match static files
|
||||||
|
app.use(connect_st({ |
||||||
|
path: __dirname + '/static', |
||||||
|
content: { maxAge: config.staticMaxAge }, |
||||||
|
passthrough: true, |
||||||
|
index: false |
||||||
|
})); |
||||||
|
|
||||||
|
// Then we can loop back - and everything else should be a token,
|
||||||
|
// so route it back to /
|
||||||
|
app.use(route(function(router) { |
||||||
|
router.get('/:id', function(request, response, next) { |
||||||
|
request.sturl = '/'; |
||||||
|
next(); |
||||||
|
}); |
||||||
|
})); |
||||||
|
|
||||||
|
// And match index
|
||||||
|
app.use(connect_st({ |
||||||
|
path: __dirname + '/static', |
||||||
|
content: { maxAge: config.staticMaxAge }, |
||||||
|
index: 'index.html' |
||||||
|
})); |
||||||
|
|
||||||
|
http.createServer(app).listen(config.port, config.host); |
||||||
|
|
||||||
|
winston.info('listening on ' + config.host + ':' + config.port); |
@ -0,0 +1,175 @@ |
|||||||
|
body { |
||||||
|
background: #002B36; |
||||||
|
padding: 20px 50px; |
||||||
|
margin: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
/* textarea */ |
||||||
|
|
||||||
|
textarea { |
||||||
|
background: transparent; |
||||||
|
border: 0px; |
||||||
|
color: #fff; |
||||||
|
padding: 0px; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
font-family: monospace; |
||||||
|
outline: none; |
||||||
|
resize: none; |
||||||
|
font-size: 13px; |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
/* the line numbers */ |
||||||
|
|
||||||
|
#linenos { |
||||||
|
color: #7d7d7d; |
||||||
|
z-index: -1000; |
||||||
|
position: absolute; |
||||||
|
top: 20px; |
||||||
|
left: 0px; |
||||||
|
width: 30px; /* 30 to get 20 away from box */ |
||||||
|
font-size: 13px; |
||||||
|
font-family: monospace; |
||||||
|
text-align: right; |
||||||
|
user-select: none; |
||||||
|
} |
||||||
|
|
||||||
|
/* code box when locked */ |
||||||
|
|
||||||
|
#box { |
||||||
|
padding: 0px; |
||||||
|
margin: 0px; |
||||||
|
width: 100%; |
||||||
|
border: 0px; |
||||||
|
outline: none; |
||||||
|
font-size: 13px; |
||||||
|
overflow: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
#box code { |
||||||
|
padding: 0px; |
||||||
|
background: transparent !important; /* don't hide hastebox */ |
||||||
|
} |
||||||
|
|
||||||
|
/* key */ |
||||||
|
|
||||||
|
#key { |
||||||
|
position: fixed; |
||||||
|
top: 0px; |
||||||
|
right: 0px; |
||||||
|
z-index: +1000; /* watch out */ |
||||||
|
} |
||||||
|
|
||||||
|
#box1 { |
||||||
|
padding: 5px; |
||||||
|
text-align: center; |
||||||
|
background: #00222b; |
||||||
|
} |
||||||
|
|
||||||
|
#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; |
||||||
|
} |
||||||
|
|
||||||
|
#box2 .function { |
||||||
|
background: url(function-icons.png); |
||||||
|
width: 32px; |
||||||
|
height: 37px; |
||||||
|
display: inline-block; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
#box2 .link embed { |
||||||
|
vertical-align: bottom; /* fix for zeroClipboard style */ |
||||||
|
} |
||||||
|
|
||||||
|
#box2 .function.enabled:hover { |
||||||
|
cursor: hand; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
#pointer { |
||||||
|
display: block; |
||||||
|
height: 5px; |
||||||
|
width: 10px; |
||||||
|
background: url(hover-dropdown-tip.png); |
||||||
|
bottom: 0px; |
||||||
|
position: absolute; |
||||||
|
margin: auto; |
||||||
|
left: 0px; |
||||||
|
right: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
#box3, #messages li { |
||||||
|
background: #173e48; |
||||||
|
font-family: Helvetica, sans-serif; |
||||||
|
font-size: 12px; |
||||||
|
line-height: 14px; |
||||||
|
padding: 10px 15px; |
||||||
|
user-select: none; |
||||||
|
} |
||||||
|
|
||||||
|
#box3 .label, #messages li { |
||||||
|
color: #fff; |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
|
||||||
|
#box3 .shortcut { |
||||||
|
color: #c4dce3; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
#box2 .function.save { background-position: -5px top; } |
||||||
|
#box2 .function.enabled.save { background-position: -5px center; } |
||||||
|
#box2 .function.enabled.save:hover { background-position: -5px bottom; } |
||||||
|
|
||||||
|
#box2 .function.new { background-position: -42px top; } |
||||||
|
#box2 .function.enabled.new { background-position: -42px center; } |
||||||
|
#box2 .function.enabled.new:hover { background-position: -42px bottom; } |
||||||
|
|
||||||
|
#box2 .function.duplicate { background-position: -79px top; } |
||||||
|
#box2 .function.enabled.duplicate { background-position: -79px center; } |
||||||
|
#box2 .function.enabled.duplicate:hover { background-position: -79px bottom; } |
||||||
|
|
||||||
|
#box2 .function.raw { background-position: -116px top; } |
||||||
|
#box2 .function.enabled.raw { background-position: -116px center; } |
||||||
|
#box2 .function.enabled.raw:hover { background-position: -116px bottom; } |
||||||
|
|
||||||
|
#box2 .function.twitter { background-position: -153px top; } |
||||||
|
#box2 .function.enabled.twitter { background-position: -153px center; } |
||||||
|
#box2 .function.enabled.twitter:hover { background-position: -153px bottom; } |
||||||
|
#box2 .button-picture{ border-width: 0; font-size: inherit; } |
||||||
|
|
||||||
|
#messages { |
||||||
|
position:fixed; |
||||||
|
top:0px; |
||||||
|
right:138px; |
||||||
|
margin:0; |
||||||
|
padding:0; |
||||||
|
width:400px; |
||||||
|
} |
||||||
|
|
||||||
|
#messages li { |
||||||
|
background:rgba(23,62,72,0.8); |
||||||
|
margin:0 auto; |
||||||
|
list-style:none; |
||||||
|
} |
||||||
|
|
||||||
|
#messages li.error { |
||||||
|
background:rgba(102,8,0,0.8); |
||||||
|
} |
||||||
|
|
@ -0,0 +1,398 @@ |
|||||||
|
/* global $, hljs, window, document */ |
||||||
|
|
||||||
|
///// represents a single document
|
||||||
|
|
||||||
|
var haste_document = function() { |
||||||
|
this.locked = false; |
||||||
|
}; |
||||||
|
|
||||||
|
// Escapes HTML tag characters
|
||||||
|
haste_document.prototype.htmlEscape = function(s) { |
||||||
|
return s |
||||||
|
.replace(/&/g, '&') |
||||||
|
.replace(/>/g, '>') |
||||||
|
.replace(/</g, '<') |
||||||
|
.replace(/"/g, '"'); |
||||||
|
}; |
||||||
|
|
||||||
|
// Get this document from the server and lock it here
|
||||||
|
haste_document.prototype.load = function(key, callback, lang) { |
||||||
|
var _this = this; |
||||||
|
$.ajax('/documents/' + key, { |
||||||
|
type: 'get', |
||||||
|
dataType: 'json', |
||||||
|
success: function(res) { |
||||||
|
_this.locked = true; |
||||||
|
_this.key = key; |
||||||
|
_this.data = res.data; |
||||||
|
try { |
||||||
|
var high; |
||||||
|
if (lang === 'txt') { |
||||||
|
high = { value: _this.htmlEscape(res.data) }; |
||||||
|
} |
||||||
|
else if (lang) { |
||||||
|
high = hljs.highlight(lang, res.data); |
||||||
|
} |
||||||
|
else { |
||||||
|
high = hljs.highlightAuto(res.data); |
||||||
|
} |
||||||
|
} catch(err) { |
||||||
|
// failed highlight, fall back on auto
|
||||||
|
high = hljs.highlightAuto(res.data); |
||||||
|
} |
||||||
|
callback({ |
||||||
|
value: high.value, |
||||||
|
key: key, |
||||||
|
language: high.language || lang, |
||||||
|
lineCount: res.data.split('\n').length |
||||||
|
}); |
||||||
|
}, |
||||||
|
error: function() { |
||||||
|
callback(false); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// Save this document to the server and lock it here
|
||||||
|
haste_document.prototype.save = function(data, callback) { |
||||||
|
if (this.locked) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
this.data = data; |
||||||
|
var _this = this; |
||||||
|
$.ajax('/documents', { |
||||||
|
type: 'post', |
||||||
|
data: data, |
||||||
|
dataType: 'json', |
||||||
|
contentType: 'text/plain; charset=utf-8', |
||||||
|
success: function(res) { |
||||||
|
_this.locked = true; |
||||||
|
_this.key = res.key; |
||||||
|
var high = hljs.highlightAuto(data); |
||||||
|
callback(null, { |
||||||
|
value: high.value, |
||||||
|
key: res.key, |
||||||
|
language: high.language, |
||||||
|
lineCount: data.split('\n').length |
||||||
|
}); |
||||||
|
}, |
||||||
|
error: function(res) { |
||||||
|
try { |
||||||
|
callback($.parseJSON(res.responseText)); |
||||||
|
} |
||||||
|
catch (e) { |
||||||
|
callback({message: 'Something went wrong!'}); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
///// represents the paste application
|
||||||
|
|
||||||
|
var haste = function(appName, options) { |
||||||
|
this.appName = appName; |
||||||
|
this.$textarea = $('textarea'); |
||||||
|
this.$box = $('#box'); |
||||||
|
this.$code = $('#box code'); |
||||||
|
this.$linenos = $('#linenos'); |
||||||
|
this.options = options; |
||||||
|
this.configureShortcuts(); |
||||||
|
this.configureButtons(); |
||||||
|
// If twitter is disabled, hide the button
|
||||||
|
if (!options.twitter) { |
||||||
|
$('#box2 .twitter').hide(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Set the page title - include the appName
|
||||||
|
haste.prototype.setTitle = function(ext) { |
||||||
|
var title = ext ? this.appName + ' - ' + ext : this.appName; |
||||||
|
document.title = title; |
||||||
|
}; |
||||||
|
|
||||||
|
// Show a message box
|
||||||
|
haste.prototype.showMessage = function(msg, cls) { |
||||||
|
var msgBox = $('<li class="'+(cls || 'info')+'">'+msg+'</li>'); |
||||||
|
$('#messages').prepend(msgBox); |
||||||
|
setTimeout(function() { |
||||||
|
msgBox.slideUp('fast', function() { $(this).remove(); }); |
||||||
|
}, 3000); |
||||||
|
}; |
||||||
|
|
||||||
|
// Show the light key
|
||||||
|
haste.prototype.lightKey = function() { |
||||||
|
this.configureKey(['new', 'save']); |
||||||
|
}; |
||||||
|
|
||||||
|
// Show the full key
|
||||||
|
haste.prototype.fullKey = function() { |
||||||
|
this.configureKey(['new', 'duplicate', 'twitter', 'raw']); |
||||||
|
}; |
||||||
|
|
||||||
|
// Set the key up for certain things to be enabled
|
||||||
|
haste.prototype.configureKey = function(enable) { |
||||||
|
var $this, i = 0; |
||||||
|
$('#box2 .function').each(function() { |
||||||
|
$this = $(this); |
||||||
|
for (i = 0; i < enable.length; i++) { |
||||||
|
if ($this.hasClass(enable[i])) { |
||||||
|
$this.addClass('enabled'); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
$this.removeClass('enabled'); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// Remove the current document (if there is one)
|
||||||
|
// and set up for a new one
|
||||||
|
haste.prototype.newDocument = function(hideHistory) { |
||||||
|
this.$box.hide(); |
||||||
|
this.doc = new haste_document(); |
||||||
|
if (!hideHistory) { |
||||||
|
window.history.pushState(null, this.appName, '/'); |
||||||
|
} |
||||||
|
this.setTitle(); |
||||||
|
this.lightKey(); |
||||||
|
this.$textarea.val('').show('fast', function() { |
||||||
|
this.focus(); |
||||||
|
}); |
||||||
|
this.removeLineNumbers(); |
||||||
|
}; |
||||||
|
|
||||||
|
// Map of common extensions
|
||||||
|
// Note: this list does not need to include anything that IS its extension,
|
||||||
|
// due to the behavior of lookupTypeByExtension and lookupExtensionByType
|
||||||
|
// Note: optimized for lookupTypeByExtension
|
||||||
|
haste.extensionMap = { |
||||||
|
rb: 'ruby', py: 'python', pl: 'perl', php: 'php', scala: 'scala', go: 'go', |
||||||
|
xml: 'xml', html: 'xml', htm: 'xml', css: 'css', js: 'javascript', vbs: 'vbscript', |
||||||
|
lua: 'lua', pas: 'delphi', java: 'java', cpp: 'cpp', cc: 'cpp', m: 'objectivec', |
||||||
|
vala: 'vala', sql: 'sql', sm: 'smalltalk', lisp: 'lisp', ini: 'ini', |
||||||
|
diff: 'diff', bash: 'bash', sh: 'bash', tex: 'tex', erl: 'erlang', hs: 'haskell', |
||||||
|
md: 'markdown', txt: '', coffee: 'coffee', swift: 'swift' |
||||||
|
}; |
||||||
|
|
||||||
|
// Look up the extension preferred for a type
|
||||||
|
// If not found, return the type itself - which we'll place as the extension
|
||||||
|
haste.prototype.lookupExtensionByType = function(type) { |
||||||
|
for (var key in haste.extensionMap) { |
||||||
|
if (haste.extensionMap[key] === type) return key; |
||||||
|
} |
||||||
|
return type; |
||||||
|
}; |
||||||
|
|
||||||
|
// Look up the type for a given extension
|
||||||
|
// If not found, return the extension - which we'll attempt to use as the type
|
||||||
|
haste.prototype.lookupTypeByExtension = function(ext) { |
||||||
|
return haste.extensionMap[ext] || ext; |
||||||
|
}; |
||||||
|
|
||||||
|
// Add line numbers to the document
|
||||||
|
// For the specified number of lines
|
||||||
|
haste.prototype.addLineNumbers = function(lineCount) { |
||||||
|
var h = ''; |
||||||
|
for (var i = 0; i < lineCount; i++) { |
||||||
|
h += (i + 1).toString() + '<br/>'; |
||||||
|
} |
||||||
|
$('#linenos').html(h); |
||||||
|
}; |
||||||
|
|
||||||
|
// Remove the line numbers
|
||||||
|
haste.prototype.removeLineNumbers = function() { |
||||||
|
$('#linenos').html('>'); |
||||||
|
}; |
||||||
|
|
||||||
|
// Load a document and show it
|
||||||
|
haste.prototype.loadDocument = function(key) { |
||||||
|
// Split the key up
|
||||||
|
var parts = key.split('.', 2); |
||||||
|
// Ask for what we want
|
||||||
|
var _this = this; |
||||||
|
_this.doc = new haste_document(); |
||||||
|
_this.doc.load(parts[0], function(ret) { |
||||||
|
if (ret) { |
||||||
|
_this.$code.html(ret.value); |
||||||
|
_this.setTitle(ret.key); |
||||||
|
_this.fullKey(); |
||||||
|
_this.$textarea.val('').hide(); |
||||||
|
_this.$box.show().focus(); |
||||||
|
_this.addLineNumbers(ret.lineCount); |
||||||
|
} |
||||||
|
else { |
||||||
|
_this.newDocument(); |
||||||
|
} |
||||||
|
}, this.lookupTypeByExtension(parts[1])); |
||||||
|
}; |
||||||
|
|
||||||
|
// Duplicate the current document - only if locked
|
||||||
|
haste.prototype.duplicateDocument = function() { |
||||||
|
if (this.doc.locked) { |
||||||
|
var currentData = this.doc.data; |
||||||
|
this.newDocument(); |
||||||
|
this.$textarea.val(currentData); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Lock the current document
|
||||||
|
haste.prototype.lockDocument = function() { |
||||||
|
var _this = this; |
||||||
|
this.doc.save(this.$textarea.val(), function(err, ret) { |
||||||
|
if (err) { |
||||||
|
_this.showMessage(err.message, 'error'); |
||||||
|
} |
||||||
|
else if (ret) { |
||||||
|
_this.$code.html(ret.value); |
||||||
|
_this.setTitle(ret.key); |
||||||
|
var file = '/' + ret.key; |
||||||
|
if (ret.language) { |
||||||
|
file += '.' + _this.lookupExtensionByType(ret.language); |
||||||
|
} |
||||||
|
window.history.pushState(null, _this.appName + '-' + ret.key, file); |
||||||
|
_this.fullKey(); |
||||||
|
_this.$textarea.val('').hide(); |
||||||
|
_this.$box.show().focus(); |
||||||
|
_this.addLineNumbers(ret.lineCount); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
haste.prototype.configureButtons = function() { |
||||||
|
var _this = this; |
||||||
|
this.buttons = [ |
||||||
|
{ |
||||||
|
$where: $('#box2 .save'), |
||||||
|
label: 'Save', |
||||||
|
shortcutDescription: 'control + s', |
||||||
|
shortcut: function(evt) { |
||||||
|
return evt.ctrlKey && (evt.keyCode === 83); |
||||||
|
}, |
||||||
|
action: function() { |
||||||
|
if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') { |
||||||
|
_this.lockDocument(); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
$where: $('#box2 .new'), |
||||||
|
label: 'New', |
||||||
|
shortcut: function(evt) { |
||||||
|
return evt.ctrlKey && evt.keyCode === 78; |
||||||
|
}, |
||||||
|
shortcutDescription: 'control + n', |
||||||
|
action: function() { |
||||||
|
_this.newDocument(!_this.doc.key); |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
$where: $('#box2 .duplicate'), |
||||||
|
label: 'Duplicate & Edit', |
||||||
|
shortcut: function(evt) { |
||||||
|
return _this.doc.locked && evt.ctrlKey && evt.keyCode === 68; |
||||||
|
}, |
||||||
|
shortcutDescription: 'control + d', |
||||||
|
action: function() { |
||||||
|
_this.duplicateDocument(); |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
$where: $('#box2 .raw'), |
||||||
|
label: 'Just Text', |
||||||
|
shortcut: function(evt) { |
||||||
|
return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82; |
||||||
|
}, |
||||||
|
shortcutDescription: 'control + shift + r', |
||||||
|
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++) { |
||||||
|
this.configureButton(this.buttons[i]); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
haste.prototype.configureButton = function(options) { |
||||||
|
// Handle the click action
|
||||||
|
options.$where.click(function(evt) { |
||||||
|
evt.preventDefault(); |
||||||
|
if (!options.clickDisabled && $(this).hasClass('enabled')) { |
||||||
|
options.action(); |
||||||
|
} |
||||||
|
}); |
||||||
|
// Show the label
|
||||||
|
options.$where.mouseenter(function() { |
||||||
|
$('#box3 .label').text(options.label); |
||||||
|
$('#box3 .shortcut').text(options.shortcutDescription || ''); |
||||||
|
$('#box3').show(); |
||||||
|
$(this).append($('#pointer').remove().show()); |
||||||
|
}); |
||||||
|
// Hide the label
|
||||||
|
options.$where.mouseleave(function() { |
||||||
|
$('#box3').hide(); |
||||||
|
$('#pointer').hide(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// Configure keyboard shortcuts for the textarea
|
||||||
|
haste.prototype.configureShortcuts = function() { |
||||||
|
var _this = this; |
||||||
|
$(document.body).keydown(function(evt) { |
||||||
|
var button; |
||||||
|
for (var i = 0 ; i < _this.buttons.length; i++) { |
||||||
|
button = _this.buttons[i]; |
||||||
|
if (button.shortcut && button.shortcut(evt)) { |
||||||
|
evt.preventDefault(); |
||||||
|
button.action(); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
///// Tab behavior in the textarea - 2 spaces per tab
|
||||||
|
$(function() { |
||||||
|
|
||||||
|
$('textarea').keydown(function(evt) { |
||||||
|
if (evt.keyCode === 9) { |
||||||
|
evt.preventDefault(); |
||||||
|
var myValue = ' '; |
||||||
|
// http://stackoverflow.com/questions/946534/insert-text-into-textarea-with-jquery
|
||||||
|
// For browsers like Internet Explorer
|
||||||
|
if (document.selection) { |
||||||
|
this.focus(); |
||||||
|
var sel = document.selection.createRange(); |
||||||
|
sel.text = myValue; |
||||||
|
this.focus(); |
||||||
|
} |
||||||
|
// Mozilla and Webkit
|
||||||
|
else if (this.selectionStart || this.selectionStart == '0') { |
||||||
|
var startPos = this.selectionStart; |
||||||
|
var endPos = this.selectionEnd; |
||||||
|
var 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; |
||||||
|
this.selectionEnd = startPos + myValue.length; |
||||||
|
this.scrollTop = scrollTop; |
||||||
|
} |
||||||
|
else { |
||||||
|
this.value += myValue; |
||||||
|
this.focus(); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 6.1 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,68 @@ |
|||||||
|
<html> |
||||||
|
|
||||||
|
<head> |
||||||
|
|
||||||
|
<title>hastebin</title> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<link rel="stylesheet" type="text/css" href="solarized_dark.css"/> |
||||||
|
<link rel="stylesheet" type="text/css" href="application.css"/> |
||||||
|
|
||||||
|
<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> |
||||||
|
|
||||||
|
<meta name="robots" content="noindex,nofollow"/> |
||||||
|
|
||||||
|
<script type="text/javascript"> |
||||||
|
var app = null; |
||||||
|
// Handle pops |
||||||
|
var handlePop = function(evt) { |
||||||
|
var path = evt.target.location.pathname; |
||||||
|
if (path === '/') { app.newDocument(true); } |
||||||
|
else { app.loadDocument(path.substring(1, path.length)); } |
||||||
|
}; |
||||||
|
// Set up the pop state to handle loads, skipping the first load |
||||||
|
// to make chrome behave like others: |
||||||
|
// http://code.google.com/p/chromium/issues/detail?id=63040 |
||||||
|
setTimeout(function() { |
||||||
|
window.onpopstate = function(evt) { |
||||||
|
try { handlePop(evt); } catch(err) { /* not loaded yet */ } |
||||||
|
}; |
||||||
|
}, 1000); |
||||||
|
// Construct app and load initial path |
||||||
|
$(function() { |
||||||
|
app = new haste('hastebin', { twitter: true }); |
||||||
|
handlePop({ target: window }); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
|
||||||
|
</head> |
||||||
|
|
||||||
|
<body> |
||||||
|
<ul id="messages"></ul> |
||||||
|
|
||||||
|
<div id="key"> |
||||||
|
<div id="pointer" style="display:none;"></div> |
||||||
|
<div id="box1"> |
||||||
|
<a href="/about.md" class="logo"></a> |
||||||
|
</div> |
||||||
|
<div id="box2"> |
||||||
|
<button class="save function button-picture">Save</button> |
||||||
|
<button class="new function button-picture">New</button> |
||||||
|
<button class="duplicate function button-picture">Duplicate & Edit</button> |
||||||
|
<button class="raw function button-picture">Just Text</button> |
||||||
|
<button class="twitter function button-picture">Twitter</button> |
||||||
|
</div> |
||||||
|
<div id="box3" style="display:none;"> |
||||||
|
<div class="label"></div> |
||||||
|
<div class="shortcut"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div id="linenos"></div> |
||||||
|
<pre id="box" style="display:none;" class="hljs" tabindex="0"><code></code></pre> |
||||||
|
<textarea spellcheck="false" style="display:none;"></textarea> |
||||||
|
|
||||||
|
</body> |
||||||
|
|
||||||
|
</html> |
After Width: | Height: | Size: 4.6 KiB |
@ -0,0 +1,4 @@ |
|||||||
|
User-agent: * |
||||||
|
Disallow: /* |
||||||
|
Allow: /?okparam= |
||||||
|
Allow: /$ |
@ -0,0 +1,84 @@ |
|||||||
|
/* |
||||||
|
|
||||||
|
Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull <sourdrums@gmail.com> |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
.hljs { |
||||||
|
display: block; |
||||||
|
overflow-x: auto; |
||||||
|
padding: 0.5em; |
||||||
|
background: #002b36; |
||||||
|
color: #839496; |
||||||
|
} |
||||||
|
|
||||||
|
.hljs-comment, |
||||||
|
.hljs-quote { |
||||||
|
color: #586e75; |
||||||
|
} |
||||||
|
|
||||||
|
/* Solarized Green */ |
||||||
|
.hljs-keyword, |
||||||
|
.hljs-selector-tag, |
||||||
|
.hljs-addition { |
||||||
|
color: #859900; |
||||||
|
} |
||||||
|
|
||||||
|
/* Solarized Cyan */ |
||||||
|
.hljs-number, |
||||||
|
.hljs-string, |
||||||
|
.hljs-meta .hljs-meta-string, |
||||||
|
.hljs-literal, |
||||||
|
.hljs-doctag, |
||||||
|
.hljs-regexp { |
||||||
|
color: #2aa198; |
||||||
|
} |
||||||
|
|
||||||
|
/* Solarized Blue */ |
||||||
|
.hljs-title, |
||||||
|
.hljs-section, |
||||||
|
.hljs-name, |
||||||
|
.hljs-selector-id, |
||||||
|
.hljs-selector-class { |
||||||
|
color: #268bd2; |
||||||
|
} |
||||||
|
|
||||||
|
/* Solarized Yellow */ |
||||||
|
.hljs-attribute, |
||||||
|
.hljs-attr, |
||||||
|
.hljs-variable, |
||||||
|
.hljs-template-variable, |
||||||
|
.hljs-class .hljs-title, |
||||||
|
.hljs-type { |
||||||
|
color: #b58900; |
||||||
|
} |
||||||
|
|
||||||
|
/* Solarized Orange */ |
||||||
|
.hljs-symbol, |
||||||
|
.hljs-bullet, |
||||||
|
.hljs-subst, |
||||||
|
.hljs-meta, |
||||||
|
.hljs-meta .hljs-keyword, |
||||||
|
.hljs-selector-attr, |
||||||
|
.hljs-selector-pseudo, |
||||||
|
.hljs-link { |
||||||
|
color: #cb4b16; |
||||||
|
} |
||||||
|
|
||||||
|
/* Solarized Red */ |
||||||
|
.hljs-built_in, |
||||||
|
.hljs-deletion { |
||||||
|
color: #dc322f; |
||||||
|
} |
||||||
|
|
||||||
|
.hljs-formula { |
||||||
|
background: #073642; |
||||||
|
} |
||||||
|
|
||||||
|
.hljs-emphasis { |
||||||
|
font-style: italic; |
||||||
|
} |
||||||
|
|
||||||
|
.hljs-strong { |
||||||
|
font-weight: bold; |
||||||
|
} |
@ -1,69 +0,0 @@ |
|||||||
.highlight { background: #ffffff; } |
|
||||||
.highlight .c { color: #999988; font-style: italic } /* Comment */ |
|
||||||
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ |
|
||||||
.highlight .k { font-weight: bold } /* Keyword */ |
|
||||||
.highlight .o { font-weight: bold } /* Operator */ |
|
||||||
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ |
|
||||||
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ |
|
||||||
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ |
|
||||||
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ |
|
||||||
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ |
|
||||||
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ |
|
||||||
.highlight .ge { font-style: italic } /* Generic.Emph */ |
|
||||||
.highlight .gr { color: #aa0000 } /* Generic.Error */ |
|
||||||
.highlight .gh { color: #999999 } /* Generic.Heading */ |
|
||||||
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ |
|
||||||
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ |
|
||||||
.highlight .go { color: #888888 } /* Generic.Output */ |
|
||||||
.highlight .gp { color: #555555 } /* Generic.Prompt */ |
|
||||||
.highlight .gs { font-weight: bold } /* Generic.Strong */ |
|
||||||
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ |
|
||||||
.highlight .gt { color: #aa0000 } /* Generic.Traceback */ |
|
||||||
.highlight .kc { font-weight: bold } /* Keyword.Constant */ |
|
||||||
.highlight .kd { font-weight: bold } /* Keyword.Declaration */ |
|
||||||
.highlight .kn { font-weight: bold } /* Keyword.Namespace */ |
|
||||||
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ |
|
||||||
.highlight .kr { font-weight: bold } /* Keyword.Reserved */ |
|
||||||
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ |
|
||||||
.highlight .m { color: #009999 } /* Literal.Number */ |
|
||||||
.highlight .s { color: #d14 } /* Literal.String */ |
|
||||||
.highlight .na { color: #008080 } /* Name.Attribute */ |
|
||||||
.highlight .nb { color: #0086B3 } /* Name.Builtin */ |
|
||||||
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ |
|
||||||
.highlight .no { color: #008080 } /* Name.Constant */ |
|
||||||
.highlight .ni { color: #800080 } /* Name.Entity */ |
|
||||||
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ |
|
||||||
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ |
|
||||||
.highlight .nn { color: #555555 } /* Name.Namespace */ |
|
||||||
.highlight .nt { color: #000080 } /* Name.Tag */ |
|
||||||
.highlight .nv { color: #008080 } /* Name.Variable */ |
|
||||||
.highlight .ow { font-weight: bold } /* Operator.Word */ |
|
||||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */ |
|
||||||
.highlight .mf { color: #009999 } /* Literal.Number.Float */ |
|
||||||
.highlight .mh { color: #009999 } /* Literal.Number.Hex */ |
|
||||||
.highlight .mi { color: #009999 } /* Literal.Number.Integer */ |
|
||||||
.highlight .mo { color: #009999 } /* Literal.Number.Oct */ |
|
||||||
.highlight .sb { color: #d14 } /* Literal.String.Backtick */ |
|
||||||
.highlight .sc { color: #d14 } /* Literal.String.Char */ |
|
||||||
.highlight .sd { color: #d14 } /* Literal.String.Doc */ |
|
||||||
.highlight .s2 { color: #d14 } /* Literal.String.Double */ |
|
||||||
.highlight .se { color: #d14 } /* Literal.String.Escape */ |
|
||||||
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */ |
|
||||||
.highlight .si { color: #d14 } /* Literal.String.Interpol */ |
|
||||||
.highlight .sx { color: #d14 } /* Literal.String.Other */ |
|
||||||
.highlight .sr { color: #009926 } /* Literal.String.Regex */ |
|
||||||
.highlight .s1 { color: #d14 } /* Literal.String.Single */ |
|
||||||
.highlight .ss { color: #990073 } /* Literal.String.Symbol */ |
|
||||||
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ |
|
||||||
.highlight .vc { color: #008080 } /* Name.Variable.Class */ |
|
||||||
.highlight .vg { color: #008080 } /* Name.Variable.Global */ |
|
||||||
.highlight .vi { color: #008080 } /* Name.Variable.Instance */ |
|
||||||
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ |
|
||||||
|
|
||||||
.type-csharp .highlight .k { color: #0000FF } |
|
||||||
.type-csharp .highlight .kt { color: #0000FF } |
|
||||||
.type-csharp .highlight .nf { color: #000000; font-weight: normal } |
|
||||||
.type-csharp .highlight .nc { color: #2B91AF } |
|
||||||
.type-csharp .highlight .nn { color: #000000 } |
|
||||||
.type-csharp .highlight .s { color: #A31515 } |
|
||||||
.type-csharp .highlight .sc { color: #A31515 } |
|
@ -1,255 +0,0 @@ |
|||||||
@import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); |
|
||||||
|
|
||||||
body { |
|
||||||
padding:50px; |
|
||||||
font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; |
|
||||||
color:#777; |
|
||||||
font-weight:300; |
|
||||||
} |
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 { |
|
||||||
color:#222; |
|
||||||
margin:0 0 20px; |
|
||||||
} |
|
||||||
|
|
||||||
p, ul, ol, table, pre, dl { |
|
||||||
margin:0 0 20px; |
|
||||||
} |
|
||||||
|
|
||||||
h1, h2, h3 { |
|
||||||
line-height:1.1; |
|
||||||
} |
|
||||||
|
|
||||||
h1 { |
|
||||||
font-size:28px; |
|
||||||
} |
|
||||||
|
|
||||||
h2 { |
|
||||||
color:#393939; |
|
||||||
} |
|
||||||
|
|
||||||
h3, h4, h5, h6 { |
|
||||||
color:#494949; |
|
||||||
} |
|
||||||
|
|
||||||
a { |
|
||||||
color:#39c; |
|
||||||
font-weight:400; |
|
||||||
text-decoration:none; |
|
||||||
} |
|
||||||
|
|
||||||
a small { |
|
||||||
font-size:11px; |
|
||||||
color:#777; |
|
||||||
margin-top:-0.6em; |
|
||||||
display:block; |
|
||||||
} |
|
||||||
|
|
||||||
.wrapper { |
|
||||||
width:860px; |
|
||||||
margin:0 auto; |
|
||||||
} |
|
||||||
|
|
||||||
blockquote { |
|
||||||
border-left:1px solid #e5e5e5; |
|
||||||
margin:0; |
|
||||||
padding:0 0 0 20px; |
|
||||||
font-style:italic; |
|
||||||
} |
|
||||||
|
|
||||||
code, pre { |
|
||||||
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; |
|
||||||
color:#333; |
|
||||||
font-size:12px; |
|
||||||
} |
|
||||||
|
|
||||||
pre { |
|
||||||
padding:8px 15px; |
|
||||||
background: #f8f8f8; |
|
||||||
border-radius:5px; |
|
||||||
border:1px solid #e5e5e5; |
|
||||||
overflow-x: auto; |
|
||||||
} |
|
||||||
|
|
||||||
table { |
|
||||||
width:100%; |
|
||||||
border-collapse:collapse; |
|
||||||
} |
|
||||||
|
|
||||||
th, td { |
|
||||||
text-align:left; |
|
||||||
padding:5px 10px; |
|
||||||
border-bottom:1px solid #e5e5e5; |
|
||||||
} |
|
||||||
|
|
||||||
dt { |
|
||||||
color:#444; |
|
||||||
font-weight:700; |
|
||||||
} |
|
||||||
|
|
||||||
th { |
|
||||||
color:#444; |
|
||||||
} |
|
||||||
|
|
||||||
img { |
|
||||||
max-width:100%; |
|
||||||
} |
|
||||||
|
|
||||||
header { |
|
||||||
width:270px; |
|
||||||
float:left; |
|
||||||
position:fixed; |
|
||||||
} |
|
||||||
|
|
||||||
header ul { |
|
||||||
list-style:none; |
|
||||||
height:40px; |
|
||||||
|
|
||||||
padding:0; |
|
||||||
|
|
||||||
background: #eee; |
|
||||||
background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); |
|
||||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); |
|
||||||
background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
|
||||||
background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
|
||||||
background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
|
||||||
background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
|
||||||
|
|
||||||
border-radius:5px; |
|
||||||
border:1px solid #d2d2d2; |
|
||||||
box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; |
|
||||||
width:270px; |
|
||||||
} |
|
||||||
|
|
||||||
header li { |
|
||||||
width:89px; |
|
||||||
float:left; |
|
||||||
border-right:1px solid #d2d2d2; |
|
||||||
height:40px; |
|
||||||
} |
|
||||||
|
|
||||||
header ul a { |
|
||||||
line-height:1; |
|
||||||
font-size:11px; |
|
||||||
color:#999; |
|
||||||
display:block; |
|
||||||
text-align:center; |
|
||||||
padding-top:6px; |
|
||||||
height:40px; |
|
||||||
} |
|
||||||
|
|
||||||
strong { |
|
||||||
color:#222; |
|
||||||
font-weight:700; |
|
||||||
} |
|
||||||
|
|
||||||
header ul li + li { |
|
||||||
width:88px; |
|
||||||
border-left:1px solid #fff; |
|
||||||
} |
|
||||||
|
|
||||||
header ul li + li + li { |
|
||||||
border-right:none; |
|
||||||
width:89px; |
|
||||||
} |
|
||||||
|
|
||||||
header ul a strong { |
|
||||||
font-size:14px; |
|
||||||
display:block; |
|
||||||
color:#222; |
|
||||||
} |
|
||||||
|
|
||||||
section { |
|
||||||
width:500px; |
|
||||||
float:right; |
|
||||||
padding-bottom:50px; |
|
||||||
} |
|
||||||
|
|
||||||
small { |
|
||||||
font-size:11px; |
|
||||||
} |
|
||||||
|
|
||||||
hr { |
|
||||||
border:0; |
|
||||||
background:#e5e5e5; |
|
||||||
height:1px; |
|
||||||
margin:0 0 20px; |
|
||||||
} |
|
||||||
|
|
||||||
footer { |
|
||||||
width:270px; |
|
||||||
float:left; |
|
||||||
position:fixed; |
|
||||||
bottom:50px; |
|
||||||
} |
|
||||||
|
|
||||||
@media print, screen and (max-width: 960px) { |
|
||||||
|
|
||||||
div.wrapper { |
|
||||||
width:auto; |
|
||||||
margin:0; |
|
||||||
} |
|
||||||
|
|
||||||
header, section, footer { |
|
||||||
float:none; |
|
||||||
position:static; |
|
||||||
width:auto; |
|
||||||
} |
|
||||||
|
|
||||||
header { |
|
||||||
padding-right:320px; |
|
||||||
} |
|
||||||
|
|
||||||
section { |
|
||||||
border:1px solid #e5e5e5; |
|
||||||
border-width:1px 0; |
|
||||||
padding:20px 0; |
|
||||||
margin:0 0 20px; |
|
||||||
} |
|
||||||
|
|
||||||
header a small { |
|
||||||
display:inline; |
|
||||||
} |
|
||||||
|
|
||||||
header ul { |
|
||||||
position:absolute; |
|
||||||
right:50px; |
|
||||||
top:52px; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@media print, screen and (max-width: 720px) { |
|
||||||
body { |
|
||||||
word-wrap:break-word; |
|
||||||
} |
|
||||||
|
|
||||||
header { |
|
||||||
padding:0; |
|
||||||
} |
|
||||||
|
|
||||||
header ul, header p.view { |
|
||||||
position:static; |
|
||||||
} |
|
||||||
|
|
||||||
pre, code { |
|
||||||
word-wrap:normal; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@media print, screen and (max-width: 480px) { |
|
||||||
body { |
|
||||||
padding:15px; |
|
||||||
} |
|
||||||
|
|
||||||
header ul { |
|
||||||
display:none; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@media print { |
|
||||||
body { |
|
||||||
padding:0.4in; |
|
||||||
font-size:12pt; |
|
||||||
color:#444; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,26 @@ |
|||||||
|
/* global describe, it */ |
||||||
|
|
||||||
|
var assert = require('assert'); |
||||||
|
|
||||||
|
var DocumentHandler = require('../lib/document_handler'); |
||||||
|
var Generator = require('../lib/key_generators/random'); |
||||||
|
|
||||||
|
describe('document_handler', function() { |
||||||
|
|
||||||
|
describe('randomKey', function() { |
||||||
|
|
||||||
|
it('should choose a key of the proper length', function() { |
||||||
|
var gen = new Generator(); |
||||||
|
var dh = new DocumentHandler({ keyLength: 6, keyGenerator: gen }); |
||||||
|
assert.equal(6, dh.acceptableKey().length); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should choose a default key length', function() { |
||||||
|
var gen = new Generator(); |
||||||
|
var dh = new DocumentHandler({ keyGenerator: gen }); |
||||||
|
assert.equal(dh.keyLength, DocumentHandler.defaultKeyLength); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
@ -0,0 +1,34 @@ |
|||||||
|
/* global describe, it */ |
||||||
|
|
||||||
|
const assert = require('assert'); |
||||||
|
|
||||||
|
const fs = require('fs'); |
||||||
|
|
||||||
|
const Generator = require('../../lib/key_generators/dictionary'); |
||||||
|
|
||||||
|
describe('DictionaryGenerator', function() { |
||||||
|
describe('options', function() { |
||||||
|
it('should throw an error if given no options', () => { |
||||||
|
assert.throws(() => { |
||||||
|
new Generator(); |
||||||
|
}, Error); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should throw an error if given no path', () => { |
||||||
|
assert.throws(() => { |
||||||
|
new Generator({}); |
||||||
|
}, Error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
describe('generation', function() { |
||||||
|
it('should return a key of the proper number of words from the given dictionary', () => { |
||||||
|
const path = '/tmp/haste-server-test-dictionary'; |
||||||
|
const words = ['cat']; |
||||||
|
fs.writeFileSync(path, words.join('\n')); |
||||||
|
|
||||||
|
const gen = new Generator({path}, () => { |
||||||
|
assert.equal('catcatcat', gen.createKey(3)); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,35 @@ |
|||||||
|
/* global describe, it */ |
||||||
|
|
||||||
|
const assert = require('assert'); |
||||||
|
|
||||||
|
const Generator = require('../../lib/key_generators/phonetic'); |
||||||
|
|
||||||
|
const vowels = 'aeiou'; |
||||||
|
const consonants = 'bcdfghjklmnpqrstvwxyz'; |
||||||
|
|
||||||
|
describe('PhoneticKeyGenerator', () => { |
||||||
|
describe('generation', () => { |
||||||
|
it('should return a key of the proper length', () => { |
||||||
|
const gen = new Generator(); |
||||||
|
assert.equal(6, gen.createKey(6).length); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should alternate consonants and vowels', () => { |
||||||
|
const gen = new Generator(); |
||||||
|
|
||||||
|
const key = gen.createKey(3); |
||||||
|
|
||||||
|
// if it starts with a consonant, we expect cvc
|
||||||
|
// if it starts with a vowel, we expect vcv
|
||||||
|
if(consonants.includes(key[0])) { |
||||||
|
assert.ok(consonants.includes(key[0])); |
||||||
|
assert.ok(consonants.includes(key[2])); |
||||||
|
assert.ok(vowels.includes(key[1])); |
||||||
|
} else { |
||||||
|
assert.ok(vowels.includes(key[0])); |
||||||
|
assert.ok(vowels.includes(key[2])); |
||||||
|
assert.ok(consonants.includes(key[1])); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,24 @@ |
|||||||
|
/* global describe, it */ |
||||||
|
|
||||||
|
const assert = require('assert'); |
||||||
|
|
||||||
|
const Generator = require('../../lib/key_generators/random'); |
||||||
|
|
||||||
|
describe('RandomKeyGenerator', () => { |
||||||
|
describe('generation', () => { |
||||||
|
it('should return a key of the proper length', () => { |
||||||
|
const gen = new Generator(); |
||||||
|
assert.equal(gen.createKey(6).length, 6); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should use a key from the given keyset if given', () => { |
||||||
|
const gen = new Generator({keyspace: 'A'}); |
||||||
|
assert.equal(gen.createKey(6), 'AAAAAA'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not use a key from the given keyset if not given', () => { |
||||||
|
const gen = new Generator({keyspace: 'A'}); |
||||||
|
assert.ok(!gen.createKey(6).includes('B')); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,54 @@ |
|||||||
|
/* global it, describe, afterEach */ |
||||||
|
|
||||||
|
var assert = require('assert'); |
||||||
|
|
||||||
|
var winston = require('winston'); |
||||||
|
winston.remove(winston.transports.Console); |
||||||
|
|
||||||
|
var RedisDocumentStore = require('../lib/document_stores/redis'); |
||||||
|
|
||||||
|
describe('redis_document_store', function() { |
||||||
|
|
||||||
|
/* reconnect to redis on each test */ |
||||||
|
afterEach(function() { |
||||||
|
if (RedisDocumentStore.client) { |
||||||
|
RedisDocumentStore.client.quit(); |
||||||
|
RedisDocumentStore.client = false; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
describe('set', function() { |
||||||
|
|
||||||
|
it('should be able to set a key and have an expiration set', function(done) { |
||||||
|
var store = new RedisDocumentStore({ expire: 10 }); |
||||||
|
store.set('hello1', 'world', function() { |
||||||
|
RedisDocumentStore.client.ttl('hello1', function(err, res) { |
||||||
|
assert.ok(res > 1); |
||||||
|
done(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not set an expiration when told not to', function(done) { |
||||||
|
var store = new RedisDocumentStore({ expire: 10 }); |
||||||
|
store.set('hello2', 'world', function() { |
||||||
|
RedisDocumentStore.client.ttl('hello2', function(err, res) { |
||||||
|
assert.equal(-1, res); |
||||||
|
done(); |
||||||
|
}); |
||||||
|
}, true); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not set an expiration when expiration is off', function(done) { |
||||||
|
var store = new RedisDocumentStore({ expire: false }); |
||||||
|
store.set('hello3', 'world', function() { |
||||||
|
RedisDocumentStore.client.ttl('hello3', function(err, res) { |
||||||
|
assert.equal(-1, res); |
||||||
|
done(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
Loading…
Reference in new issue