]> Sergey Matveev's repositories - paster.git/blob - main.go
Netstrings are simpler than bencode
[paster.git] / main.go
1 /*
2 paster -- paste service
3 Copyright (C) 2021-2023 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package main
19
20 import (
21         "bufio"
22         "bytes"
23         "crypto/rand"
24         "crypto/sha512"
25         _ "embed"
26         "encoding/base32"
27         "encoding/hex"
28         "flag"
29         "fmt"
30         "html/template"
31         "io"
32         "os"
33
34         "go.cypherpunks.ru/netstring/v2"
35 )
36
37 const MaxExtLen = 9
38
39 var (
40         //go:embed asciicast.tmpl
41         ASCIICastHTMLTmplRaw string
42         ASCIICastHTMLTmpl    = template.Must(template.New("asciicast").Parse(
43                 ASCIICastHTMLTmplRaw,
44         ))
45 )
46
47 func fatal(s string) {
48         fmt.Println(s)
49         os.Exit(1)
50 }
51
52 func asciicastHTML(playerPath, cast string) error {
53         var buf bytes.Buffer
54         err := ASCIICastHTMLTmpl.Execute(&buf, struct {
55                 PlayerPath string
56                 Cast       string
57         }{
58                 PlayerPath: playerPath,
59                 Cast:       cast,
60         })
61         if err != nil {
62                 return err
63         }
64         fn := cast + ".html"
65         fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
66         if err != nil {
67                 return err
68         }
69         if _, err = fd.Write(buf.Bytes()); err != nil {
70                 os.Remove(fn)
71                 return err
72         }
73         return fd.Close()
74 }
75
76 func main() {
77         maxSize := flag.Uint64("max-size", 1<<20, "Maximal upload size")
78         asciicastPath := flag.String("asciicast-path", "", "Generate HTMLs for .cast asciicasts, specify \"asciinema-player-v2.6.1\"")
79         flag.Usage = func() {
80                 fmt.Fprintf(os.Stderr, "Usage: paster [options] URL [URL ...]\n")
81                 flag.PrintDefaults()
82         }
83         flag.Parse()
84         if len(flag.Args()) == 0 {
85                 flag.Usage()
86                 os.Exit(1)
87         }
88         r := netstring.NewReader(os.Stdin)
89         size, err := r.Next()
90         if err != nil {
91                 fatal(err.Error())
92         }
93         if size > MaxExtLen {
94                 fatal("too long extension length")
95         }
96         data, err := io.ReadAll(r)
97         if err != nil {
98                 fatal(err.Error())
99         }
100         var ext string
101         if len(data) == 0 {
102                 ext = ".txt"
103         } else {
104                 ext = "." + string(data)
105         }
106         size, err = r.Next()
107         if err != nil {
108                 fatal(err.Error())
109         }
110         if size == 0 {
111                 fatal("empty paste")
112         }
113         if size > *maxSize {
114                 fatal("too big")
115         }
116         rnd := make([]byte, 12)
117         if _, err = io.ReadFull(rand.Reader, rnd); err != nil {
118                 fatal(err.Error())
119         }
120         fn := "." + base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(rnd) + ext
121         fd, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
122         if err != nil {
123                 fatal(err.Error())
124         }
125         h := sha512.New()
126         bfd := bufio.NewWriter(fd)
127         mw := io.MultiWriter(bfd, h)
128         buf := make([]byte, 1)
129         _, err = io.CopyN(mw, r, int64(size-1))
130         if err != nil {
131                 goto Failed
132         }
133         _, err = r.Read(buf)
134         if err != nil {
135                 goto Failed
136         }
137         _, err = mw.Write(buf)
138         if err != nil {
139                 goto Failed
140         }
141         if (ext == ".txt" || ext == ".url") && buf[0] != '\n' {
142                 err = bfd.WriteByte('\n')
143                 if err != nil {
144                         goto Failed
145                 }
146         }
147         err = bfd.Flush()
148         if err != nil {
149                 goto Failed
150         }
151         err = fd.Close()
152         if err != nil {
153                 goto Failed
154         }
155         err = os.Rename(fn, fn[1:])
156         if err != nil {
157                 goto Failed
158         }
159         for _, u := range flag.Args() {
160                 fmt.Println(u + fn[1:])
161         }
162         fmt.Println("SHA512/2:", hex.EncodeToString(h.Sum(nil)[:sha512.Size/2]))
163         if ext == ".cast" && *asciicastPath != "" {
164                 if err = asciicastHTML(*asciicastPath, fn[1:]); err != nil {
165                         goto Failed
166                 }
167                 for _, u := range flag.Args() {
168                         fmt.Println(u + fn[1:] + ".html")
169                 }
170         }
171         fmt.Fprintf(
172                 os.Stderr,
173                 "[%s]:%s %s %d\n",
174                 os.Getenv("TCPREMOTEIP"),
175                 os.Getenv("TCPREMOTEPORT"),
176                 fn[1:], size,
177         )
178         return
179 Failed:
180         os.Remove(fn)
181         fatal(err.Error())
182 }