diff options
author | Thomas Voss <mail@thomasvoss.com> | 2021-10-12 17:59:37 +0200 |
---|---|---|
committer | Thomas Voss <mail@thomasvoss.com> | 2021-10-12 17:59:37 +0200 |
commit | 11a2560513e114ae30904f0598c4732aeac1972a (patch) | |
tree | 15643f492e57cae1e5a9a858f2f13b3fa5633335 |
[Meta] Initial commit
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | LICENSE | 14 | ||||
-rw-r--r-- | Makefile | 19 | ||||
-rw-r--r-- | README.rst | 23 | ||||
-rw-r--r-- | go.mod | 16 | ||||
-rw-r--r-- | go.sum | 85 | ||||
-rw-r--r-- | macros.m4 | 4 | ||||
-rw-r--r-- | mpaste.1 | 140 | ||||
-rw-r--r-- | mpaste.go | 262 |
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 @@ -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. @@ -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 +) @@ -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)) +} |