From: Sergey Matveev Date: Wed, 15 Feb 2023 10:31:11 +0000 (+0300) Subject: Netstrings are simpler than bencode X-Git-Tag: v2.0.0^0 X-Git-Url: http://www.git.stargrave.org/?p=paster.git;a=commitdiff_plain;h=9cab99731696b821c2643544c057f54ba7222a7a Netstrings are simpler than bencode --- diff --git a/contrib/clean.sh b/contrib/clean.sh index a9148a9..452c040 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-tls-gnutls b/contrib/paster-tls-gnutls.sh similarity index 85% rename from contrib/paster-tls-gnutls rename to contrib/paster-tls-gnutls.sh index e091bfd..b916998 100755 --- a/contrib/paster-tls-gnutls +++ b/contrib/paster-tls-gnutls.sh @@ -1,2 +1,2 @@ -#!/bin/sh +#!/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.sh similarity index 90% rename from contrib/paster-tls-ucspi rename to contrib/paster-tls-ucspi.sh index 0ee162a..fdf7a80 100755 --- a/contrib/paster-tls-ucspi +++ b/contrib/paster-tls-ucspi.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/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.sh similarity index 87% rename from contrib/paster-ucspi rename to contrib/paster-ucspi.sh index 8ab1070..cd5d12d 100755 --- a/contrib/paster-ucspi +++ b/contrib/paster-ucspi.sh @@ -1,3 +1,3 @@ -#!/bin/sh +#!/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 b/contrib/paster.sh similarity index 51% rename from contrib/paster rename to contrib/paster.sh index 64f8c79..bac1926 100755 --- a/contrib/paster +++ b/contrib/paster.sh @@ -1,15 +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="1:e${#_ext}:$_ext" + [ "$bn" = "$_ext" ] || ext="${#_ext}:$_ext," else 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," fi -size=`perl -e 'print -s $ARGV[0]' $src` -( echo -n "d${ext}1:v${size}:" ; cat $src ; echo -n e ) | $DST +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 7ef8f0b..ad7c4ef 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 @@ if $GnuTLS { 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 a4850d5..76f1464 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 @@ size=`zstat +size $src` 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 56bcfa6..ffd0539 100644 --- a/doc/features.texi +++ b/doc/features.texi @@ -12,8 +12,8 @@ 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 8083cb3..8d62e6b 100644 --- a/doc/install.texi +++ b/doc/install.texi @@ -7,7 +7,7 @@ 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 491cc79..e1c0acc 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 af8f1e5..4dbc7c1 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 0000000..5044963 --- /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 912408b..25e93d4 100644 --- a/main.go +++ b/main.go @@ -30,9 +30,12 @@ import ( "html/template" "io" "os" - "strconv" + + "go.cypherpunks.ru/netstring/v2" ) +const MaxExtLen = 9 + var ( //go:embed asciicast.tmpl ASCIICastHTMLTmplRaw string @@ -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 @@ func main() { 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 { - goto Failed - } - if buf[0] != 'e' { - os.Remove(fn) - fatal("bad bencode: no dictionary end") - } - if err = bfd.Flush(); err != nil { + err = bfd.Flush() + if 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