initial commit

pull/1/head
xuu 2017-04-14 16:15:07 -06:00
commit 8e6da6cd30
19 changed files with 3387 additions and 0 deletions

283
assets/app.js Normal file
View File

@ -0,0 +1,283 @@
var app = angular.module('souris-app', ['souris-common', 'hljs']).
config(function ($routeProvider) {
$routeProvider.
when('/', {controller: CreateCtrl, templateUrl: 'create.html'}).
when('/:id!:key', {controller: ViewCtrl, templateUrl: 'view.html'}).
when('/:id', {controller: ViewCtrl, templateUrl: 'view.html'}).
otherwise({redirectTo: '/'});
});
var TEXT = "";
function CreateCtrl($scope, $remoteService, $location, $route) {
"use strict";
$scope.reload = $route.reload;
$scope.o = {text: TEXT};
var base_url = $location.absUrl();
base_url = base_url.slice(0, base_url.indexOf('#'));
$scope.$base_url = base_url;
// Add Randomness to RNG
console.info("Adding extra entropy from server and user input.");
$scope.entropy = 0;
var addEntropy = function (s) {
$scope.entropy += s.length;
fn.seed(s, {entropy: true});
};
$remoteService('/paste/rng').get().success(addEntropy);
(function (events, count) {
var t = [];
function w(e) {
t.push([e.pageX, e.pageY, e.keyCode, +new Date]);
if (t.length < count) {
return;
}
addEntropy(t);
$scope.$apply();
t = [];
}
for (var i in events)
if (events.hasOwnProperty(i))
document.addEventListener(events[i], w);
})(['mousemove', 'keydown', 'keypress', 'click', 'scroll'], 16);
$scope.HighliteLang = fn.obj(HighliteLang);
$scope.ExpireTimes = fn.obj(ExpireTimes);
// Encrypt and send function
$scope.Encrypt = function (o) {
if (o.text.length > 512) {o.zip = true; $scope.o.zip = true; }
var e = encrypt(o);
console.log("Sending:\n" + json(e));
$remoteService('/paste')
.post({}, e.txt)
.success(function (d) {
console.log("Received:\n" + d);
d = d.split(' ');
$scope.result = {status: d[0], id: d[1], key: e.key, text: e.txt};
});
};
}
CreateCtrl.$inject = ['$scope', '$remoteService', '$location', '$route'];
function ViewCtrl($scope, $params, $remoteService, $location) {
"use strict";
var base_url = $location.absUrl();
base_url = base_url.slice(0, base_url.indexOf('#'));
$scope.$base_url = base_url;
var id = $params.id,
key = $params.key;
$scope.id = id;
$scope.key = key;
function store(o) {
$scope.store = o;
}
$scope.copy = function(t) {
TEXT = t;
$location.path('/');
}
$remoteService('/paste/:id')
.get({id: id})
.success(decrypt(key, store))
.error(function(d, c){
var msg = '';
switch(c){
case 403: msg = 'Authentication Required.'; break;
case 404: msg = 'Message Not Found.'; break;
case 410: msg = 'Message Expired.'; break;
}
$scope.store = {err:msg, code:c};
});
}
ViewCtrl.$inject = ['$scope', '$routeParams', '$remoteService', '$location'];
var HighliteLang = [["text", "Plain Text"], ["apache", "Apache"], ["bash", "Bash"], ["coffeescript", "CoffeeScript"], ["cpp", "C++"], ["cs", "C#"], ["css", "CSS"], ["diff", "Diff"], ["http", "HTTP"], ["ini", "Ini"], ["java", "Java"], ["javascript", "JavaScript"], ["json", "JSON"], ["makefile", "Makefile"], ["markdown", "Markdown"], ["nginx", "Nginx"], ["objectivec", "Objective C"], ["perl", "Perl"], ["php", "PHP"], ["python", "Python"], ["ruby", "Ruby"], ["sql", "SQL"], ["xml", "HTML, XML"]];
var ExpireTimes = [[3600, "1 Hour"], [86400, "1 Day"], [604800, "1 Week"], [2419200, "4 Weeks"], [15778463, "6 Months"], [31556926, "1 Year"]];
var fn = {
sha: function (s) { return this.b64(CryptoJS.SHA256(s)); },
rmd: function (s) { return this.b64(CryptoJS.RIPEMD160(s)); },
chk: function (s) { return this.b64(CryptoJS.RIPEMD160(CryptoJS.SHA256(s))); },
enc: function (t, p) { return CryptoJS.AES.encrypt(t, p).toString(); },
dec: function (c, p) { return CryptoJS.AES.decrypt(c, p); },
b64: function (s) {
if (s == undefined) return;
return CryptoJS.enc.Base64.stringify(s).replace(/[=]+/, '').replace(/\//g, '_').replace(/\+/g, '-');
},
d64: function (s) {
if (s == undefined) return;
switch (s.length % 3) {
case 2:
s += '=';
// fallthrough
case 1:
s += '=';
}
return CryptoJS.enc.Base64.parse(s.replace(/_/g, '/').replace(/-/g, '+'));
},
rng: function (n) { return this.b64(CryptoJS.lib.WordArray.random(n)); },
seed: Math.seedrandom,
u8a: function (wa) {
var w = wa.words;
var b = new Uint8Array(w.length * 4), v, i, j, k = 0;
for (i = 0; i < w.length; ++i) {
v = w[i];
for (j = 3; j >= 0; --j) {
b[k++] = ((v >> 8 * j) & 0xFF);
}
}
return b;
},
u16a: function (ua) {
var s = '';
for (var i = 0; i < ua.length; i++) {
s += ('0' + ua[i].toString(16)).slice(-2);
}
return CryptoJS.enc.Hex.parse(s);
},
str8: function (ua) {
var s = '';
for (var i = 0; i < ua.byteLength; i++) {
if ((ua[i]&0x80) == 0) s += String.fromCharCode(ua[i]);
else if ((ua[i]&0xe0) == 0xc0 && (ua[i+1]&0xc0) == 0x80) {
s += String.fromCharCode(((ua[i]&0x1f)<<6) + (ua[i+1]&0x3f));
i += 1;
}
else if ((ua[i]&0xf0) == 0xe0 && (ua[i+1]&0xc0) == 0x80 && (ua[i+2]&0xc0) == 0x80){
s += String.fromCharCode(((ua[i]&0x0f)<<12) + ((ua[i+1]&0x3f)<<6) + (ua[i+2]&0x3f));
i += 2;
}
else if ((ua[i]&0xf8) == 0xf0 && (ua[i+1]&0xc0) == 0x80 && (ua[i+2]&0xc0) == 0x80 && (ua[i+3]&0xc0) == 0x80) {
s += String.fromCharCode(((ua[i]&0x0f)<<18) + ((ua[i+1]&0x3f)<<12) + ((ua[i+2]&0x3f)<<6) + (ua[i+3]&0x3f));
i += 3;
}
else { s += String.fromCharCode(65533); }
}
return s;
},
obj: function (a) {
var o = [];
for (var i = 0; i < a.length; i++) {
o.push({key: a[i][0], val: a[i][1]});
}
return o;
}
};
var decrypt = function (key, tgt) {
return function (d) {
if (d.length === 0) tgt({err: "Not Found", code: 'not_found'});
var s = d.split('\n');
d = {};
var i = 0;
while (true) {
if (s[i] == "") break;
var l = s[i].trim().split(':\t');
d[l[0]] = l[1];
i++;
}
d.tx = s.splice(i).join('');
if (key === undefined) tgt({err: "Missing Key", code: 'no_key'});
else if (d.chk != fn.rmd(fn.d64(key))) tgt({err: "Invalid Key", code: "bad_key"});
else {
var tx;
if (d.zip) {
tx = fn.dec(d.tx, key);
tx = fn.u8a(tx);
tx = fn.str8(pako.inflate(tx));
} else {
tx = fn.dec(d.tx, key).toString(CryptoJS.enc.Utf8);
}
var lang = 'text';
for (i = 0; i < HighliteLang.length; i++)
if (d.lang == HighliteLang[i][0])
lang = HighliteLang[i][0];
tgt({code: 'ok', tx: tx, lang: lang, exp: d.exp, zip: d.zip});
}
};
};
var encrypt = function (o) {
var ts = Date.now();
console.info("Begin Encryption Process...");
var pass;
if (o.pass !== undefined) pass = fn.chk(o.pass);
var r = fn.rng(40);
var key = fn.sha(r);
var chk = fn.chk(r);
var text = o.text;
if (o.zip) {
console.info("Compressing text...");
var bl = o.text.length, bs = Date.now();
var deflate = pako.gzip(text);
text = fn.u16a(deflate);
console.info("Compress complete: " + (Date.now() - bs) + "ms, " + bl + " => " + o.text.length + " bytes");
}
var enc = fn.enc(text, key);
var exp = (o.exp === undefined ? undefined : (o.exp + Date.now() / 1000 | 0));
var header = {
'pass': pass,
'chk': chk,
'lang': o.lang,
'exp': exp,
'zip': o.zip,
'burn': o.burn
};
var s = '';
for (var i in header) if (header.hasOwnProperty(i)) if (header[i] !== undefined) {
s += i + ":\t" + header[i] + "\n";
}
s += "\n";
while (enc.length > 79) {
s += enc.slice(0, 79) + "\n";
enc = enc.slice(79);
}
s += enc + "\n";
console.info("Encrypt complete: " + (Date.now() - ts) + " ms");
return {'txt': s, 'key': key};
};
m.filter('blength', function () {
return function (o) {
if (!(typeof o == 'string' || o instanceof String)) return;
if (o === undefined || o.length === undefined) return;
var utf8length = 0;
for (var n = 0; n < o.length; n++) {
var c = o.charCodeAt(n);
if (c < 128) {
utf8length++;
}
else if ((c > 127) && (c < 2048)) {
utf8length = utf8length + 2;
} else {
utf8length = utf8length + 3;
}
}
return utf8length;
};
});

56
assets/create.html Normal file
View File

@ -0,0 +1,56 @@
<div class=row ng-show='result != undefined'>
<div class=col-xs-12>
<div class="input-group">
<span class="input-group-btn">
<a class="btn btn-default" ng-click='reload()' type="button">New</a>
</span>
<input type=text readonly class=form-control select-on-click value="{{$base_url}}#/{{result.id}}!{{result.key}}">
<span class="input-group-btn">
<a class='btn btn-default' ng-href='#/{{result.id}}!{{result.key}}'>Open</a>
</span>
</div>
</div>
<pre class=col-xs-12> # Command Line: curl -s {{$base_url}}api/get/{{result.id}} | sed "1,/^\$/d" | openssl aes-256-cbc -d -a -k {{result.key}} <span ng-if='o.zip == true''>| gzip -dc</span>
{{result.text}}</pre>
</div>
<div ng-hide='result != undefined'>
<form name=paste ng-submit='Encrypt(o)'>
<div class="form form-inline">
<ol class='breadcrumb'>
<li>
<label>Syntax</label>
<select class='form-control input-sm' ng-model=o.lang ng-options='i.key as i.val for i in HighliteLang | orderBy:"+val"' ng-init='o.lang = "text"'></select>
</li>
<li>
<label>Expires</label>
<select class='form-control input-sm' ng-model=o.exp ng-options='i.key as i.val for i in ExpireTimes | orderBy:"+key"' ng-init='o.exp = 604800'></select>
</li>
<li>
<label><input type=checkbox ng-model='o.burn' /> Burn on Read</label>
</li>
</ol>
</div>
<textarea style='font-family: hack,"Anonymous Pro",consolita,monospace' required class='form-control' rows=20 ng-model="o.text"></textarea>
<pre>Additional Entropy: {{entropy}} bytes / Content size: {{o.text|blength|default:0}} bytes</pre>
<button type=submit class='btn btn-default btn-lg btn-block' ng-disabled="o.text == undefined || o.text.length == 0">Encrypt</button>
</form>
<p>Create pastes from the command line! <a href=./paste.sh>paste.sh</a>
<pre>
$ echo /etc/passwd | ./paste.sh
env options:
PASTE_URL - Set the url base for paste operations (default: HTTPS://sour.is/paste)
PASTE_GZIP - 0 = No Compression, 1 = Use gzip compression (default: 0)
PASTE_BURN - 0 = No Burn on Read, 1 = Burn on read (default: 0)
PASTE_DATE - Value to be used when setting expire date. (default: next-week)
</pre>
</p>
</div>

57
assets/index.html Normal file
View File

@ -0,0 +1,57 @@
<?doctype html?>
<html ng-app='souris-app'>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PasteBox</title>
</head>
<body>
<div id="wrapper">
<div class="container-responsive">
<article ng-view></article>
<a onclick='var elm = document.getElementById("debug"); elm.parentNode.style.display="block"; window.scrollTop = window.scrollHeight;' style="margin:3px;cursor:context-menu;font-family:monospace;position:fixed;bottom:0;right:0">&pi;</a>
<div class='panel panel-default' style='height:13em;margin-bottom:0;margin-top:2em;;display:none;position:relative;bottom:0'><b>Debug Log</b>
<div style='float:right'>
<a class="btn" onclick='document.getElementById("debug").parentNode.style.display="none";'><i class='glyphicon glyphicon-remove'></i></a><br/>
<a class="btn" onclick='var elm=document.getElementById("debug");while (elm.firstChild) {elm.removeChild(elm.firstChild);}'><i class='glyphicon glyphicon-ban-circle'></i></a>
</div>
<pre id=debug style='height:12em; overflow:x-scroll;'></pre>
<footer></footer></div>
</div>
</div>
<link rel="stylesheet" href="style.css" integrity='sha384-2g6CT1TMuzCclIAqYC+AkSkfA21njEedIBVs+k3tcZ6gHhCU7s17aGJMLvYsF0fK'>
<script src='app.js'></script>
<noscript>
<div class=container-responsive>
<h1>PasteBox</h1>
<p>It looks like yo don't have javascript enabled for this site. But thats ok. You can still submit and read the content of pastes by using a few curl/openssl/gunzip commands.</p>
<h2>Get the paste</h2>
<p>Lets say you have the following link <code>https://domain.tld/#/FeLq42kIQV69hQCJA8m9lg!5EDDziaCjceHjeG5UQ9M7-6wgyq5YVfysAEZ0wUNy6w</code>. Query the REST endpoint for the ID or part before the ! in the url hash.</p>
<pre><code>$ curl -i https://domain.tld/api/FeLq42kIQV69hQCJA8m9lg</code></pre>
<h2>Decrypt</h2>
<p>Using Openssl you want to remove the header and pass the remaining base64 for decryption. The cypher used is aes-256-cbc. The key is the portion after the ! in the link.</p>
<pre><code>... | sed '1,/^$/d' | openssl aes-256-cbc -d -a -k 5EDDziaCjceHjeG5UQ9M7-6wgyq5YVfysAEZ0wUNy6w</code></pre>
<h2>Deflate</h2>
<p>If as in the provided example the paste has been compressed pass it through gunzip. The header will have "zip: true" if it has been compressed.</p>
<pre><code> ... | gzip -dc </code></pre>
<h2>Example Output</h2>
<pre><code>$ curl -s "https://domain.tld/api/FeLq42kIQV69hQCJA8m9lg" | sed "1,/^$/d" | openssl aes-256-cbc -d -a -k 5EDDziaCjceHjeG5UQ9M7-6wgyq5YVfysAEZ0wUNy6w | gzip -dc
. ____ .-.
.-"` `",( __\_
.-==:;-._ .' .-. `'.
.' `"-:'-/ ( \} -=a .)
/ \/ \,== `- __..-'`
'-' | | | .'\ `;
\ _/---'\ ( `"`
/.`._ ) \ `; Sour.is Paste
\`-/.' `"`
`"\`-.</code></pre>
</div>
</noscript>
</body>
</html>

458
assets/lib.js Normal file

File diff suppressed because one or more lines are too long

45
assets/paste.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
if [ "$1" = "-h" ]; then
cat 1>&2 <<EOL
usage: echo /etc/passwd | ./paste.sh
env options:
PASTE_URL - Set the url base for paste operations (default: HTTPS://sour.is/paste)
PASTE_GZIP - 0 = No Compression, 1 = Use gzip compression (default: 0)
PASTE_BURN - 0 = No Burn on Read, 1 = Burn on read (default: 0)
PASTE_DATE - Value to be used when setting expire date. (default: next-week)
EOL
exit
fi
PASTE_URL=${PASTE_URL-"https://sour.is/paste"}
PASTE_BURN=${PASTE_BURN-0}
PASTE_DATE=${PASTE_DATE-"next-week"}
PASTE_GZIP=${PASTE_GZIP-0}
GZBIN="cat"
[ "$PASTE_GZIP" -eq "1" ] && GZBIN="gzip -c"
PASS=$(head -c 40 /dev/urandom);
CHK=$(echo -s $PASS | openssl dgst -sha256 -binary | openssl dgst -ripemd160 -binary | base64 | tr '/+' '_-' | tr -d '=')
PASS=$(echo -s $PASS | openssl dgst -sha256 -binary | base64 | tr '/+' '_-' | tr -d '=')
HASH=$((echo -e "exp:\t$(date +%s -d ${PASTE_DATE})"; \
echo -e "chk:\t$CHK"; \
[ "$PASTE_BURN" -eq "1" ] && echo -e "burn:\ttrue"; \
[ "$PASTE_GZIP" -eq "1" ] && echo -e "zip:\ttrue"; \
echo; \
cat /dev/stdin | $GZBIN | openssl aes-256-cbc -e -a -k $PASS) | \
curl -s -X POST ${PASTE_URL}/api/ --data-binary @-)
HASH_OK=$(echo $HASH | cut -c1-2)
if [ "$HASH_OK" = "OK" ]; then
HASH=$(echo $HASH | cut -f2 -d' ')
echo "url: ${PASTE_URL}/#/${HASH}!${PASS}"
echo -n "shell: curl -s ${PASTE_URL}/api/get/${HASH} | sed '1,/^\$/d' | openssl aes-256-cbc -d -a -k ${PASS}"
[ "$PASTE_GZIP" -eq "1" ] && echo " | gzip -dc" || echo;
exit
fi
echo $HASH

12
assets/style.css Normal file

File diff suppressed because one or more lines are too long

1
assets/test.txt Normal file
View File

@ -0,0 +1 @@
Test.

21
assets/view.html Normal file
View File

@ -0,0 +1,21 @@
<div class=row>
<div class=col-xs-12>
<div class="input-group">
<span class="input-group-btn">
<a class="btn btn-default" ng-href='#/' type="button">New</a>
</span>
<input type=text readonly class=form-control select-on-click value="{{$base_url}}#/{{id}}!{{key}}">
<span class="input-group-btn">
<a class='btn btn-default' ng-click='copy(store.tx)'>Copy</a>
</span>
</div>
</div>
</div>
<div ng-if="store.err == undefined">
<div class='well well-sm'>
<b>Lang:</b> {{store.lang}}, <b>Expires:</b> <span ng-if='store.exp != "burn_on_read"'>{{store.exp*1000|date}}</span><span ng-if='store.exp == "burn_on_read"'>Burn on Read</span>
</div>
<div hljs language="{{store.lang}}" source="store.tx"></div>
<pre class=col-xs-12> # Command Line: curl -s {{$base_url}}api/get/{{id}} | sed "1,/^\$/d" | openssl aes-256-cbc -d -a -k {{key}} <span ng-if='store.zip != undefined'>| gzip -dc</span>
</div>
<div ng-if="store.err != undefined"><h3>Error: {{store.err}}</h3></div>

77
config.go Normal file
View File

@ -0,0 +1,77 @@
package main
import (
"github.com/spf13/viper"
"github.com/docopt/docopt.go"
"log"
"io/ioutil"
"sour.is/x/httpsrv"
"bytes"
"fmt"
)
var APP_NAME string = "Paste API"
var APP_USAGE string = `Paste API
Usage:
mercury [-v] serve [--listen=<ListenAddress>]
Options:
-v Log to console.
-l <ListenAddress>, --listen=<ListenAddress> Address to listen on.
-c <ConfigFile>, --conf=<ConfigFile> Config file name. [Default: config.toml]
Config:
The config file is read from the following locations:
- /etc/paste/
- ~/.local/paste/
- Working Directory
`
var args map[string]interface{}
func init() {
var err error
if args, err = docopt.Parse(APP_USAGE, nil, true, APP_NAME, false); err != nil {
log.Fatal(err)
}
if args["-v"] == false {
log.SetOutput(ioutil.Discard)
}
viper.SetConfigName("config")
viper.AddConfigPath("/etc/reg42/")
viper.AddConfigPath("$HOME/.local/reg42/")
viper.AddConfigPath(".")
viper.SetConfigType("toml")
viper.ReadConfig(bytes.NewBuffer(defaultConfig))
err = viper.MergeInConfig()
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
log.Print("Read config from: ", viper.ConfigFileUsed())
if args["serve"] == true {
if args["--listen"] != nil {
viper.Set("listen", args["--listen"].(string))
}
httpsrv.Config()
}
}
var defaultConfig []byte = []byte(`
listen = ":9010"
fileserver = "/public/:/"
[module.paste]
random = "4096"
store = "data/"
`)

11
config.toml Normal file
View File

@ -0,0 +1,11 @@
listen = ":9010"
identity = "mock"
fileserver = "/:public/"
[idm.mock]
identity = "anon"
display_name = "Default User"
[module.paste]
random = "4096"
store = "data/"

2
data/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.git*

39
main.go Normal file
View File

@ -0,0 +1,39 @@
// Package Paste API.
//
// the purpose of this application is to provide an application
// that is using plain go code to define an API
//
// Terms Of Service:
//
// there are no TOS at this moment, use at your own risk we take no responsibility
//
// Schemes: http, https
// Host: paste.sour.is
// BasePath: /
// Version: 0.0.1
// License: MIT http://opensource.org/licenses/MIT
// Contact: Xuu <me@sour.is> https://sour.is
//
// Consumes:
// - application/json
// - text/plain
//
// Produces:
// - application/json
// - text/plain
//
//
// swagger:meta
package main
import (
"sour.is/x/httpsrv"
//_ "sour.is/x/httpsrv/routes"
_ "sour.is/x/paste/routes"
)
func main() {
if args["serve"] == true {
httpsrv.Run()
}
}

33
model/nulltime.go Normal file
View File

@ -0,0 +1,33 @@
package model
import (
"time"
"database/sql/driver"
"log"
)
type NullTime struct {
Time time.Time
Valid bool // Valid is true if Time is not NULL
}
// Scan implements the Scanner interface.
func (nt *NullTime) Scan(value interface{}) error {
nt.Time, nt.Valid = value.(time.Time)
return nil
}
// Value implements the driver Valuer interface.
func (nt NullTime) Value() (driver.Value, error) {
if !nt.Valid {
return nil, nil
}
return nt.Time, nil
}
func checkErr(err error) {
if err != nil {
log.Print(err)
panic(err)
}
}

136
model/paste.go Normal file
View File

@ -0,0 +1,136 @@
package model
import "time"
import (
"sour.is/x/dbm"
"database/sql"
"bytes"
"fmt"
"log"
"crypto/sha256"
"encoding/base64"
)
type Paste struct{
Id string `json:"paste_id"`
Text string `json:"paste_text"`
Chk string `json:"paste_chk"`
Lang string `json:"paste_lang"`
Zip bool `json:"paste_zip"`
Burn bool `json:"paste_burn"`
CreatedOn time.Time `json:"created_on"`
CreatedBy string `json:"created_by"`
ExpiresOn *time.Time `json:"expires_on"`
Deleted bool `json:"deleted"`
}
/*
func GetPaste(id string) (p Paste, err error) {
db := dbm.GetDB()
stmt, _ := db.Prepare(`
select paste_id, paste_text, created_on, created_by, paste_lang, expires_on, paste_zip, paste_burn, paste_chk
from paste
where paste_id = ?`)
var lang sql.NullString
var zip sql.NullBool
var burn sql.NullBool
var expiresOn NullTime
err = stmt.QueryRow(id).Scan(&p.Id, &p.Text, &p.CreatedOn, &p.CreatedBy, &lang, &expiresOn, &zip, &burn, &p.Chk);
if err != nil {
log.Print(err)
return p, err
}
if lang.Valid {
p.Lang = lang.String
}
if zip.Valid {
p.Zip = zip.Bool
}
if burn.Valid {
p.Burn = burn.Bool
}
if expiresOn.Valid {
p.ExpiresOn = &expiresOn.Time
}
return
}
func CreatePaste(p Paste) (Paste, error) {
db := dbm.GetDB()
s256 := sha256.Sum256([]byte(p.String()))
p.Id = base64.RawURLEncoding.EncodeToString(s256[12:])
count, err := db.Prepare(`
SELECT count(1) ok
FROM paste
WHERE paste_id=?`)
checkErr(err)
insert, err := db.Prepare(`
INSERT paste
SET paste_id = ?,
paste_text = ?,
paste_chk = ?,
created_by = ?,
paste_lang = ?,
paste_zip = ?,
paste_burn = ?,
expires_on = ?`)
checkErr(err)
update, err := db.Prepare(`
UPDATE paste
SET paste_text = ?,
paste_chk = ?,
created_by = ?,
paste_lang = ?,
paste_zip = ?,
paste_burn = ?,
expires_on = ?
WHERE paste_id = ?`)
checkErr(err)
var ok bool
err = count.QueryRow(p.Id).Scan(&ok)
checkErr(err)
if ok {
_, err = update.Exec(p.Text, p.Chk, p.CreatedBy, p.Lang, p.Zip, p.Burn, p.ExpiresOn, p.Id)
checkErr(err)
} else {
_, err = insert.Exec(p.Id, p.Text, p.Chk, p.CreatedBy, p.Lang, p.Zip, p.Burn, p.ExpiresOn)
checkErr(err)
}
np, err := GetPaste(p.Id)
checkErr(err)
return np, err
}
*/
func (p Paste) String() string {
var b bytes.Buffer
b.WriteString(fmt.Sprintf("; id:\t%s\n", p.Id))
b.WriteString(fmt.Sprintf("chk:\t%s\n", p.Chk))
b.WriteString(fmt.Sprintf("lang:\t%s\n", p.Lang))
if p.ExpiresOn != nil {
b.WriteString(fmt.Sprintf("exp:\t%d\n", p.ExpiresOn.Unix()))
}
if p.Zip {
b.WriteString(fmt.Sprintf("zip:\t%t\n", p.Zip))
}
if p.Burn {
b.WriteString(fmt.Sprintf("burn:\t%t\n", p.Burn))
}
b.WriteString("\n")
b.WriteString(p.Text)
return b.String()
}

1871
public/app.js Normal file

File diff suppressed because one or more lines are too long

21
public/index.html Normal file
View File

@ -0,0 +1,21 @@
<?doctype html?><html ng-app=souris-app><meta charset=utf-8><meta content="IE=edge"http-equiv=X-UA-Compatible><meta content="width=device-width,initial-scale=1"name=viewport><title>PasteBox</title><script id=create.html type=text/ng-template><div class=row ng-show="result != undefined"><div class=col-xs-12><div class=input-group><span class=input-group-btn><a class="btn btn-default"ng-click=reload() type=button>New</a> </span><input class=form-control readonly select-on-click value={{$base_url}}#/{{result.id}}!{{result.key}}> <span class=input-group-btn><a class="btn btn-default"ng-href=#/{{result.id}}!{{result.key}}>Open</a></span></div></div><pre class=col-xs-12> # Command Line: curl -s {{$base_url}}api/get/{{result.id}} | sed "1,/^\$/d" | openssl aes-256-cbc -d -a -k {{result.key}} <span ng-if="o.zip == true">| gzip -dc</span>
{{result.text}}</pre></div><div ng-hide="result != undefined"><form name=paste ng-submit=Encrypt(o)><div class="form form-inline"><ol class=breadcrumb><li><label>Syntax</label><select class="form-control input-sm"ng-init='o.lang = "text"'ng-model=o.lang ng-options='i.key as i.val for i in HighliteLang | orderBy:"+val"'></select><li><label>Expires</label><select class="form-control input-sm"ng-init="o.exp = 604800"ng-model=o.exp ng-options='i.key as i.val for i in ExpireTimes | orderBy:"+key"'></select><li><label><input ng-model=o.burn type=checkbox> Burn on Read</label></ol></div><textarea class=form-control ng-model=o.text required rows=20 style='font-family:hack,"Anonymous Pro",consolita,monospace'></textarea><pre>Additional Entropy: {{entropy}} bytes / Content size: {{o.text|blength|default:0}} bytes</pre><button class="btn btn-default btn-block btn-lg"ng-disabled="o.text == undefined || o.text.length == 0"type=submit>Encrypt</button></form><p>Create pastes from the command line! <a href=./paste.sh>paste.sh</a><pre>
$ echo /etc/passwd | ./paste.sh
env options:
PASTE_URL - Set the url base for paste operations (default: HTTPS://sour.is/paste)
PASTE_GZIP - 0 = No Compression, 1 = Use gzip compression (default: 0)
PASTE_BURN - 0 = No Burn on Read, 1 = Burn on read (default: 0)
PASTE_DATE - Value to be used when setting expire date. (default: next-week)
</pre></div></script><script id=view.html type=text/ng-template><div class=row><div class=col-xs-12><div class=input-group><span class=input-group-btn><a class="btn btn-default" ng-click='new()' type=button>New</a> </span><input class=form-control readonly select-on-click value={{$base_url}}#/{{id}}!{{key}}><span class="input-group-btn"><a class='btn btn-default' ng-click='copy(store.tx)'>Copy</a></span></div></div></div><div ng-if="store.err == undefined"><div class="well well-sm"><b>Lang:</b> {{store.lang}}, <b>Expires:</b> <span ng-if='store.exp != "burn_on_read"'>{{store.exp*1000|date}}</span><span ng-if='store.exp == "burn_on_read"'>Burn on Read</span></div><div hljs language={{store.lang}} source=store.tx></div><pre class=col-xs-12> # Command Line: curl -s {{$base_url}}api/get/{{id}} | sed "1,/^\$/d" | openssl aes-256-cbc -d -a -k {{key}} <span ng-if="store.zip != undefined">| gzip -dc</span></div><div ng-if="store.err != undefined"><h3>Error: {{store.err}}</h3></div></script><div id=wrapper><div class=container-responsive><article ng-view></article><a onclick='var e=document.getElementById("debug");e.parentNode.style.display="block",window.scrollTop=window.scrollHeight'style=margin:3px;cursor:context-menu;font-family:monospace;position:fixed;bottom:0;right:0>π</a><div class="panel panel-default"style=height:13em;margin-bottom:0;margin-top:2em;display:none;position:relative;bottom:0><b>Debug Log</b><div style=float:right><a onclick='document.getElementById("debug").parentNode.style.display="none"'class=btn><i class="glyphicon glyphicon-remove"></i></a><br><a onclick='for(var e=document.getElementById("debug");e.firstChild;)e.removeChild(e.firstChild)'class=btn><i class="glyphicon glyphicon-ban-circle"></i></a></div><pre id=debug style=height:12em;overflow:x-scroll></pre><footer></footer></div></div></div><link href=style.css integrity=sha384-2g6CT1TMuzCclIAqYC+AkSkfA21njEedIBVs+k3tcZ6gHhCU7s17aGJMLvYsF0fK rel=stylesheet><script src=app.js></script><noscript><div class=container-responsive><h1>PasteBox</h1><p>It looks like yo don't have javascript enabled for this site. But thats ok. You can still submit and read the content of pastes by using a few curl/openssl/gunzip commands.<h2>Get the paste</h2><p>Lets say you have the following link <code>https://domain.tld/#/FeLq42kIQV69hQCJA8m9lg!5EDDziaCjceHjeG5UQ9M7-6wgyq5YVfysAEZ0wUNy6w</code>. Query the REST endpoint for the ID or part before the ! in the url hash.<pre><code>$ curl -i https://domain.tld/api/FeLq42kIQV69hQCJA8m9lg</code></pre><h2>Decrypt</h2><p>Using Openssl you want to remove the header and pass the remaining base64 for decryption. The cypher used is aes-256-cbc. The key is the portion after the ! in the link.<pre><code>... | sed '1,/^$/d' | openssl aes-256-cbc -d -a -k 5EDDziaCjceHjeG5UQ9M7-6wgyq5YVfysAEZ0wUNy6w</code></pre><h2>Deflate</h2><p>If as in the provided example the paste has been compressed pass it through gunzip. The header will have "zip: true" if it has been compressed.<pre><code> ... | gzip -dc </code></pre><h2>Example Output</h2><pre><code>$ curl -s "https://domain.tld/api/FeLq42kIQV69hQCJA8m9lg" | sed "1,/^$/d" | openssl aes-256-cbc -d -a -k 5EDDziaCjceHjeG5UQ9M7-6wgyq5YVfysAEZ0wUNy6w | gzip -dc
. ____ .-.
.-"` `",( __\_
.-==:;-._ .' .-. `'.
.' `"-:'-/ ( \} -=a .)
/ \/ \,== `- __..-'`
'-' | | | .'\ `;
\ _/---'\ ( `"`
/.`._ ) \ `; Sour.is Paste
\`-/.' `"`
`"\`-.</code></pre></div></noscript>

45
public/paste.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
if [ "$1" = "-h" ]; then
cat 1>&2 <<EOL
usage: echo /etc/passwd | ./paste.sh
env options:
PASTE_URL - Set the url base for paste operations (default: HTTPS://sour.is/paste)
PASTE_GZIP - 0 = No Compression, 1 = Use gzip compression (default: 0)
PASTE_BURN - 0 = No Burn on Read, 1 = Burn on read (default: 0)
PASTE_DATE - Value to be used when setting expire date. (default: next-week)
EOL
exit
fi
PASTE_URL=${PASTE_URL-"https://sour.is/paste"}
PASTE_BURN=${PASTE_BURN-0}
PASTE_DATE=${PASTE_DATE-"next-week"}
PASTE_GZIP=${PASTE_GZIP-0}
GZBIN="cat"
[ "$PASTE_GZIP" -eq "1" ] && GZBIN="gzip -c"
PASS=$(head -c 40 /dev/urandom);
CHK=$(echo -s $PASS | openssl dgst -sha256 -binary | openssl dgst -ripemd160 -binary | base64 | tr '/+' '_-' | tr -d '=')
PASS=$(echo -s $PASS | openssl dgst -sha256 -binary | base64 | tr '/+' '_-' | tr -d '=')
HASH=$((echo -e "exp:\t$(date +%s -d ${PASTE_DATE})"; \
echo -e "chk:\t$CHK"; \
[ "$PASTE_BURN" -eq "1" ] && echo -e "burn:\ttrue"; \
[ "$PASTE_GZIP" -eq "1" ] && echo -e "zip:\ttrue"; \
echo; \
cat /dev/stdin | $GZBIN | openssl aes-256-cbc -e -a -k $PASS) | \
curl -s -X POST ${PASTE_URL}/api/ --data-binary @-)
HASH_OK=$(echo $HASH | cut -c1-2)
if [ "$HASH_OK" = "OK" ]; then
HASH=$(echo $HASH | cut -f2 -d' ')
echo "url: ${PASTE_URL}/#/${HASH}!${PASS}"
echo -n "shell: curl -s ${PASTE_URL}/api/get/${HASH} | sed '1,/^\$/d' | openssl aes-256-cbc -d -a -k ${PASS}"
[ "$PASTE_GZIP" -eq "1" ] && echo " | gzip -dc" || echo;
exit
fi
echo $HASH

12
public/style.css Normal file

File diff suppressed because one or more lines are too long

207
routes/paste.go Normal file
View File

@ -0,0 +1,207 @@
package routes
import (
"net/http"
"sour.is/x/httpsrv"
"sour.is/x/ident"
"encoding/json"
"github.com/gorilla/mux"
"io/ioutil"
"io"
"sour.is/x/log"
"os"
"golang.org/x/sys/unix"
"crypto/rand"
"strconv"
"sour.is/x/paste/model"
)
var store string
var randBytes int
func init() {
httpsrv.HttpRegister("paste", httpsrv.HttpRoutes{
{ "Paste", "GET", "/paste/rng", GetRandom, },
{ "Paste", "GET", "/paste/{id}", GetPaste, },
{ "Paste", "GET", "/paste/get/{id}", GetPaste, },
{ "Paste", "POST", "/paste", PostPaste, },
// { "Paste", "DELETE", "/paste/{id}", DeletePaste, },
{ "Paste", "GET", "/api/rng", GetRandom, },
{ "Paste", "GET", "/api/{id}", GetPaste, },
{ "Paste", "GET", "/api/get/{id}", GetPaste, },
{ "Paste", "POST", "/api", PostPaste, },
// { "Paste", "DELETE", "/api/{id}", DeletePaste, },
})
}
func SetConfig (config map[string]string) {
store = "data/"
if config["store"] != "" {
store = config["store"]
}
if !chkStore(store) {
log.Fatalf("[routes::Paste] Store location [%s] does not exist or is not writable.", store)
}
randBytes = 1024
if config["random"] != "" {
randBytes, _ = strconv.Atoi(config["random"])
}
}
func chkStore(path string) bool {
file, err := os.Stat(path)
if err == nil { return true }
if os.IsNotExist(err) { return false }
if !file.IsDir() { return false }
if unix.Access(path, unix.W_OK & unix.R_OK) != nil { return false }
return true
}
func chkFile(path string) bool {
file, err := os.Stat(path)
if err == nil { return true }
if os.IsNotExist(err) { return false }
if file.IsDir() { return false }
if unix.Access(path, unix.W_OK & unix.R_OK) != nil { return false }
return true
}
func chkGone(path string) bool {
file, err := os.Stat(path)
if err != nil { return true }
if file.Size() == 0 { return true }
return false
}
func GetRandom(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("content-type","application/octet-stream")
s := make([]byte, randBytes)
rand.Read(s)
w.Write(s)
}
// swagger:route GET /paste pasteIndex
//
// Welcomes user.
//
// This welcome user based on identity used.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http
//
// Security:
// user_token:
//
// Responses:
// default: genericError
// 200: someResponse
// 422: validationError
func GetPaste(w http.ResponseWriter, r *http.Request, i ident.Ident) {
vars := mux.Vars(r)
id := vars["id"]
p, err := model.GetPaste(id)
log.Printf("%#v",p)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Bad Request"))
return
}
if p.Id == "" {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Found"))
return
}
//switch accept {
//case "text/plain":
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(p.String()));
err != nil {
panic(err)
}
/*
case "application/json":
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(p);
err != nil {
panic(err)
}
default:
w.WriteHeader(http.StatusNotAcceptable)
w.Write([]byte(""))
}
*/
}
func PostPaste(w http.ResponseWriter, r *http.Request, i ident.Ident) {
var p model.Paste
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
checkErr(err, w)
err = r.Body.Close()
checkErr(err, w)
if err := json.Unmarshal(body, &p); err != nil {
w.WriteHeader(422) // unprocessable entity
err = json.NewEncoder(w).Encode(err)
checkErr(err, w)
}
p.CreatedBy = i.Identity()
np, err := model.CreatePaste(p)
checkErr(err, w)
// accept := negotiate.Negotiate("text/plain,application/json;q=0.7", r)
//switch accept {
//case "text/plain":
w.WriteHeader(http.StatusCreated)
if _, err := w.Write([]byte(np.String()));
err != nil {
panic(err)
}
/*
case "application/json":
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(np);
err != nil {
panic(err)
}
default:
w.WriteHeader(http.StatusNotAcceptable)
w.Write([]byte(""))
}
*/
}
func checkErr(err error, w http.ResponseWriter) {
if err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(err);
panic(err)
}
}