Pastebin

Check-in [d861ce585b]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Initial import.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d861ce585b805b332418160b4c7fce441e6ab92fd7afd8fc96b22b1d80cc8edb
User & Date: jef 2019-11-04 14:46:02
Context
2019-11-04
14:54
Use a reserved TLD for examples. check-in: 9fe314d5fa user: jef tags: trunk
14:46
Initial import. check-in: d861ce585b user: jef tags: trunk
07:01
initial empty check-in check-in: 25bf117397 user: cozs_jef tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added Makefile.











































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.POSIX:

PREFIX = /usr/local
MANPREFIX = $(PREFIX)/man
PROG = pastebin
GOFILES != ls *.go

all: $(PROG) doc

doc: $(PROG).1.txt

$(PROG).1.txt: $(PROG).1
	# generate text documentation from man pages
	# (section title is erased using a perl on liner)
	@mandoc -I os="" -T locale $(PROG).1 \
		| col -bx \
		| perl -pe 's/^(\S+\(\d\))(\s+.*\s+)\1$$/$$1 . " " x length($$2) . $$1/e' \
		> $(PROG).1.txt
	
pastebin: $(GOFILES)
	go build

install: $(PROG) $(PROG).1
	mkdir -p $(PREFIX)/bin
	cp -f $(PROG) $(PREFIX)/bin
	chmod 0755 $(PREFIX)/bin/$(PROG)
	mkdir -p $(MANPREFIX)/man1
	cp -f $(PROG).1 $(MANPREFIX)/man1
	gzip -f $(MANPREFIX)/man1/$(PROG).1
	chmod 0644 $(MANPREFIX)/man1/$(PROG).1.gz

uninstall:
	rm -f $(PREFIX)/bin/$(PROG)
	rm -f $(MANPREFIX)/man1/$(PROG).1.gz

clean:
	rm -f $(PROG)

Added README.md.



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# pastebin

Simplistic web pastebin

## Installation

Installation can be done using go get:

    $ go get foutaise.org/fossil/pastebin

