// paster -- paste service // Copyright (C) 2021-2024 Sergey Matveev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package main import ( "bufio" "bytes" "crypto/rand" _ "embed" "encoding/base32" "encoding/hex" "flag" "fmt" "html/template" "io" "os" "go.cypherpunks.ru/netstring/v2" "lukechampine.com/blake3" ) const MaxExtLen = 9 var ( //go:embed asciicast.tmpl ASCIICastHTMLTmplRaw string ASCIICastHTMLTmpl = template.Must(template.New("asciicast").Parse( ASCIICastHTMLTmplRaw, )) ) func fatal(s string) { fmt.Println(s) os.Exit(1) } func asciicastHTML(playerPath, cast string) error { var buf bytes.Buffer err := ASCIICastHTMLTmpl.Execute(&buf, struct { PlayerPath string Cast string }{ PlayerPath: playerPath, Cast: cast, }) if err != nil { return err } fn := cast + ".html" fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.FileMode(0666)) if err != nil { return err } if _, err = fd.Write(buf.Bytes()); err != nil { os.Remove(fn) return err } return fd.Close() } 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 [URL ...]\n") flag.PrintDefaults() } flag.Parse() if len(flag.Args()) == 0 { flag.Usage() os.Exit(1) } r := netstring.NewReader(os.Stdin) size, err := r.Next() if err != nil { fatal(err.Error()) } if size > MaxExtLen { fatal("too long extension length") } data, err := io.ReadAll(r) if err != nil { fatal(err.Error()) } 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 fd, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666)) if err != nil { fatal(err.Error()) } h := blake3.New(32, nil) bfd := bufio.NewWriter(fd) mw := io.MultiWriter(bfd, h) buf := make([]byte, 1) _, err = io.CopyN(mw, r, int64(size-1)) if err != nil { goto Failed } _, err = r.Read(buf) if err != nil { goto Failed } _, err = mw.Write(buf) if err != nil { goto Failed } if (ext == ".txt" || ext == ".url") && buf[0] != '\n' { err = bfd.WriteByte('\n') if err != nil { goto Failed } } err = bfd.Flush() if err != nil { goto Failed } err = fd.Close() if err != nil { goto Failed } err = os.Rename(fn, fn[1:]) if err != nil { goto Failed } for _, u := range flag.Args() { fmt.Println(u + fn[1:]) } fmt.Println("BLAKE3-256:", hex.EncodeToString(h.Sum(nil))) if ext == ".cast" && *asciicastPath != "" { if err = asciicastHTML(*asciicastPath, fn[1:]); err != nil { goto Failed } for _, u := range flag.Args() { fmt.Println(u + fn[1:] + ".html") } } fmt.Fprintf( os.Stderr, "[%s]:%s %s %d\n", os.Getenv("TCPREMOTEIP"), os.Getenv("TCPREMOTEPORT"), fn[1:], size, ) return Failed: os.Remove(fn) fatal(err.Error()) }