furl - main.go
1 package main
2
3 import (
4 "bufio"
5 "context"
6 _ "embed"
7 "flag"
8 "fmt"
9 "html/template"
10 "io"
11 "net"
12 "net/http"
13 "os"
14 "os/signal"
15 "path"
16 "strings"
17
18 "vimagination.zapto.org/furl"
19 )
20
21 //go:embed index.tmpl
22 var index string
23
24 func main() {
25 if err := run(); err != nil {
26 fmt.Fprintln(os.Stderr, err)
27 os.Exit(1)
28 }
29 }
30
31 func keyValidator(key string) bool {
32 for _, c := range key {
33 if !strings.ContainsRune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", c) {
34 return false
35 }
36 }
37 return true
38 }
39
40 type tmplVars struct {
41 Success, URL, URLError, Key, KeyError string
42 NotFound bool
43 }
44
45 func run() error {
46 tmpl := template.Must(template.New("").Parse(index))
47 file := flag.String("f", "", "filename to store key:url map data")
48 port := flag.Int("p", 8080, "port for server to listen on")
49 serverURL := flag.String("s", "", "base server url. e.g. http://furl.com/")
50 flag.Parse()
51
52 furlParams := []furl.Option{
53 furl.URLValidator(furl.HTTPURL),
54 furl.KeyValidator(keyValidator),
55 furl.Index(func(w http.ResponseWriter, r *http.Request, code int, data string) {
56 if r.Method == http.MethodGet {
57 isRoot := r.URL.Path == "/" || r.URL.Path == ""
58 if code == http.StatusUnprocessableEntity && !isRoot {
59 http.Redirect(w, r, "/", http.StatusFound)
60 return
61 }
62 var tv tmplVars
63 if code == http.StatusNotFound && !isRoot {
64 w.WriteHeader(code)
65 tv.NotFound = true
66 tv.Key = path.Base("/" + r.URL.Path)
67 }
68 tmpl.Execute(w, tv)
69 } else if r.Method == http.MethodPost {
70 tv := tmplVars{
71 URL: r.PostForm.Get("url"),
72 }
73 switch code {
74 case http.StatusOK:
75 tv.Key = data
76 tv.Success = *serverURL + data
77 case http.StatusBadRequest:
78 tv.URLError = "Invalid URL"
79 case http.StatusUnprocessableEntity:
80 tv.KeyError = "Invalid Alias"
81 case http.StatusMethodNotAllowed:
82 tv.KeyError = "Alias Exists"
83 }
84 tmpl.Execute(w, tv)
85 }
86 }),
87 }
88
89 if *file != "" { // if we're loading a file-back store
90 f, err := os.OpenFile(*file, os.O_RDWR|os.O_CREATE, 0o666)
91 if err != nil {
92 return fmt.Errorf("error opening database file (%s): %w", *file, err)
93 }
94 defer f.Close()
95 data := make(map[string]string)
96 b := bufio.NewReader(f)
97 var length [2]byte
98
99 /*
100 Each key:url pair is stored sequentially and according to the following format:
101
102 struct {
103 KeyLength uint16
104 Key [KeyLength]byte
105 URLLength uint16
106 URL [URLLength]byte
107 }
108
109 The uint16s are store in LittleEndian format.
110 */
111
112 for {
113 if _, err := io.ReadFull(b, length[:]); err != nil && err != io.EOF {
114 return fmt.Errorf("error reading key length: %w", err)
115 }
116 keyLength := int(length[0]) | (int(length[1]) << 8)
117 if keyLength == 0 {
118 break
119 }
120 key := make([]byte, keyLength)
121 if _, err = io.ReadFull(b, key); err != nil {
122 return fmt.Errorf("error reading key: %w", err)
123 }
124 if _, err := io.ReadFull(b, length[:]); err != nil && err != io.EOF {
125 return fmt.Errorf("error reading url length: %w", err)
126 }
127 if urlLength := int(length[0]) | (int(length[1]) << 8); urlLength > 0 {
128 url := make([]byte, urlLength)
129 if _, err = io.ReadFull(b, url); err != nil {
130 return fmt.Errorf("error reading url: %w", err)
131 }
132 data[string(key)] = string(url)
133 }
134 length[0] = 0
135 length[1] = 0
136 }
137 w := bufio.NewWriter(f)
138 furlParams = append(furlParams, furl.SetStore(furl.NewStore(furl.Data(data), furl.Save(func(key, url string) {
139 length[0] = byte(len(key))
140 length[1] = byte(len(key) >> 8)
141 if _, err := w.Write(length[:]); err != nil {
142 panic(fmt.Errorf("error while writing key length: %w", err))
143 }
144 if _, err := w.WriteString(key); err != nil {
145 panic(fmt.Errorf("error while writing key: %w", err))
146 }
147 length[0] = byte(len(url))
148 length[1] = byte(len(url) >> 8)
149 if _, err := w.Write(length[:]); err != nil {
150 panic(fmt.Errorf("error while writing url length: %w", err))
151 }
152 if _, err := w.WriteString(url); err != nil {
153 panic(fmt.Errorf("error while writing url: %w", err))
154 }
155 if err := w.Flush(); err != nil {
156 panic(fmt.Errorf("error while flushing buffers: %w", err))
157 }
158 if err := f.Sync(); err != nil {
159 panic(fmt.Errorf("error while syncing file: %w", err))
160 }
161 }))))
162 }
163 l, err := net.ListenTCP("tcp", &net.TCPAddr{Port: *port})
164 if err != nil {
165 return fmt.Errorf("error listening on port %d: %w", *port, err)
166 }
167
168 server := &http.Server{
169 Handler: furl.New(furlParams...),
170 }
171
172 go server.Serve(l)
173
174 // wait for SIGINT
175
176 sc := make(chan os.Signal, 1)
177 signal.Notify(sc, os.Interrupt)
178 <-sc
179 signal.Stop(sc)
180 close(sc)
181
182 return server.Shutdown(context.Background())
183 }
184