or by cloning this repository using [fossil](https://www.fossil-scm.org/index.html/uv/download.html):

    $ fossil clone https://foutaise.org/fossil/pastebin pastebin.fossil
    $ mkdir pastebin
    $ cd pastebin
    $ fossil open ../pastebin.fossil
    $ make
    $ sudo make install

## Documentation

pastebin documentation is available in the [man page](/doc/tip/pastebin.1.txt).

## Command line client

A [pastebin client](/dir?name=client/) is provided to create and manage
pastes from the command line:

    $ pastebin-client -h
    Command line pastebin client
    Usage:
            pastebin-client < file
    
    Options:
            -h           : display this help and exit
            -l           : list avalable pastes
            -d <pasteid> : delete a specific paste
            -p <ndays>   : purge pastes older than <ndays>

The header of the file should be edited to specify the target
pastebin, and associated API key if any.

Added cfg.go.



















































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package main

import (
	"fmt"
	"io/ioutil"
	"net/url"
	"strconv"
	"strings"
	"unicode"
)

type PasteBinCfg struct {
	DbPath    string   // database sqlite3 file (required)
	PasteURL  *url.URL // cgi script base url (required)
	SizeLimit int64    // upload size limit (default -1)
	ApiKey    string   // key to access api calls (default "")
	HttpPort  int      // http listen port (default 8080)
}

type cfgParser func(string, *PasteBinCfg) (bool, error)

func cfgParsePrefix(line string, prefix string) (bool, string) {
	if strings.HasPrefix(line, prefix) {
		line = strings.TrimPrefix(line, prefix)
		line = strings.TrimSpace(line)
		return true, line
	}

	return false, line
}

func cfgParseDbPath(line string, cfg *PasteBinCfg) (parsed bool, err error) {
	parsed, line = cfgParsePrefix(line, "dbpath:")
	if !parsed {
		return false, nil
	}

	if line == "" {
		return true, fmt.Errorf("empty dbpath value")
	}
	cfg.DbPath = line
	return true, nil
}

func cfgParsePasteURL(line string, cfg *PasteBinCfg) (parsed bool, err error) {
	parsed, line = cfgParsePrefix(line, "pasteurl:")
	if !parsed {
		return false, nil
	}

	if line == "" {
		return true, fmt.Errorf("empty baseurl value")
	}

	url, err := url.Parse(line)
	if err != nil {
		return true, fmt.Errorf("invalid baseurl value")
	}
	cfg.PasteURL = url
	return true, nil
}

func cfgParseSizeLimit(line string, cfg *PasteBinCfg) (parsed bool, err error) {
	parsed, line = cfgParsePrefix(line, "sizelimit:")
	if !parsed {
		return false, nil
	}

	sizelimit, err := SizeParse(line)
	if err != nil {
		return true, fmt.Errorf("not a valid int value")
	}

	cfg.SizeLimit = sizelimit
	return true, nil
}

func cfgParseApiKey(line string, cfg *PasteBinCfg) (parsed bool, err error) {
	parsed, line = cfgParsePrefix(line, "apikey:")
	if !parsed {
		return false, nil
	}

	apikey := line
	if apikey == "" {
		return true, fmt.Errorf("cfg api key is empty")
	}

	for _, c := range apikey {
		if unicode.IsSpace(c) {
			return true, fmt.Errorf("cfg api key contains spaces")
		}
	}

	if len(apikey) < 10 {
		return true, fmt.Errorf("cfg api key too short (10 chars min)")
	}

	cfg.ApiKey = apikey
	return true, nil
}

func cfgParseHttpPort(line string, cfg *PasteBinCfg) (parsed bool, err error) {
	parsed, line = cfgParsePrefix(line, "httpport:")
	if !parsed {
		return false, nil
	}

	httpport, err := strconv.Atoi(line)
	if err != nil || httpport < 0 || httpport > 65535 {
		return true, fmt.Errorf("invalid cfg httpport value")
	}

	cfg.HttpPort = httpport
	return true, nil
}

func cfgParse(filename string) (cfg *PasteBinCfg, err error) {
	parsers := [...]cfgParser{
		cfgParseDbPath,
		cfgParsePasteURL,
		cfgParseSizeLimit,
		cfgParseApiKey,
		cfgParseHttpPort,
	}

	cfgData, err := ioutil.ReadFile(filename)
	if err != nil {
		return
	}

	cfg = &PasteBinCfg{
		DbPath:    "",
		PasteURL:  nil,
		SizeLimit: -1,
		ApiKey:    "",
		HttpPort:  8080,
	}

	for _, line := range strings.Split(string(cfgData), "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		parsed := false
		for _, p := range parsers {
			parsed, err = p(line, cfg)
			if err != nil {
				return
			}
			if parsed {
				break
			}
		}

		if !parsed {
			err = fmt.Errorf("invalid cfg line '%s'", line)
			return
		}
	}

	if cfg.PasteURL == nil || cfg.DbPath == "" {
		err = fmt.Errorf("missing required fields in cfg file")
		return
	}

	return
}

Added client/pastebin-client.





































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env bash
#
# Command line pastebin client

PASTEURL="<insert paste url here>"
APIKEY="<insert API key here"

function usage() {
	local progname=${0##*/}

	cat <<-EOF
	Command line pastebin client
	Usage:
	        $progname < file

	Options:
	        -h           : display this help and exit
	        -l           : list avalable pastes
	        -d <pasteid> : delete a specific paste
		-p <ndays>   : purge pastes older than <ndays>
	EOF
}

# parse command line options
while getopts "hld:p:" OPTION
do
	case $OPTION in
	h)
		usage
		exit 0
		;;
	l)
		curl -s -d apikey="$APIKEY" "$PASTEURL/paste/api/list"
		exit $?
		;;
	d)
		curl -s -d apikey="$APIKEY" "$PASTEURL/paste/api/del/$OPTARG"
		exit $?
		;;
	p)
		curl -s -d apikey="$APIKEY" "$PASTEURL/paste/api/purge/$OPTARG"
		exit $?
		;;
	*)
		exit 1
		;;
	esac
