From 11a2560513e114ae30904f0598c4732aeac1972a Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Tue, 12 Oct 2021 17:59:37 +0200 Subject: [Meta] Initial commit --- .gitignore | 5 ++ LICENSE | 14 ++++ Makefile | 19 +++++ README.rst | 23 ++++++ go.mod | 16 ++++ go.sum | 85 ++++++++++++++++++++ macros.m4 | 4 + mpaste.1 | 140 +++++++++++++++++++++++++++++++++ mpaste.go | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 568 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.rst create mode 100644 go.mod create mode 100644 go.sum create mode 100644 macros.m4 create mode 100644 mpaste.1 create mode 100644 mpaste.go 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)) +} -- cgit v1.2.3