aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2021-10-12 17:59:37 +0200
committerThomas Voss <mail@thomasvoss.com> 2021-10-12 17:59:37 +0200
commit11a2560513e114ae30904f0598c4732aeac1972a (patch)
tree15643f492e57cae1e5a9a858f2f13b3fa5633335
[Meta] Initial commit
-rw-r--r--.gitignore5
-rw-r--r--LICENSE14
-rw-r--r--Makefile19
-rw-r--r--README.rst23
-rw-r--r--go.mod16
-rw-r--r--go.sum85
-rw-r--r--macros.m44
-rw-r--r--mpaste.1140
-rw-r--r--mpaste.go262
9 files changed, 568 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..24d1966
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+files/
+counter
+index
+mpaste
+users
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..562eb47
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,14 @@
+BSD Zero Clause License
+
+Copyright (c) 2021 Thomas Voss
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e038050
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,19 @@
+.POSIX:
+
+MANDIR = /usr/share/man/man1
+target = mpaste
+
+all: ${target}
+${target}: macros.m4 mpaste.go
+ m4 macros.m4 mpaste.go >tmp.go
+ go build tmp.go
+ mv tmp ${target}
+ rm tmp.go
+
+docs:
+ >/dev/null command -v gzip && gzip -c9 mpaste.1 >${MANDIR}/mpaste.1.gz || \
+ cp mpaste.1 ${MANDIR}
+
+clean:
+ rm -rf ${target} tmp.go counter files/
+.PHONY: clean
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..8ed5334
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,23 @@
+.. vi: tw=100
+
+mpaste
+======
+
+**mpaste** is a super minimal paste server written in Go. It supports extremely simple file uploads
+assuming you have the tools available to do so, such as ``curl(1)``. It also supports viewing pastes
+in the browser as either raw text or with syntax highlighting. This paste server has no web
+interface for adding pastes, no stupid moving parts, no useless garbage.
+
+
+Documentation
+-------------
+
+Read `the manual`_
+
+.. _the manual: mpaste.1
+
+
+Compilation
+-----------
+
+Just run the ``make`` command.
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a18635c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,16 @@
+module mpaste
+
+go 1.17
+
+require github.com/alecthomas/chroma v0.9.2
+
+require (
+ github.com/Mango0x45/getgopt v0.0.0-20211008215918-31013048af5c // indirect
+ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
+ github.com/dlclark/regexp2 v1.4.0 // indirect
+ github.com/goccy/go-json v0.7.9 // indirect
+ github.com/lestrrat-go/jwx v1.2.7 // indirect
+ github.com/lestrrat-go/option v1.0.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..993dad3
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,85 @@
+github.com/Mango0x45/getgopt v0.0.0-20211008215918-31013048af5c h1:nGtV2AJrDzl1NFcrVa0ybZlIrH2guRlndAa1qCVq5Xs=
+github.com/Mango0x45/getgopt v0.0.0-20211008215918-31013048af5c/go.mod h1:MgnwF7U7DDwnqtwEJ1nEbaBZVhRsI//AOXqH1C4nHTk=
+github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
+github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
+github.com/alecthomas/chroma v0.9.2 h1:yU1sE2+TZbLIQPMk30SolL2Hn53SR/Pv750f7qZ/XMs=
+github.com/alecthomas/chroma v0.9.2/go.mod h1:eMuEnpA18XbG/WhOWtCzJHS7WqEtDAI+HxdwoW0nVSk=
+github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
+github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
+github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
+github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
+github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
+github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
+github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
+github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.7.9 h1:mSp3uo1tr6MXQTYopSNhHTUnJhd2zQ4Yk+HdJZP+ZRY=
+github.com/goccy/go-json v0.7.9/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
+github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
+github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
+github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
+github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
+github.com/lestrrat-go/jwx v1.2.7 h1:wO7fEc3PW56wpQBMU5CyRkrk4DVsXxCoJg7oIm5HHE4=
+github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA=
+github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
+github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
+golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/macros.m4 b/macros.m4
new file mode 100644
index 0000000..068ff3e
--- /dev/null
+++ b/macros.m4
@@ -0,0 +1,4 @@
+define(`m4_len', defn(`len'))
+undefine(`len')
+define(`WRITE_HEADER',
+ `w.WriteHeader($1); ifelse(eval(m4_len($2) == 0), 0, fmt.Fprintln(w, $2);) return;')
diff --git a/mpaste.1 b/mpaste.1
new file mode 100644
index 0000000..f163734
--- /dev/null
+++ b/mpaste.1
@@ -0,0 +1,140 @@
+.\" vi: tw=100
+.Dd $Mdocdate: October 12 2021 $
+.Dt MPASTE 1 URM
+.Os UNIX
+.Sh NAME
+.Nm mpaste
+.Nd a simple and minimal paste server
+.Sh SYNOPSIS
+.Nm
+.Op Fl c Ar counter
+.Op Fl f Ar file_dir
+.Op Fl i Ar index
+.Op Fl u Ar users
+.Ar domain
+.Ar port
+.Sh DESCRIPTION
+.Nm
+is a minimal paste server for hosting plaintext data.
+The paste server has support for file uploads, syntax highlighting, a customizable homepage, and a
+password protected mode where only users with valid API keys can upload pastes.
+For the simplest example of a working paste server, simply run
+.Nm
+and provide it with a
+.Ar domain
+and
+.Ar port .
+The provided domain is not super important, it is just used in the message sent back to the client
+after a successful paste so that they have a direct link to click on to go to their paste.
+The port on the otherhand does matter, it is the port on which the server will listen.
+.Pp
+Once the server is running you can POST a file to the server by sending a form with the name
+.Dq file .
+Here is an example of POSTing a file with
+.Xr curl 1 :
+.Pp
+.Dl $ curl -X POST -F 'file=@foo.txt' domain.com
+.Pp
+After a successful POST the server will respond with a URI to the post in the form
+.Dq domain.com/ID
+where
+.Dq ID
+is a number which increments with each paste.
+When viewed, the paste will be displayed as unformatted plaintext.
+If you would like syntax highlighting simply append the appropiate file extension to the URI.
+For example, to syntax highlight C code with paste ID 5, go to
+.Dq domain.com/5.c .
+.Pp
+If you would like to protect the server by requiring all users to have an API key, simply set the
+.Ev MPASTE_SECRET
+environment variable.
+With this secret set, you can generate a JWT token encoded with that same secret, and with the
+playload
+.Dq name=USERS NAME .
+For example, one might have the payload
+.Dq name=Johnny Appleseed .
+This name is then looked up in the
+.Pa users
+file.
+If the name is found in that file, the POST is allowed, otherwise it is rejected.
+You can specify the
+.Pa users
+file with the
+.Fl u
+flag.
+An example file might look like this:
+.Pp
+.Bd -literal -offset indent
+Johnny Appleseed
+John Doe
+Hunter
+.Ed
+.Pp
+As a user if you want to authenticate yourself you must send your JWT token in an authorization
+header.
+As an example using
+.Xr curl 1 :
+.Pp
+.Dl $ curl -X POST -H 'Authorization: YOUR.JWT.TOKEN' -F 'file=@foo.txt' domain.com
+.Pp
+Finally, you may want to display content on the paste servers homepage.
+This is easy and can be done by creating a
+.Pa index.html
+in the current working directory.
+If you would like to specify a different file, you can use the
+.Fl i Ar index
+flag.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c Ar counter
+Specify the path to a file to use as a counter.
+This file will hold the number of the ID that will be assigned to the next paste.
+If the given file does not exist, it will be created on the next successful paste.
+If this flag is not specified then it will default to
+.Pa counter .
+.It Fl f Ar file_dir
+Specify a directory in which to store the pastes that users POST to the server.
+If the given folder does not exist, then it will be created.
+If this flag is not specified then it will default to
+.Pa files/ .
+.It Fl i Ar index
+Specify a file to serve on the servers root
+.Pq Pa /
+page.
+If this flag is not specified then it will default to
+.Pa index.html .
+.It Fl u Ar users
+Specify a file to store authorized users in.
+This file must be created by the user and must contain a newline seperated list of authorized users
+as shown in the
+.Sx DESCRIPTION
+section of this manual.
+If this flag is specified and the
+.Ev MPASTE_SECRET
+environment variable is not set, it will have no effect.
+If the environment is set and this file does not exist, then no users will be allowed to POST.
+.El
+.Sh FILES
+.Bl -tag -width Ds
+.It Pa counter
+This is where the ID of the next paste is stored.
+.It Pa index.html
+This is the default file that the
+.Nm
+server will attempt to serve on the root
+.Pq Pa /
+page.
+.It Pa users
+This is a newline seperated list of authenticated users.
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width Ds
+.It Ev MPASTE_SECRET
+This is the secret key used to encode and decode the JWT tokens used when authenticating users.
+Under no circumstances should you share this token with anybody.
+If not set, anyone will be able to POST their pastes to the server.
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr curl 1
diff --git a/mpaste.go b/mpaste.go
new file mode 100644
index 0000000..eec6a20
--- /dev/null
+++ b/mpaste.go
@@ -0,0 +1,262 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/Mango0x45/getgopt"
+ "github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/lexers"
+ "github.com/alecthomas/chroma/styles"
+ "github.com/dgrijalva/jwt-go"
+)
+
+const (
+ URL_HOMEPAGE = iota
+ URL_INVALID
+ URL_SYNTAX
+ URL_VALID
+)
+
+var (
+ counter int
+ counter_file string
+ domain string
+ file_prefix string
+ index_file string
+ mutex sync.Mutex
+ secret_key = os.Getenv("MPASTE_SECRET")
+ user_file string
+)
+
+var (
+ style = styles.Get("pygments")
+ formatter = html.New(html.Standalone(true), html.WithClasses(true),
+ html.WithLineNumbers(true), html.LineNumbersInTable(true))
+)
+
+func usage() {
+ fmt.Fprintf(os.Stderr,
+ "Usage: %s [-c file] [-f directory] [-i file] [-u file] domain port\n",
+ os.Args[0])
+ os.Exit(1)
+}
+
+func error_and_die(e interface{}) {
+ fmt.Fprintln(os.Stderr, e)
+ os.Exit(1)
+}
+
+func remove_ext(s string) string {
+ return strings.TrimSuffix(s, path.Ext(s))
+}
+
+func allowed_user(name string) bool {
+ mutex.Lock()
+ defer mutex.Unlock()
+
+ if _, err := os.Stat(user_file); os.IsNotExist(err) {
+ return false
+ }
+
+ file, err := os.Open(user_file)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return false
+ }
+
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ scanner.Split(bufio.ScanLines)
+
+ for scanner.Scan() {
+ if scanner.Text() == name {
+ return true
+ }
+ }
+
+ return false
+}
+
+func validate_token(r *http.Request) bool {
+ token, _ := jwt.Parse(r.Header.Get("Authorization"), func(t *jwt.Token) (interface{}, error) {
+ if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("Something went wrong\n")
+ }
+ return []byte(secret_key), nil
+ })
+
+ if token == nil {
+ return false
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+
+ if !(ok && token.Valid) {
+ return false
+ }
+
+ if user_file == "" {
+ return true
+ }
+
+ return allowed_user(claims["name"].(string))
+}
+
+func is_valid_url(s string) int {
+ var i int
+ var c rune
+ for i, c = range s {
+ if c == '.' && i > 0 {
+ return URL_SYNTAX
+ } else if c < '0' || c > '9' {
+ return URL_INVALID
+ }
+ }
+
+ if c != 0 {
+ return URL_VALID
+ }
+ return URL_HOMEPAGE
+}
+
+func syntax_highlighting(w http.ResponseWriter, r *http.Request) {
+ lexer := lexers.Match(r.URL.Path[1:])
+ if lexer == nil {
+ http.ServeFile(w, r, file_prefix+r.URL.Path[1:])
+ return
+ }
+
+ data, err := ioutil.ReadFile(file_prefix + remove_ext(r.URL.Path[1:]))
+ if err != nil {
+ WRITE_HEADER(http.StatusNotFound, "404 page not found")
+ }
+
+ iterator, err := lexer.Tokenise(nil, string(data))
+ if err != nil {
+ WRITE_HEADER(http.StatusInternalServerError, "Failed to tokenize output")
+ }
+
+ if err := formatter.Format(w, style, iterator); err != nil {
+ WRITE_HEADER(http.StatusInternalServerError, "Failed to format output")
+ }
+}
+
+func endpoint(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodGet:
+ switch is_valid_url(r.URL.Path[1:]) {
+ case URL_HOMEPAGE:
+ http.ServeFile(w, r, index_file)
+ case URL_INVALID:
+ WRITE_HEADER(http.StatusNotFound, "404 page not found")
+ case URL_SYNTAX:
+ w.Header().Set("Content-Type", "text/html")
+ syntax_highlighting(w, r)
+ case URL_VALID:
+ w.Header().Set("Content-Type", "text/plain")
+ http.ServeFile(w, r, file_prefix+r.URL.Path[1:])
+ }
+ case http.MethodPost:
+ if secret_key != "" && !validate_token(r) {
+ WRITE_HEADER(http.StatusForbidden, "Invalid API key")
+ }
+
+ file, _, err := r.FormFile("data")
+ defer file.Close()
+ if err != nil {
+ WRITE_HEADER(http.StatusInternalServerError, "Failed to parse form")
+ }
+
+ mutex.Lock()
+
+ fname := file_prefix + strconv.Itoa(counter)
+ nfile, err := os.Create(fname)
+ defer nfile.Close()
+ if err != nil {
+ WRITE_HEADER(http.StatusInternalServerError, "Failed to create file")
+ }
+
+ if _, err = io.Copy(nfile, file); err != nil {
+ WRITE_HEADER(http.StatusInternalServerError, "Failed to write file")
+ }
+
+ if err = os.WriteFile(counter_file, []byte(strconv.Itoa(counter+1)), 0644); err != nil {
+ WRITE_HEADER(http.StatusInternalServerError, "Failed to update counter")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, domain+"/%d\n", counter)
+
+ counter++
+ mutex.Unlock()
+ default:
+ WRITE_HEADER(http.StatusMethodNotAllowed, "Only GET and POST requests are supported")
+ }
+}
+
+func main() {
+ for opt := byte(0); getgopt.Getopt(len(os.Args), os.Args, ":c:f:i:u:", &opt); {
+ switch opt {
+ case 'c':
+ counter_file = getgopt.Optarg
+ case 'f':
+ file_prefix = getgopt.Optarg
+ case 'i':
+ index_file = getgopt.Optarg
+ case 'u':
+ user_file = getgopt.Optarg
+ default:
+ usage()
+ }
+ }
+
+ argv := os.Args[getgopt.Optind:]
+ if len(argv) != 2 {
+ usage()
+ }
+ domain = argv[0]
+ port := argv[1]
+
+ if file_prefix == "" {
+ file_prefix = "files/"
+ } else if file_prefix[len(file_prefix)-1] != '/' {
+ file_prefix += "/"
+ }
+
+ if index_file == "" {
+ index_file = "index.html"
+ }
+
+ if _, err := os.Stat(index_file); os.IsNotExist(err) {
+ error_and_die(err)
+ }
+
+ if _, err := os.Stat(file_prefix); os.IsNotExist(err) {
+ if err = os.MkdirAll(file_prefix, 0755); err != nil {
+ error_and_die(err)
+ }
+ }
+
+ if _, err := os.Stat(counter_file); os.IsNotExist(err) {
+ counter = 0
+ } else {
+ data, err := ioutil.ReadFile(counter_file)
+ if err != nil {
+ error_and_die(err)
+ }
+ counter, _ = strconv.Atoi(string(data))
+ }
+
+ http.HandleFunc("/", endpoint)
+ error_and_die(http.ListenAndServe(":"+port, nil))
+}