done

curl -F 'data=<-' $PASTEURL

Added db.go.

























































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package main

import (
	"database/sql"
	"fmt"
	_ "github.com/mattn/go-sqlite3"
	"net/url"
	"os"
	"path/filepath"
	"strings"
)

type PasteData struct {
	MimeType string
	Data     []byte
}

// Paste

type Paste struct {
	PasteId    string `json:"-"`
	URL        string `json:"url"`
	InsertTime string `json:"inserttime"`
	MimeType   string `json:"mimetype"`
}

// PasteBinDb

type PasteBinDb struct {
	Cfg *PasteBinCfg
	Db  *sql.DB
}

func (p *PasteBinDb) init() error {
	dbDir := filepath.Dir(p.Cfg.DbPath)
	err := os.MkdirAll(dbDir, 0700)
	if err != nil {
		return err
	}

	db, err := sql.Open("sqlite3", p.Cfg.DbPath)
	if err != nil {
		return err
	}
	defer db.Close()

	_, err = db.Exec(`CREATE TABLE pastes (
	pasteid TEXT UNIQUE NOT NULL,
	mimetype TEXT DEFAULT "text/plain; charset=UTF-8",
	data BLOB,
	inserttime TIMESTAMP default (datetime('now', 'localtime'))
);
CREATE INDEX idx_pasteid on pastes (pasteid);`)

	return err
}

func (p *PasteBinDb) Open() (err error) {
	if _, err = os.Stat(p.Cfg.DbPath); os.IsNotExist(err) {
		if err = p.init(); err != nil {
			return
		}
	}

	p.Db, err = sql.Open("sqlite3", p.Cfg.DbPath)
	if err != nil {
		return
	}

	_, err = p.Db.Query("PRAGMA busy_timeout = 5000;")
	return
}

func (p *PasteBinDb) Close() {
	p.Db.Close()
}

func (p *PasteBinDb) List(baseURL *url.URL) (pastes []*Paste, err error) {
	rows, err := p.Db.Query(`SELECT pasteid, mimetype, inserttime FROM pastes ORDER BY inserttime`)
	if err != nil {
		return
	}
	defer rows.Close()

	for rows.Next() {
		paste := Paste{}
		err := rows.Scan(&paste.PasteId, &paste.MimeType, &paste.InsertTime)
		if err != nil {
			return nil, err
		}
		paste.URL = fmt.Sprintf("%s/%s", strings.TrimRight(baseURL.String(), "/"), paste.PasteId)
		pastes = append(pastes, &paste)
	}

	return
}

func (p *PasteBinDb) Add(data string) (url string, mimetype string, err error) {
	stmt, err := p.Db.Prepare(`INSERT INTO pastes (pasteid, mimetype, data) VALUES (?, ?, ?)`)
	if err != nil {
		return
	}
	defer stmt.Close()

	mimetype = MimeString(data)
	for _ = range []int{1, 2, 3} {
		pasteid := PasteIdRandom()
		_, err = stmt.Exec(pasteid, mimetype, data)
		if err == nil {
			url = fmt.Sprintf("%s/%s", strings.TrimRight(p.Cfg.PasteURL.String(), "/"), pasteid)
			return
		}
	}

	return
}

