1 package battlemap 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "encoding/json" 7 "fmt" 8 "hash" 9 "io" 10 "net/http" 11 "path" 12 "path/filepath" 13 "strconv" 14 "strings" 15 "sync" 16 17 "vimagination.zapto.org/keystore" 18 ) 19 20 type hasher struct { 21 hash.Hash 22 } 23 24 func (h *hasher) ReadFrom(r io.Reader) (int64, error) { 25 return io.Copy(h.Hash, r) 26 } 27 28 type assetsDir struct { 29 folders 30 handler http.Handler 31 hashes map[[sha256.Size]byte][]uint64 32 sync.Once 33 } 34 35 func (a *assetsDir) Init(b *Battlemap, links links) error { 36 var ( 37 location keystore.String 38 locname string 39 lm linkManager 40 ) 41 switch a.fileType { 42 case fileTypeImage: 43 locname = "ImageAssetsDir" 44 lm = links.images 45 case fileTypeAudio: 46 locname = "AudioAssetsDir" 47 lm = links.audio 48 default: 49 return ErrInvalidFileType 50 } 51 err := b.config.Get(locname, &location) 52 if err != nil { 53 return fmt.Errorf("error getting asset data directory: %w", err) 54 } 55 l := filepath.Join(b.config.BaseDir, string(location)) 56 assetStore, err := keystore.NewFileStore(l, l, keystore.NoMangle) 57 if err != nil { 58 return fmt.Errorf("error creating asset meta store: %w", err) 59 } 60 a.handler = http.FileServer(http.Dir(l)) 61 return a.folders.Init(b, assetStore, lm) 62 } 63 64 func (a *assetsDir) makeHashMap() { 65 a.hashes = make(map[[sha256.Size]byte][]uint64) 66 h := hasher{Hash: sha256.New()} 67 for _, key := range a.Keys() { 68 id, err := strconv.ParseUint(key, 10, 64) 69 if err != nil { 70 continue 71 } 72 var hash [sha256.Size]byte 73 if err := a.Get(key, &h); err != nil { 74 continue 75 } 76 h.Sum(hash[:0]) 77 a.hashes[hash] = append(a.hashes[hash], id) 78 h.Reset() 79 } 80 } 81 82 func (a *assetsDir) ServeHTTP(w http.ResponseWriter, r *http.Request) { 83 switch r.Method { 84 case http.MethodGet, http.MethodHead: 85 if r.URL.Path == folderMetadata { 86 http.NotFound(w, r) 87 } else { 88 a.handler.ServeHTTP(w, r) 89 } 90 case http.MethodPost: 91 if !a.auth.IsAdmin(r) { 92 http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 93 } else if r.URL.Path != "" { 94 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 95 } else if err := a.Post(w, r); err != nil { 96 http.Error(w, err.Error(), http.StatusBadRequest) 97 } 98 default: 99 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 100 } 101 } 102 103 func (a *assetsDir) Post(w http.ResponseWriter, r *http.Request) error { 104 a.Once.Do(a.makeHashMap) 105 m, err := r.MultipartReader() 106 defer r.Body.Close() 107 if err != nil { 108 return err 109 } 110 var ( 111 added []idName 112 gft getFileType 113 hash [sha256.Size]byte 114 folder map[string]uint64 115 ) 116 h := sha256.New() 117 if err := r.ParseForm(); err != nil { 118 return fmt.Errorf("error processing form data: %w", err) 119 } 120 folderPath := path.Clean("/" + r.Form.Get("path")) 121 if f := a.getFolder(folderPath); f != nil { 122 folder = f.Items 123 folderPath += "/" 124 } else { 125 folderPath = "/" 126 folder = a.root.Items 127 } 128 for { 129 p, err := m.NextPart() 130 if err != nil { 131 if err == io.EOF { 132 break 133 } 134 return err 135 } 136 gft.Type = fileTypeUnknown 137 bufLen, err := gft.ReadFrom(p) 138 if err != nil { 139 return err 140 } 141 if gft.Type != a.fileType { 142 continue 143 } 144 a.mu.Lock() 145 a.lastID++ 146 id := a.lastID 147 a.mu.Unlock() 148 idStr := strconv.FormatUint(id, 10) 149 b := bufReaderWriterTo{gft.Buffer[:bufLen], p, h, 0} 150 if err = a.Set(idStr, &b); err != nil { 151 return err 152 } 153 h.Sum(hash[:0]) 154 h.Reset() 155 if ids, ok := a.hashes[hash]; ok { 156 match := false 157 for _, fid := range ids { 158 fidStr := strconv.FormatUint(fid, 10) 159 fs, err := a.Stat(fidStr) 160 if err != nil { 161 continue 162 } 163 if fs.Size() == b.size { 164 a.Get(idStr, readerFromFunc(func(ar io.Reader) { 165 a.Get(fidStr, readerFromFunc(func(br io.Reader) { 166 abuf, bbuf := make([]byte, 32768), make([]byte, 32768) 167 for { 168 n, erra := io.ReadFull(ar, abuf) 169 m, errb := io.ReadFull(br, bbuf) 170 if !bytes.Equal(abuf[:n], bbuf[:m]) { 171 return 172 } 173 if erra == io.EOF || erra == io.ErrUnexpectedEOF { 174 match = erra == errb 175 return 176 } else if erra != nil || errb != nil { 177 return 178 } 179 } 180 })) 181 })) 182 if match { 183 id = fid 184 a.mu.Lock() 185 a.lastID-- 186 a.mu.Unlock() 187 a.Remove(idStr) 188 break 189 } 190 } 191 } 192 if !match { 193 a.hashes[hash] = append(ids, id) 194 } 195 } else { 196 a.hashes[hash] = []uint64{id} 197 } 198 filename := p.FileName() 199 if filename == "" || strings.ContainsAny(filename, invalidFilenameChars) { 200 filename = idStr 201 } 202 newName := addItemTo(folder, filename, id) 203 added = append(added, idName{id, newName}) 204 } 205 if len(added) == 0 { 206 w.WriteHeader(http.StatusNoContent) 207 return nil 208 } 209 a.mu.Lock() 210 a.saveFolders() 211 a.mu.Unlock() 212 buf := fmt.Appendf([]byte{}, "[{\"id\":%d,\"name\":%q}", added[0].ID, folderPath+added[0].Name) 213 for _, id := range added[1:] { 214 buf = fmt.Appendf(buf, ",{\"id\":%d,\"name\":%q}", id.ID, folderPath+id.Name) 215 } 216 buf = append(buf, ']') 217 bid := broadcastImageItemAdd 218 if a.fileType == fileTypeAudio { 219 bid-- 220 } 221 a.socket.broadcastAdminChange(bid, json.RawMessage(buf), SocketIDFromRequest(r)) 222 w.Header().Set(contentType, "application/json") 223 w.Header().Set("Content-Length", strconv.FormatUint(uint64(len(buf)), 10)) 224 w.Write(buf) 225 return nil 226 } 227 228 type bufReaderWriterTo struct { 229 Buf []byte 230 Reader io.Reader 231 hash hash.Hash 232 size int64 233 } 234 235 func (b *bufReaderWriterTo) WriteTo(w io.Writer) (int64, error) { 236 b.hash.Reset() 237 b.hash.Write(b.Buf) 238 n, err := w.Write(b.Buf) 239 if err != nil { 240 return int64(n), err 241 } 242 m, err := io.Copy(io.MultiWriter(w, b.hash), b.Reader) 243 b.size = int64(n) + m 244 return int64(n) + m, err 245 } 246 247 type readerFromFunc func(io.Reader) 248 249 func (rff readerFromFunc) ReadFrom(r io.Reader) (int64, error) { 250 rff(r) 251 return 0, nil 252 } 253 254 const ( 255 invalidFilenameChars = "\x00\r\n\\/" 256 contentType = "Content-Type" 257 ) 258