1 package main 2 3 import ( 4 "crypto/sha256" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io/fs" 9 "net/http" 10 "os" 11 "strings" 12 "sync" 13 14 "vimagination.zapto.org/httpfile" 15 ) 16 17 const defaultConfig = `{"allowUnsigned":false,"keys":[]}` 18 19 type Options struct { 20 MarkdownHTML json.RawMessage `json:"markdownHTML,omitempty"` 21 Embed json.RawMessage `json:"embed,omitempty"` 22 } 23 24 type Config struct { 25 AllowUnsigned bool `json:"allowUnsigned"` 26 Options 27 Keys []struct { 28 Options 29 Name string `json:"name"` 30 Hash string `json:"hash"` 31 Key struct { 32 Alg string `json:"alg"` 33 CRV string `json:"crv"` 34 Ext bool `json:"ext"` 35 KeyOps []string `json:"key_ops"` 36 KTY string `json:"kty"` 37 X string `json:"x"` 38 Y string `json:"y"` 39 } `json:"key"` 40 } `json:"keys"` 41 } 42 43 type Error struct { 44 Code int 45 error 46 } 47 48 type ConfigHandler struct { 49 pass string 50 opts string 51 52 file *httpfile.File 53 http.ServeMux 54 55 mu sync.Mutex 56 path string 57 } 58 59 func NewConfigHandler(path, pass string) (*ConfigHandler, error) { 60 if path == "" { 61 return nil, ErrConfigRequired 62 } 63 64 data, err := os.ReadFile(path) 65 if errors.Is(err, fs.ErrNotExist) { 66 data = []byte(defaultConfig) 67 } else if err != nil { 68 return nil, fmt.Errorf("error reading config: %w", err) 69 } 70 71 c := &ConfigHandler{ 72 pass: pass, 73 opts: "OPTIONS, GET, HEAD", 74 path: path, 75 file: httpfile.NewWithData("config.json", data), 76 } 77 78 c.Handle(http.MethodGet+" /", c.file) 79 c.Handle(http.MethodOptions+" /", http.HandlerFunc(c.Options)) 80 81 if pass != "" { 82 c.Handle(http.MethodPost+" /", http.HandlerFunc(c.Post)) 83 84 c.pass = strings.ToUpper(c.pass) 85 c.opts = "OPTIONS, GET, HEAD, POST" 86 } 87 88 return c, nil 89 } 90 91 func (c *ConfigHandler) Post(w http.ResponseWriter, r *http.Request) { 92 if err := c.post(w, r); err != nil { 93 var errc Error 94 95 if errors.As(err, &errc) { 96 http.Error(w, errc.Error(), errc.Code) 97 98 return 99 } 100 101 http.Error(w, err.Error(), http.StatusInternalServerError) 102 } 103 } 104 105 func (c *ConfigHandler) post(w http.ResponseWriter, r *http.Request) error { 106 _, password, ok := r.BasicAuth() 107 if !ok { 108 return ErrPasswordRequiredCode 109 } 110 111 if c.pass != fmt.Sprintf("%X", sha256.Sum256([]byte(password))) { 112 return ErrInvalidPasswordCode 113 } 114 115 var conf Config 116 117 if err := json.NewDecoder(r.Body).Decode(&conf); err != nil { 118 return Error{ 119 Code: http.StatusBadRequest, 120 error: err, 121 } 122 } 123 124 f := c.file.Create() 125 126 if err := json.NewEncoder(f).Encode(c); err != nil { 127 return err 128 } 129 130 f.Close() 131 132 go c.saveConfig() 133 134 w.WriteHeader(http.StatusNoContent) 135 136 return nil 137 } 138 139 func (c *ConfigHandler) saveConfig() { 140 c.mu.Lock() 141 c.mu.Unlock() 142 143 f, err := os.Create(c.path) 144 if err != nil { 145 return 146 } 147 defer f.Close() 148 149 c.file.WriteTo(f) 150 } 151 152 func (c *ConfigHandler) Options(w http.ResponseWriter, _ *http.Request) { 153 w.Header().Set("Allow", c.opts) 154 w.WriteHeader(http.StatusNoContent) 155 } 156 157 var ( 158 ErrConfigRequired = errors.New("config location is required") 159 ErrPasswordRequired = errors.New("password required") 160 ErrInvalidPassword = errors.New("invalid password") 161 162 ErrPasswordRequiredCode = Error{ 163 Code: http.StatusForbidden, 164 error: ErrPasswordRequired, 165 } 166 ErrInvalidPasswordCode = Error{ 167 Code: http.StatusForbidden, 168 error: ErrInvalidPassword, 169 } 170 ) 171