func (p *PasteBinDb) Del(pasteId string) error {
	if !PasteIdIsValid(pasteId) {
		return fmt.Errorf("invalid pasteid")
	}

	stmt, err := p.Db.Prepare(`DELETE FROM pastes WHERE pasteid = ?`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	_, err = stmt.Exec(pasteId)
	return err
}

func (p *PasteBinDb) Purge(ndays int) error {
	if ndays < 0 {
		return nil
	}

	delay := fmt.Sprintf("-%d days", ndays)
	stmt, err := p.Db.Prepare(`DELETE FROM pastes WHERE inserttime < datetime('now', ?, 'localtime');`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	_, err = stmt.Exec(delay)
	return err
}

func (p *PasteBinDb) Get(pasteid string) (pdata *PasteData, err error) {
	stmt, err := p.Db.Prepare(`SELECT data, mimetype FROM pastes WHERE pasteid = ?`)
	if err != nil {
		return
	}
	defer stmt.Close()

	rows, err := stmt.Query(pasteid)
	if err != nil {
		return
	}

	if !rows.Next() {
		return nil, nil
	}

	pdata = &PasteData{}
	err = rows.Scan(&pdata.Data, &pdata.MimeType)
	if err != nil {
		return
	}

	return pdata, nil
}

Added go.mod.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
module pastebin

go 1.13

require (
	github.com/gabriel-vasile/mimetype v0.3.21
	github.com/mattn/go-sqlite3 v1.11.0
)

Added go.sum.









>
>
>
>
1
2
3
4
github.com/gabriel-vasile/mimetype v0.3.21 h1:Mc81ydjjIFN3Ir12WJ4myhnMs6cFAIlthU7MKY6XAIk=
github.com/gabriel-vasile/mimetype v0.3.21/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=

Added http.go.



















































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"net/http/cgi"
	"os"
	"regexp"
	"strconv"
	"strings"
)

// Error report

func pasteBinFatal(a ...interface{}) {
	fmt.Fprint(os.Stderr, "pastebin: ")
	fmt.Fprintln(os.Stderr, a...)
	os.Exit(1)
}

type httpHandler struct {
	cfg *PasteBinCfg
	db  *PasteBinDb
}

func HTTPHandler(args []string) {
	h := &httpHandler{
		cfg: nil,
		db:  nil,
	}

	if len(args) == 0 {
		pasteBinFatal("cfg file missing")
	}

	var err error
	h.cfg, err = cfgParse(args[0])
	if err != nil {
		pasteBinFatal(err)
	}

	if h.cfg.HttpPort == 0 {
		err := cgi.Serve(h)
		if err != nil {
			pasteBinFatal(err)
		}
	} else {
		log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", h.cfg.HttpPort), h))
	}
}

// Page handlers

func (h *httpHandler) pagePasteShow(w http.ResponseWriter, r *http.Request) {
	matches := regexp.MustCompile("/([0-9a-f]{10})$").FindStringSubmatch(r.URL.RequestURI())
	if len(matches) != 2 {
		http.Error(w, "Page not found", http.StatusNotFound)
		return
	}

	pdata, err := h.db.Get(matches[1])
	if err != nil {
		http.Error(w, fmt.Sprintf("error: %s", err), http.StatusForbidden)
		return
	}

	if pdata == nil {
		http.Error(w, "Page not found", http.StatusNotFound)
		return
	}

	w.Header().Add("Content-type", pdata.MimeType)
	w.Write(pdata.Data)
}

func (h *httpHandler) pageIndex(w http.ResponseWriter, r *http.Request) {

	data := r.PostFormValue("data")
	if data != "" {
		h.pasteAddData(w, r, data)
		return
	}

	file, handler, err := r.FormFile("data")
	if err == nil {
		h.pasteAddFile(w, r, file, handler)
		defer file.Close()
		return
	}

	w.Header().Add("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w,
		`<!doctype html>
<html lang="en">
  <head>
    <meta charset='utf-8'/>
    <title>Pastebin</title>
    <style>
      h1 {
        font-family: sans-serif;
        font-size: 120%%;
      }
      form {
        margin-bottom: 2em;
      }
    </style>
  </head>
  <body>
    <h1>New paste:</h1>
    <form method="POST">
      <textarea name="data" rows="25" cols="80"></textarea><br/>
      <input name="submit" value="Upload" type="submit">
    </form>
    <h1>Upload file:</h1>
    <form method="POST" enctype="multipart/form-data">
      <input type="file" name="data"/><br/>
      <input name="submit" value="Upload" type="submit">
    </form>
    <p>(Upload limited to %s bytes)</p>
  </body>
</html>`, SizeString(h.cfg.SizeLimit))
}

// pageIndex helper function: process pasted text
func (h *httpHandler) pasteAddData(w http.ResponseWriter, r *http.Request, data string) {

	if h.cfg.SizeLimit > 0 {
		if r.ContentLength > h.cfg.SizeLimit || int64(len(data)) > h.cfg.SizeLimit {
			http.Error(w, "error: paste too big", http.StatusForbidden)
			return
		}
	}

	h.pasteAddDb(w, r, data)
}

// pageIndex helper function: process pasted file
func (h *httpHandler) pasteAddFile(w http.ResponseWriter, r *http.Request,
	file multipart.File, header *multipart.FileHeader) {

	if h.cfg.SizeLimit > 0 && header.Size > h.cfg.SizeLimit {
		http.Error(w, "error: paste too big", http.StatusForbidden)
		return
	}

	data, err := ioutil.ReadAll(file)
	if err != nil {
		http.Error(w, "error: unable to read multipart data", http.StatusForbidden)
		return
	}

	h.pasteAddDb(w, r, string(data))
}

func (h *httpHandler) pasteAddDb(w http.ResponseWriter, r *http.Request, data string) {
	url, mimetype, err := h.db.Add(data)
	if err != nil {
		http.Error(w, fmt.Sprintf("error: %s", err), http.StatusForbidden)
		return
	}

	w.Header().Add("Content-type", "text/plain; charset=UTF-8")
	if strings.HasPrefix(mimetype, "text/") || strings.HasPrefix(mimetype, "image/") {
		w.Header().Add("Location", url)
		w.WriteHeader(http.StatusSeeOther)
	}
	fmt.Fprintln(w, url)
}

// API Handlers

func (h *httpHandler) apiCheckKey(handler http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if h.cfg.ApiKey == "" {
			http.Error(w, "error: api globally disabled", http.StatusForbidden)
			return
		}

		apikey := r.FormValue("apikey")
		if apikey == "" {
			http.Error(w, "error: no api key provided", http.StatusForbidden)
			return
		}

		if apikey != h.cfg.ApiKey {
			http.Error(w, "error: invalid api key", http.StatusForbidden)
			return
		}

		handler(w, r)
	}
}

