contrib/clean.sh | 4 ++-- contrib/paster | 15 --------------- contrib/paster-tls-gnutls | 2 -- contrib/paster-tls-gnutls.sh | 2 ++ contrib/paster-tls-ucspi | 4 ---- contrib/paster-tls-ucspi.sh | 4 ++++ contrib/paster-ucspi | 3 --- contrib/paster-ucspi.sh | 3 +++ contrib/paster.sh | 16 ++++++++++++++++ contrib/paster.tcl | 14 ++++++-------- contrib/paster.zsh | 7 ++++--- doc/features.texi | 4 ++-- doc/install.texi | 2 +- doc/protocol.texi | 16 ++++++---------- go.mod | 6 ++++-- go.sum | 2 ++ main.go | 116 ++++++++++++++++++++--------------------------------- diff --git a/contrib/clean.sh b/contrib/clean.sh index a9148a9ae7c187bbbe66a2241474fd7326f41918..452c04063a6022d3cadaeb6b6603087778556cc7 100755 --- a/contrib/clean.sh +++ b/contrib/clean.sh @@ -1,6 +1,6 @@ -#!/bin/sh +#!/bin/sh -e -find . -type f -mtime +1 \ +exec find . -type f -mtime +1 \ -and -not -name index.html \ -and -not -name "asciinema-player-*" \ -delete diff --git a/contrib/paster b/contrib/paster deleted file mode 100755 index 64f8c79aee9dc2617c17b3b8611525b8ab05bcb9..0000000000000000000000000000000000000000 --- a/contrib/paster +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -e -DST=${DST:-nc paster.example.com 2020} -if [ -s "$1" ]; then - src="$1" - bn="${1##*/}" - _ext="${bn##*.}" - [ "$bn" = "$_ext" ] || ext="1:e${#_ext}:$_ext" -else - src=`mktemp` - trap "rm -f $src" HUP PIPE INT QUIT TERM EXIT - cat > $src - [ $# -eq 0 ] || ext="1:e${#1}:$1" -fi -size=`perl -e 'print -s $ARGV[0]' $src` -( echo -n "d${ext}1:v${size}:" ; cat $src ; echo -n e ) | $DST diff --git a/contrib/paster-tls-gnutls b/contrib/paster-tls-gnutls deleted file mode 100755 index e091bfd734e12cf5daf39910df5b8b7670eb0674..0000000000000000000000000000000000000000 --- a/contrib/paster-tls-gnutls +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -DST="gnutls-cli --logfile=/dev/null -p 2021 paster.example.com" paster $@ diff --git a/contrib/paster-tls-gnutls.sh b/contrib/paster-tls-gnutls.sh new file mode 100755 index 0000000000000000000000000000000000000000..b916998a9e11baa00a2e72c3a79be5c9cc1f4c26 --- /dev/null +++ b/contrib/paster-tls-gnutls.sh @@ -0,0 +1,2 @@ +#!/bin/sh -e +DST="gnutls-cli --logfile=/dev/null -p 2021 paster.example.com" paster $@ diff --git a/contrib/paster-tls-ucspi b/contrib/paster-tls-ucspi deleted file mode 100755 index 0ee162a790d7e3e32bc5a72b16f4202aed7fd768..0000000000000000000000000000000000000000 --- a/contrib/paster-tls-ucspi +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -HOST=paster.example.com -DST=cat paster $@ | -tcpclient -DHR -l 0 $HOST 2021 tlsc -name $HOST sh -c "cat >&7 ; cat <&6" diff --git a/contrib/paster-tls-ucspi.sh b/contrib/paster-tls-ucspi.sh new file mode 100755 index 0000000000000000000000000000000000000000..fdf7a80fd6825d1a1cc19dbcfba2bbd5e5250e52 --- /dev/null +++ b/contrib/paster-tls-ucspi.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e +HOST=paster.example.com +DST=cat paster $@ | +tcpclient -DHR -l 0 $HOST 2021 tlsc -name $HOST sh -c "cat >&7 ; cat <&6" diff --git a/contrib/paster-ucspi b/contrib/paster-ucspi deleted file mode 100755 index 8ab1070a623af8b90f105d4d7adc310b87d2e7be..0000000000000000000000000000000000000000 --- a/contrib/paster-ucspi +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -DST=cat paster $@ | -tcpclient -DHR -l 0 paster.example.com 2020 sh -c "cat >&7 ; cat <&6" diff --git a/contrib/paster-ucspi.sh b/contrib/paster-ucspi.sh new file mode 100755 index 0000000000000000000000000000000000000000..cd5d12d8b4e230695b4b7afe52a523c28f4042ee --- /dev/null +++ b/contrib/paster-ucspi.sh @@ -0,0 +1,3 @@ +#!/bin/sh -e +DST=cat paster $@ | +tcpclient -DHR -l 0 paster.example.com 2020 sh -c "cat >&7 ; cat <&6" diff --git a/contrib/paster.sh b/contrib/paster.sh new file mode 100755 index 0000000000000000000000000000000000000000..bac1926b0da2c728f4c66be573e6b9b1cb51b4d4 --- /dev/null +++ b/contrib/paster.sh @@ -0,0 +1,16 @@ +#!/bin/sh -e +DST=${DST:-nc paster.example.com 2020} +ext="0:," +if [ -s "$1" ]; then + src="$1" + bn="${1##*/}" + _ext="${bn##*.}" + [ "$bn" = "$_ext" ] || ext="${#_ext}:$_ext," +else + src=`mktemp` + trap "rm -f $src" HUP PIPE INT QUIT TERM EXIT + cat > $src + [ $# -eq 0 ] || ext="${#1}:$1," +fi +size=$(perl -e 'print -s $ARGV[0]' "$src") +( echo -n "${ext}${size}:" ; cat "$src" ; echo -n , ) | $DST diff --git a/contrib/paster.tcl b/contrib/paster.tcl index 7ef8f0b159b03064c3389b34c33124aa9d560d4a..ad7c4eff984fa1d5e9b8f6d26dcca44d1670ab31 100755 --- a/contrib/paster.tcl +++ b/contrib/paster.tcl @@ -4,10 +4,11 @@ set Host paster.example.com set Port 2021 set GnuTLS 1 +set ext "" set fn [lindex $argv 0] set size 0 if {($argc > 0) && [file exists $fn]} { - set ext [file extension $fn] + set ext [string trimleft [file extension $fn] .] set fd [open $fn {RDONLY BINARY}] set size [file size $fn] } else { @@ -23,19 +24,16 @@ } { set sock [socket $Host $Port] chan configure $sock -encoding binary -translation binary } -puts -nonewline $sock "d" -if {[info exists ext]} { - set ext [string trimleft $ext .] - puts -nonewline $sock [string cat "1:e" [string length $ext] ":$ext"] -} -puts -nonewline $sock "1:v$size:" + +puts -nonewline $sock [string cat [string length $ext] ":$ext,"] +puts -nonewline $sock "$size:" if {[info exists fd]} { fcopy $fd $sock close $fd } { puts -nonewline $sock $data } -puts -nonewline $sock "e" +puts -nonewline $sock "," flush $sock while {[gets $sock line] >= 0} { puts $line } close $sock diff --git a/contrib/paster.zsh b/contrib/paster.zsh index a4850d574ad47537d819555e540a4bcefc819c27..76f1464697ee567553b46c09e2dd64ebdfa1a408 100755 --- a/contrib/paster.zsh +++ b/contrib/paster.zsh @@ -3,16 +3,17 @@ set -e DST=${DST:-paster.example.com 2020} +ext="0:," [[ -s "$1" ]] && { src="$1" bn=$src:t e=${bn##*.} - [[ $bn = $e ]] || ext="1:e${#e}:$e" + [[ $bn = $e ]] || ext="${#e}:$e," } || { src=`mktemp` trap "rm -f $src" HUP PIPE INT QUIT TERM EXIT cat > $src - [[ $# -eq 0 ]] || ext="1:e${#1}:$1" + [[ $# -eq 0 ]] || ext="${#1}:$1," } zmodload -F zsh/stat b:zstat size=`zstat +size $src` @@ -20,6 +21,6 @@ zmodload zsh/net/tcp ztcp ${=DST} fd=$REPLY -( print -n "d${ext}1:v${size}:" ; cat $src ; print -n e ) >&$fd +( print -n "${ext}${size}:" ; cat $src ; print -n , ) >&$fd cat <&$fd ztcp -c $fd diff --git a/doc/features.texi b/doc/features.texi index 56bcfa6774c240b431015d1d75cf900efc62a706..ffd0539007e52e39f6e55636516936570daf72a8 100644 --- a/doc/features.texi +++ b/doc/features.texi @@ -12,8 +12,8 @@ be overriden for the given paste. So your HTTP service can answer with varying @code{Content-Types}. You can share images for example, not only plaintext -@item No excessive HTTP protocol: just send - @url{https://en.wikipedia.org/wiki/Bencode, bencode}-ed dictionary +@item No excessive HTTP protocol: just send two + @url{https://en.wikipedia.org/wiki/Netstring, netstring}-encoded strings with the data over the TCP @item Newline is appended for @file{.txt}/@file{.url} pastes, if it is missing diff --git a/doc/install.texi b/doc/install.texi index 8083cb3443090a69573a8b7740b86e5100168d18..8d62e6b96c85fb8340de5ad00952700a9c3a9da7 100644 --- a/doc/install.texi +++ b/doc/install.texi @@ -7,7 +7,7 @@ @item Install paster itself: @example -$ go get go.stargrave.org/paster +$ go get go.stargrave.org/paster/v2 @end example If you have got problems with your trust anchors, unwilling to diff --git a/doc/protocol.texi b/doc/protocol.texi index 491cc790508088412c979c3e14cdf2a4d598b6a6..e1c0acc5553bad9cda348bcc3cb841b14a04d9bd 100644 --- a/doc/protocol.texi +++ b/doc/protocol.texi @@ -1,16 +1,12 @@ @node Protocol @unnumbered Protocol -Protocol is very simple: @url{https://en.wikipedia.org/wiki/Bencode, bencode}d -dictionary is sent over TCP. - -@itemize -@item @code{v} key contains the data you want to paste -@item optional @code{e} key, holding the desired filename extension, - without the leading dot, up to 9 characters long -@end itemize +Protocol is very simple: two +@url{https://en.wikipedia.org/wiki/Netstring, netstring}s are sent over TCP. +First one holds desired filename extension, without the leading dot, up to 9 +characters long. Second one is the paste data itself. @example -"hello world" => d1:v11:hello worlde -"http://example.com/" and "url" extension => d1:e3:url1:v18:http://example.come + "hello world", no explicit extension => 0:,11:hello world, +"http://example.com/", "url" extension => 3:url,18:http://example.com, @end example diff --git a/go.mod b/go.mod index af8f1e53ffacca55c578f95ab005e34fdbd51a32..4dbc7c13a1407b4c3ffcdbef9bd7e091ca88e38d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module go.stargrave.org/paster +module go.stargrave.org/paster/v2 -go 1.16 +go 1.17 + +require go.cypherpunks.ru/netstring/v2 v2.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..504496380f36cf8b8a16120bb537303b9676e2cb --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +go.cypherpunks.ru/netstring/v2 v2.4.0 h1:qBOtHJj1hoCUpYkouuTurXl20R1IKnEkh+q7/J0TgZ4= +go.cypherpunks.ru/netstring/v2 v2.4.0/go.mod h1:6YDx4gW414SmHdvSBMKbHaB2/7w9WZ04NQb7XIUV/pA= diff --git a/main.go b/main.go index 912408b035f2b885f3d4998750d00d27d9849737..25e93d4f45ad03d34809e6b1f153b6673b3e6124 100644 --- a/main.go +++ b/main.go @@ -30,8 +30,11 @@ "fmt" "html/template" "io" "os" - "strconv" + + "go.cypherpunks.ru/netstring/v2" ) + +const MaxExtLen = 9 var ( //go:embed asciicast.tmpl @@ -74,7 +77,7 @@ func main() { maxSize := flag.Uint64("max-size", 1<<20, "Maximal upload size") asciicastPath := flag.String("asciicast-path", "", "Generate HTMLs for .cast asciicasts, specify \"asciinema-player-v2.6.1\"") flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: paster [options] URL [...]\n") + fmt.Fprintf(os.Stderr, "Usage: paster [options] URL [URL ...]\n") flag.PrintDefaults() } flag.Parse() @@ -82,114 +85,81 @@ if len(flag.Args()) == 0 { flag.Usage() os.Exit(1) } - var fn string - r := bufio.NewReader(os.Stdin) - b, err := r.ReadByte() + r := netstring.NewReader(os.Stdin) + size, err := r.Next() if err != nil { fatal(err.Error()) } - if b != 'd' { - fatal("bad bencode: no dictionary start") + if size > MaxExtLen { + fatal("too long extension length") } - buf := make([]byte, 21) - ext := ".txt" - var size uint64 -AnotherKey: - if _, err = io.ReadFull(r, buf[:3]); err != nil { + data, err := io.ReadAll(r) + if err != nil { fatal(err.Error()) } - switch s := string(buf[:3]); s { - case "1:e": - if _, err = io.ReadFull(r, buf[:2]); err != nil { - fatal(err.Error()) - } - if buf[1] != ':' { - fatal(`bad bencode: invalid "e" format`) - } - extLen, err := strconv.Atoi(string(buf[:1])) - if err != nil { - fatal(err.Error()) - } - if _, err = io.ReadFull(r, buf[:extLen]); err != nil { - fatal(err.Error()) - } - ext = "." + string(buf[:extLen]) - goto AnotherKey - case "1:v": - n, err := r.Read(buf) - if err != nil { - fatal(err.Error()) - } - i := bytes.IndexByte(buf[:n], ':') - if i == -1 { - fatal(`bad bencode: invalid "v" length`) - } - size, err = strconv.ParseUint(string(buf[:i]), 10, 64) - if err != nil { - fatal(err.Error()) - } - if size == 0 { - fatal("empty paste") - } - if size > *maxSize { - fatal("too big") - } - buf = buf[i+1 : n] - default: - fatal("bad bencode: invalid key") + var ext string + if len(data) == 0 { + ext = ".txt" + } else { + ext = "." + string(data) + } + size, err = r.Next() + if err != nil { + fatal(err.Error()) + } + if size == 0 { + fatal("empty paste") + } + if size > *maxSize { + fatal("too big") } rnd := make([]byte, 12) if _, err = io.ReadFull(rand.Reader, rnd); err != nil { fatal(err.Error()) } - fn = "." + base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(rnd) + ext + fn := "." + base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(rnd) + ext fd, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666)) if err != nil { fatal(err.Error()) } h := sha512.New() bfd := bufio.NewWriter(fd) - mr := io.MultiReader(bytes.NewReader(buf), r) mw := io.MultiWriter(bfd, h) - if _, err = io.CopyN(mw, mr, int64(size-1)); err != nil { + buf := make([]byte, 1) + _, err = io.CopyN(mw, r, int64(size-1)) + if err != nil { goto Failed } - if len(buf) == 0 { - buf = append(buf, 0) - } else { - buf = buf[:1] - } - if _, err = mr.Read(buf); err != nil { + _, err = r.Read(buf) + if err != nil { goto Failed } - if _, err = mw.Write(buf); err != nil { + _, err = mw.Write(buf) + if err != nil { goto Failed } if (ext == ".txt" || ext == ".url") && buf[0] != '\n' { - if err = bfd.WriteByte('\n'); err != nil { + err = bfd.WriteByte('\n') + if err != nil { goto Failed } } - if _, err = mr.Read(buf); err != nil { + err = bfd.Flush() + if err != nil { goto Failed } - if buf[0] != 'e' { - os.Remove(fn) - fatal("bad bencode: no dictionary end") - } - if err = bfd.Flush(); err != nil { - goto Failed - } - if err = fd.Close(); err != nil { + err = fd.Close() + if err != nil { goto Failed } - if err = os.Rename(fn, fn[1:]); err != nil { + err = os.Rename(fn, fn[1:]) + if err != nil { goto Failed } for _, u := range flag.Args() { fmt.Println(u + fn[1:]) } - fmt.Println("SHA512/2:", hex.EncodeToString(h.Sum(nil)[:512/2/8])) + fmt.Println("SHA512/2:", hex.EncodeToString(h.Sum(nil)[:sha512.Size/2])) if ext == ".cast" && *asciicastPath != "" { if err = asciicastHTML(*asciicastPath, fn[1:]); err != nil { goto Failed