1 package battlemap 2 3 import ( 4 "compress/gzip" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "path/filepath" 10 "strings" 11 "sync" 12 "time" 13 14 "vimagination.zapto.org/httpdir" 15 "vimagination.zapto.org/httpgzip" 16 "vimagination.zapto.org/keystore" 17 "vimagination.zapto.org/memio" 18 "vimagination.zapto.org/rwcount" 19 ) 20 21 const pluginConfigExt = ".config" 22 23 type plugin struct { 24 Enabled bool `json:"enabled"` 25 Data map[string]keystoreData `json:"data"` 26 } 27 28 func (p *plugin) ReadFrom(r io.Reader) (int64, error) { 29 rc := rwcount.Reader{Reader: r} 30 err := json.NewDecoder(&rc).Decode(&p) 31 return rc.Count, err 32 } 33 34 func (p *plugin) WriteTo(w io.Writer) (int64, error) { 35 wc := rwcount.Writer{Writer: w} 36 p.WriteToUser(&wc, true) 37 return wc.Count, wc.Err 38 } 39 40 var ( 41 pluginStart = []byte{'{', '"', 'e', 'n', 'a', 'b', 'l', 'e', 'd', '"', ':'} 42 pluginDisabled = []byte{'{', '"', 'e', 'n', 'a', 'b', 'l', 'e', 'd', '"', ':', 'f', 'a', 'l', 's', 'e', ',', '"', 'd', 'a', 't', 'a', '"', ':', '{', '}', '}'} 43 pluginTrue = []byte{'t', 'r', 'u', 'e'} 44 pluginFalse = []byte{'f', 'a', 'l', 's', 'e'} 45 pluginMid = []byte{',', '"', 'd', 'a', 't', 'a', '"', ':', '{'} 46 pluginComma = []byte{','} 47 pluginDataStart = []byte{':', '{', '"', 'u', 's', 'e', 'r', '"', ':'} 48 pluginDataMid = []byte{',', '"', 'd', 'a', 't', 'a', '"', ':'} 49 pluginEnd = []byte{'}', '}'} 50 ) 51 52 func (p *plugin) WriteToUser(w io.Writer, isAdmin bool) { 53 w.Write(pluginStart) 54 if p.Enabled { 55 w.Write(pluginTrue) 56 } else { 57 w.Write(pluginFalse) 58 } 59 w.Write(pluginMid) 60 first := true 61 for key, val := range p.Data { 62 if isAdmin || val.User { 63 if first { 64 first = false 65 } else { 66 w.Write(pluginComma) 67 } 68 fmt.Fprintf(w, "%q", key) // TODO: need to replace with JSON specific code 69 w.Write(pluginDataStart) 70 if val.User { 71 w.Write(pluginTrue) 72 } else { 73 w.Write(pluginFalse) 74 } 75 w.Write(pluginDataMid) 76 w.Write(val.Data) 77 w.Write(pluginEnd[:1]) 78 } 79 } 80 w.Write(pluginEnd) 81 } 82 83 type pluginsDir struct { 84 *Battlemap 85 http.Handler 86 plugins map[string]*plugin 87 88 *keystore.FileStore 89 90 mu sync.RWMutex 91 json json.RawMessage 92 userJSON json.RawMessage 93 } 94 95 func (p *pluginsDir) Init(b *Battlemap, links links) error { 96 var pd keystore.String 97 err := b.config.Get("PluginsDir", &pd) 98 if err != nil { 99 return fmt.Errorf("error retrieving plugins location: %w", err) 100 } 101 base := filepath.Join(b.config.BaseDir, string(pd)) 102 p.FileStore, err = keystore.NewFileStore(base, base, keystore.NoMangle) 103 if err != nil { 104 return fmt.Errorf("error creating plugins keystore: %w", err) 105 } 106 p.plugins = make(map[string]*plugin) 107 hd := httpdir.New(time.Now()) 108 g, _ := gzip.NewWriterLevel(nil, gzip.BestCompression) 109 for _, file := range p.FileStore.Keys() { 110 if !strings.HasSuffix(file, ".js") { 111 continue 112 } 113 st, err := p.FileStore.Stat(file) 114 if err != nil { 115 return fmt.Errorf("error stat'ing plugin (%s): %w", file, err) 116 } 117 buf := make(memio.Buffer, 0, st.Size()) 118 if err := p.FileStore.Get(file, &buf); err != nil { 119 return fmt.Errorf("error reading plugin: %w", err) 120 } 121 var gBuf memio.Buffer 122 g.Reset(&gBuf) 123 g.Write(buf) 124 g.Close() 125 ft := st.ModTime() 126 hd.Create(file, httpdir.FileBytes(buf, ft)) 127 hd.Create(file+".gz", httpdir.FileBytes(gBuf, ft)) 128 s := file + pluginConfigExt 129 if p.FileStore.Exists(s) { 130 var plugin plugin 131 if err := p.FileStore.Get(s, &plugin); err != nil { 132 return fmt.Errorf("error reading plugin: %w", err) 133 } 134 p.plugins[file] = &plugin 135 } else { 136 p.plugins[file] = &plugin{Data: make(map[string]keystoreData)} 137 } 138 } 139 p.Battlemap = b 140 p.Handler = httpgzip.FileServer(hd) 141 p.updateJSON() 142 return nil 143 } 144 145 func (p *pluginsDir) updateJSON() { 146 wa := append(memio.Buffer{}, '{') 147 wu := append(memio.Buffer{}, '{') 148 first := true 149 for id, plugin := range p.plugins { 150 if first { 151 first = false 152 } else { 153 wa = append(wa, ',') 154 wu = append(wu, ',') 155 } 156 wa = append(appendString(wa, id), ':') 157 wu = append(appendString(wu, id), ':') 158 if plugin.Enabled { 159 plugin.WriteToUser(&wa, true) 160 plugin.WriteToUser(&wu, false) 161 } else { 162 wa = append(wa, pluginDisabled...) 163 wu = append(wu, pluginDisabled...) 164 } 165 } 166 wa = append(wa, '}') 167 wu = append(wu, '}') 168 p.json = json.RawMessage(wa) 169 p.userJSON = json.RawMessage(wu) 170 } 171 172 func (p *pluginsDir) ServeHTTP(w http.ResponseWriter, r *http.Request) { 173 p.Handler.ServeHTTP(w, r) 174 } 175 176 func (p *pluginsDir) RPCData(cd ConnData, method string, data json.RawMessage) (interface{}, error) { 177 switch method { 178 case "list": 179 var j json.RawMessage 180 p.mu.RLock() 181 if cd.IsAdmin() { 182 j = p.json 183 } else { 184 j = p.userJSON 185 } 186 p.mu.RUnlock() 187 return j, nil 188 case "set": 189 var toSet struct { 190 ID string `json:"id"` 191 Setting map[string]keystoreData `json:"setting"` 192 Removing []string `json:"removing"` 193 } 194 if err := json.Unmarshal(data, &toSet); err != nil { 195 return nil, err 196 } 197 if len(toSet.Setting) == 0 && len(toSet.Removing) == 0 { 198 return nil, nil 199 } 200 p.mu.Lock() 201 plugin, ok := p.plugins[toSet.ID] 202 if !ok { 203 p.mu.Unlock() 204 return nil, ErrUnknownPlugin 205 } 206 p.socket.broadcastAdminChange(broadcastPluginSettingChange, data, cd.ID) 207 buf := appendString(append(data[:0], "{\"id\":"...), toSet.ID) 208 buf = append(buf, ",\"setting\":{"...) 209 var userRemoves []string 210 send := false 211 for key, val := range toSet.Setting { 212 if val.User { 213 buf = append(append(append(appendString(append(buf, ','), key), ":{\"user\":true,\"data\":"...), val.Data...), '}') 214 send = true 215 } else if mv, ok := plugin.Data[key]; ok && mv.User { 216 userRemoves = append(userRemoves, key) 217 send = true 218 } 219 plugin.Data[key] = val 220 } 221 buf = append(buf, "},\"removing\":["...) 222 first := true 223 for _, key := range toSet.Removing { 224 val, ok := plugin.Data[key] 225 if !ok { 226 continue 227 } 228 if val.User { 229 send = true 230 if !first { 231 buf = append(buf, ',') 232 } else { 233 first = false 234 } 235 buf = appendString(buf, key) 236 } 237 delete(plugin.Data, key) 238 } 239 if err := p.FileStore.Set(toSet.ID+pluginConfigExt, plugin); err != nil { 240 return nil, err 241 } 242 for _, key := range userRemoves { 243 if !first { 244 buf = append(buf, ',') 245 } else { 246 first = false 247 } 248 buf = appendString(buf, key) 249 } 250 if send { 251 buf = append(buf, ']', '}') 252 cd.CurrentMap = 0 253 p.socket.broadcastMapChange(cd, broadcastPluginSettingChange, buf, userNotAdmin) 254 } 255 p.updateJSON() 256 p.mu.Unlock() 257 case "enable", "disable": 258 var filename string 259 if err := json.Unmarshal(data, &filename); err != nil { 260 return nil, err 261 } 262 p.mu.Lock() 263 plugin, ok := p.plugins[filename] 264 if !ok { 265 p.mu.Unlock() 266 return nil, ErrUnknownPlugin 267 } 268 plugin.Enabled = method == "enable" 269 if err := p.FileStore.Set(filename+pluginConfigExt, plugin); err != nil { 270 return nil, err 271 } 272 cd.CurrentMap = 0 273 p.socket.broadcastMapChange(cd, broadcastPluginChange, json.RawMessage{'0'}, userAny) 274 p.updateJSON() 275 p.mu.Unlock() 276 default: 277 return nil, ErrUnknownMethod 278 } 279 return nil, nil 280 } 281