func (h *httpHandler) pageApiList(w http.ResponseWriter, r *http.Request) {
	pastes, err := h.db.List(h.cfg.PasteURL)
	if err != nil {
		http.Error(w, fmt.Sprintf("error: %s"), http.StatusForbidden)
		return
	}

	jsonBytes, err := json.MarshalIndent(pastes, "", "    ")
	if err != nil {
		http.Error(w, "error: unable to generate json", http.StatusForbidden)
		return
	}

	w.Header().Add("Content-type", "application/json")
	if len(pastes) == 0 {
		return
	}

	w.Write(jsonBytes)
}

func (h *httpHandler) pageApiDel(w http.ResponseWriter, r *http.Request) {
	matches := regexp.MustCompile("/api/del/([0-9a-f]{10})$").FindStringSubmatch(r.URL.RequestURI())
	if len(matches) != 2 {
		http.Error(w, "Page not found", http.StatusNotFound)
		return
	}

	err := h.db.Del(matches[1])
	if err != nil {
		http.Error(w, fmt.Sprintf("error: %s", err), http.StatusForbidden)
		return
	}
}

func (h *httpHandler) pageApiPurge(w http.ResponseWriter, r *http.Request) {
	matches := regexp.MustCompile("/api/purge/([0-9]+)$").FindStringSubmatch(r.URL.RequestURI())
	if len(matches) != 2 {
		http.Error(w, "Page not found", http.StatusNotFound)
		return
	}

	ndays, err := strconv.Atoi(matches[1])
	if err != nil {
		http.Error(w, "error: invalid purge argument", http.StatusForbidden)
		return
	}

	err = h.db.Purge(ndays)
	if err != nil {
		http.Error(w, fmt.Sprintf("error: %s", err), http.StatusForbidden)
		return
	}
}

func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	var err error

	h.db = &PasteBinDb{Cfg: h.cfg}
	err = h.db.Open()
	if err != nil {
		http.Error(w, fmt.Sprintf("error: %s", err), http.StatusForbidden)
		return
	}
	defer h.db.Close()

	// search a valid handler by pattern matching the URI
	for _, d := range []struct {
		pattern string
		handler http.HandlerFunc
	}{
		{"/api/del/[0-9a-f]{10}$", h.apiCheckKey(h.pageApiDel)},
		{"/api/purge/[0-9]+$", h.apiCheckKey(h.pageApiPurge)},
		{"/api/list$", h.apiCheckKey(h.pageApiList)},
		{"/[0-9a-f]{10}$", h.pagePasteShow},
	} {
		if regexp.MustCompile(d.pattern).MatchString(r.URL.RequestURI()) {
			d.handler(w, r)
			return
		}
	}

	h.pageIndex(w, r)
}

Added main.go.







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"flag"
	"fmt"
	"os"
)

func main() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Simplistic web pastebin\n")
		fmt.Fprintf(os.Stderr, "Usage: %s <cfgfile>\n", os.Args[0])
		flag.PrintDefaults()
	}
	flag.Parse()

	args := flag.Args()
	HTTPHandler(args)
}

Added mime.go.





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
	"github.com/gabriel-vasile/mimetype"
	"strings"
)

func MimeString(data string) (mimeType string) {
	mimeType, _, _ = mimetype.DetectReader(strings.NewReader(data))
	if strings.HasPrefix(mimeType, "text/") {
		mimeType += "; charset=UTF-8"
	}
	return
}

Added pastebin.1.



















































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
.Dd November 2019
.Dt PASTEBIN 1
.Os
.Sh NAME
.Nm pastebin
.Nd Simplistic web pastebin
.Sh SYNOPSIS
.Nm
.Ar configfile
.Sh DESCRIPTION
.Nm
is a simple web pastebin, that can be used in HTTP or CGI mode.
People can post without any restriction (except a size limit).
.Pp
An API, accessible via an API key, let the administrator manage the
pastebin (purge old pastes, list pastes, delete a single paste).
.Sh CONFIGURATION FILE
.Nm
configuration file is made of key/value pairs:
.Ss pasteurl
Specify the base url of this pastebin server (required). This value is used to
and generate valid pastes URL. When pastebin is used behind an http
proxy, the public URL should be specified here.
.Pp
Example:
.Dl pasteurl: https://domain.com/pastebin
.Ed
.Pp
Using the value above, the pastes URL will have the following
form:
.Dl https://domain.com/pastebin/e2bd5143bd
.Pp
And the administator API will be available as:
.Dl https://domain.com/pastebin/api/...
.Ss dbpath
Specify the location of the sqlite3 database (required). The database
is created automatically if not present.
.Pp
Example:
.Dl dbpath: /var/db/pastebin.sqlite3
.Ss sizelimit
Specify the POST size limit in bytes. If this value is missing,
POST are unlimited. The value can have a K or M suffix to specify
a size in Kilo or Mega bytes.
.Pp
Example:
.Dl sizelimit: 10M
.Ss apikey
API key to access the admin API. The key should be at leat 10
characters long. If this field is missing, the admin API is
disabled.
.Pp
Example:
.Dl apikey: 810a5bdaaf
.Ss httpport
Specify the port to bind for HTTP connections. If the value is 0,
pastebin work in CGI mode. Default value is 8080.
.Pp
Example:
.Dl httpport: 80
.Sh SAMPLE HTTP CONFIG
Here goes a complete configuration in HTTP mode:
.Bd -literal -offset indent
$ cat pastebin.cfg
pasteurl: https://domain.com/pastebin
dbpath: /var/db/pastebin.sqlite3
apikey: 810a5bdaaf
sizelimit: 10M
.Ed
.Pp
And the pastebin server can be started this way:
.Pp
.Dl $ pastebin pastebin.cfg
.Ed
.Sh SAMPLE CGI CONFIG
A CGI script can be setup in a single file, using pastebin as a
shebang interpreter, and the rest of the file for configuration pragmas:
.Bd -literal -offset indent
$ cat pastebin.cgi
#!/usr/local/bin/pastebin
pasteurl: https://domain.com/pastebin.cgi
dbpath: /var/db/pastebin.sqlite3
apikey: 810a5bdaaf
sizelimit: 10M
httpport: 0
.Ed
.Pp
Then, a web server has to be configured to serve this CGI script.
.Sh USAGE
The pastebin can be used from a web browser:
.Pp
.Dl $ firefox https://domain.com/pastebin
.Pp
or from the command line:
.Pp
.Bd -literal -offset indent
$ curl https://domain.com/pastebin < image.jpg
https://domain.com/pastebin/1bbe793662
.Ed
.Sh ADMIN API
Pastebin provide a small API to manage pastes.
.Ss <pasteurl>/api/list
List pastes in JSON format.
.Pp
Example:
.Bd -literal -offset indent
$ curl -d apikey="810a5bdaaf" https://domain.com/pastebin/api/list
[
  {
    "url": "https://domain.com/pastebin/46dbac46ae",
    "inserttime": "2019-10-28T15:38:10Z",
    "mimetype": "text/plain; charset=UTF-8"
  },
  {
    "url": "https://domain.com/pastebin/1085a72014",
    "inserttime": "2019-11-04T12:45:10Z",
    "mimetype": "application/json"
  }
]
.Ss <pasteurl>/api/purge/<ndays>
Purge pastes older than <ndays> days.
.Pp
Example:
.Pp
.Bd -literal -offset indent
$ curl -d apikey="810a5bdaaf" https://domain.com/pastebin/api/purge/10
.Ed
.Ss <pasteurl>/api/del/<pasteid>
Delete a single paste. The <pasteid> argument must be the 10
characters hexadecimal value assigned to a paste.
.Pp
Example:
.Bd -literal -offset indent
$ curl -d apikey="810a5bdaaf" https://domain.com/pastebin/api/del/1085a72014
.Ed
.Sh AUTHOR
.An Gerome Fournier Aq Mt jef@foutaise.org .

Added pastebin.1.txt.























































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
PASTEBIN(1)                                                        PASTEBIN(1)

NAME
     pastebin – Simplistic web pastebin

SYNOPSIS
     pastebin configfile

DESCRIPTION
     pastebin is a simple web pastebin, that can be used in HTTP or CGI mode.
     People can post without any restriction (except a size limit).

     An API, accessible via an API key, let the administrator manage the
     pastebin (purge old pastes, list pastes, delete a single paste).

CONFIGURATION FILE
     pastebin configuration file is made of key/value pairs:

   pasteurl
     Specify the base url of this pastebin server (required). This value is
     used to and generate valid pastes URL. When pastebin is used behind an
     http proxy, the public URL should be specified here.

     Example:
           pasteurl: https://domain.com/pastebin

     Using the value above, the pastes URL will have the following form:
           https://domain.com/pastebin/e2bd5143bd

     And the administator API will be available as:
           https://domain.com/pastebin/api/...

   dbpath
     Specify the location of the sqlite3 database (required). The database is
     created automatically if not present.

     Example:
           dbpath: /var/db/pastebin.sqlite3

   sizelimit
     Specify the POST size limit in bytes. If this value is missing, POST are
     unlimited. The value can have a K or M suffix to specify a size in Kilo
     or Mega bytes.

     Example:
           sizelimit: 10M

   apikey
     API key to access the admin API. The key should be at leat 10 characters
     long. If this field is missing, the admin API is disabled.

     Example:
           apikey: 810a5bdaaf

   httpport
     Specify the port to bind for HTTP connections. If the value is 0,
     pastebin work in CGI mode. Default value is 8080.

     Example:
           httpport: 80

SAMPLE HTTP CONFIG
     Here goes a complete configuration in HTTP mode:

           $ cat pastebin.cfg
           pasteurl: https://domain.com/pastebin
           dbpath: /var/db/pastebin.sqlite3
           apikey: 810a5bdaaf
           sizelimit: 10M

     And the pastebin server can be started this way:

           $ pastebin pastebin.cfg

SAMPLE CGI CONFIG
     A CGI script can be setup in a single file, using pastebin as a shebang
     interpreter, and the rest of the file for configuration pragmas:

           $ cat pastebin.cgi
           #!/usr/local/bin/pastebin
           pasteurl: https://domain.com/pastebin.cgi
           dbpath: /var/db/pastebin.sqlite3
           apikey: 810a5bdaaf
           sizelimit: 10M
           httpport: 0

     Then, a web server has to be configured to serve this CGI script.

USAGE
     The pastebin can be used from a web browser:

           $ firefox https://domain.com/pastebin

     or from the command line:

           $ curl https://domain.com/pastebin < image.jpg
           https://domain.com/pastebin/1bbe793662

ADMIN API
     Pastebin provide a small API to manage pastes.

   <pasteurl>/api/list
     List pastes in JSON format.

     Example:

           $ curl -d apikey="810a5bdaaf" https://domain.com/pastebin/api/list
           [
             {
               "url": "https://domain.com/pastebin/46dbac46ae",
               "inserttime": "2019-10-28T15:38:10Z",
               "mimetype": "text/plain; charset=UTF-8"
             },
             {
               "url": "https://domain.com/pastebin/1085a72014",
               "inserttime": "2019-11-04T12:45:10Z",
               "mimetype": "application/json"
             }
           ]

   <pasteurl>/api/purge/<ndays>
     Purge pastes older than <ndays> days.

     Example:

           $ curl -d apikey="810a5bdaaf" https://domain.com/pastebin/api/purge/10

   <pasteurl>/api/del/<pasteid>
     Delete a single paste. The <pasteid> argument must be the 10 characters
     hexadecimal value assigned to a paste.

     Example:

           $ curl -d apikey="810a5bdaaf" https://domain.com/pastebin/api/del/1085a72014

AUTHOR
     Gerome Fournier <jef@foutaise.org>.

                                 November 2019

Added pasteid.go.



































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
	"math/rand"
	"time"
	"unicode"
)

var pasteIdRunes = []rune("0123456789abcdef")

func PasteIdIsValid(pasteid string) bool {
	if len(pasteid) != 10 {
		return false
	}

	for _, c := range pasteid {
		if !unicode.IsDigit(c) && !('a' <= c && c <= 'f') {
			return false
		}
	}

	return true
}

func PasteIdRandom() string {
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 10)
	for i := range b {
		b[i] = pasteIdRunes[rand.Intn(len(pasteIdRunes))]
	}

	return string(b)
}

Added size.go.



























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"fmt"
	"strconv"
	"strings"
)

type unit struct {
	suffix string
	value  int64
}

var units = []unit{
	{"K", 1e3},
	{"M", 1e6},
}

func SizeParse(data string) (value int64, err error) {
	data = strings.ToUpper(data)
	for _, u := range units {
		if strings.HasSuffix(data, u.suffix) {
			value, err = strconv.ParseInt(data[:len(data)-1], 10, 64)
			if err != nil {
				return
			}
			value *= u.value
			return
		}
	}

	return strconv.ParseInt(data, 10, 64)
}

func SizeString(value int64) string {
	if value < 1e3 {
		return fmt.Sprintf("%d", value)
	} else if value < 1e6 {
		f := float64(value) / 1e3
		return fmt.Sprintf("%.1fK", f)
	} else {
		f := float64(value) / 1e6
		return fmt.Sprintf("%.1fM", f)
	}
}