github.com/readium/readium-lcp-server@v0.0.0-20240509124024-799e77a0bbd6/lcpserver/api/store.go (about) 1 // Copyright 2020 Readium Foundation. All rights reserved. 2 // Use of this source code is governed by a BSD-style license 3 // that can be found in the LICENSE file exposed on Github (readium) in the project repository. 4 5 package apilcp 6 7 import ( 8 "crypto/tls" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "net/http" 15 "net/url" 16 "os" 17 "strconv" 18 19 "github.com/gorilla/mux" 20 21 "github.com/readium/readium-lcp-server/api" 22 "github.com/readium/readium-lcp-server/index" 23 "github.com/readium/readium-lcp-server/license" 24 "github.com/readium/readium-lcp-server/logging" 25 "github.com/readium/readium-lcp-server/pack" 26 "github.com/readium/readium-lcp-server/problem" 27 "github.com/readium/readium-lcp-server/storage" 28 ) 29 30 // Server groups functions used by the lcp server 31 type Server interface { 32 Store() storage.Store 33 Index() index.Index 34 Licenses() license.Store 35 Certificate() *tls.Certificate 36 Source() *pack.ManualSource 37 } 38 39 // Encrypted is used for communication with the License Server 40 type Encrypted struct { 41 ContentID string `json:"content-id"` 42 ContentKey []byte `json:"content-encryption-key"` 43 StorageMode int `json:"storage-mode"` 44 Output string `json:"protected-content-location"` 45 FileName string `json:"protected-content-disposition"` 46 Size int64 `json:"protected-content-length"` 47 Checksum string `json:"protected-content-sha256"` 48 ContentType string `json:"protected-content-type,omitempty"` 49 } 50 51 const ( 52 Storage_none = 0 53 Storage_s3 = 1 54 Storage_fs = 2 55 ) 56 57 func writeRequestFileToTemp(r io.Reader) (int64, *os.File, error) { 58 dir := os.TempDir() 59 file, err := ioutil.TempFile(dir, "readium-lcp") 60 if err != nil { 61 return 0, file, err 62 } 63 64 n, err := io.Copy(file, r) 65 66 // Rewind to the beginning of the file 67 file.Seek(0, 0) 68 69 return n, file, err 70 } 71 72 func cleanupTempFile(f *os.File) { 73 if f == nil { 74 return 75 } 76 f.Close() 77 os.Remove(f.Name()) 78 } 79 80 // StoreContent stores content passed through the request body into the storage. 81 // The content name is given in the url (name) 82 // A temporary file is created, then deleted after the content has been stored. 83 // This function is using an async task. 84 func StoreContent(w http.ResponseWriter, r *http.Request, s Server) { 85 86 vars := mux.Vars(r) 87 88 size, f, err := writeRequestFileToTemp(r.Body) 89 if err != nil { 90 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 91 return 92 } 93 94 defer cleanupTempFile(f) 95 96 t := pack.NewTask(vars["name"], f, size) 97 result := s.Source().Post(t) 98 99 if result.Error != nil { 100 problem.Error(w, r, problem.Problem{Detail: result.Error.Error()}, http.StatusBadRequest) 101 return 102 } 103 104 // must come *after* w.Header().Add()/Set(), but before w.Write() 105 w.WriteHeader(http.StatusCreated) 106 107 json.NewEncoder(w).Encode(result.ID) 108 } 109 110 // AddContent adds content to the storage 111 // lcp spec : store data resulting from an external encryption 112 // PUT method with PAYLOAD : encrypted publication in json format 113 // This method adds an encrypted file to a store 114 // and adds the corresponding decryption key to the database. 115 // The content_id is taken from the url. 116 // The input file is then deleted. 117 func AddContent(w http.ResponseWriter, r *http.Request, s Server) { 118 119 // parse the json payload 120 vars := mux.Vars(r) 121 decoder := json.NewDecoder(r.Body) 122 var encrypted Encrypted 123 err := decoder.Decode(&encrypted) 124 if err != nil { 125 http.Error(w, err.Error(), http.StatusBadRequest) 126 } 127 // get the content ID in the url 128 contentID := vars["content_id"] 129 if contentID == "" { 130 problem.Error(w, r, problem.Problem{Detail: "The content id must be set in the url"}, http.StatusBadRequest) 131 return 132 } 133 134 // add a log 135 logging.Print("Add publication " + contentID) 136 137 // if the encrypted publication has not been already stored by lcpencrypt 138 if encrypted.StorageMode == Storage_none { 139 140 // open the encrypted file, use its full path 141 file, err := getAndOpenFile(encrypted.Output) 142 if err != nil { 143 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 144 return 145 } 146 // the input file will be deleted when the function returns 147 defer cleanupTempFile(file) 148 149 // add the file to the storage, named by contentID, without file extension 150 _, err = s.Store().Add(contentID, file) 151 if err != nil { 152 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 153 return 154 } 155 } 156 157 // insert a row in the database if the content id does not already exist 158 // or update the database with a new content key and file location if the content id already exists 159 var c index.Content 160 c, err = s.Index().Get(contentID) 161 c.EncryptionKey = encrypted.ContentKey 162 // the Location field contains either the file name (useful during download) 163 // or the storage URL of the encrypted, depending the storage mode. 164 if encrypted.StorageMode != Storage_none { 165 c.Location = encrypted.Output 166 } else { 167 c.Location = encrypted.FileName 168 } 169 c.Length = encrypted.Size 170 c.Sha256 = encrypted.Checksum 171 c.Type = encrypted.ContentType 172 173 code := http.StatusCreated 174 if err == index.ErrNotFound { //insert into database 175 c.ID = contentID 176 err = s.Index().Add(c) 177 } else { //update the encryption key for c.ID = encrypted.ContentID 178 err = s.Index().Update(c) 179 code = http.StatusOK 180 } 181 if err != nil { //if db not updated 182 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError) 183 return 184 } 185 186 // set the response http code 187 w.WriteHeader(code) 188 } 189 190 // ListContents lists the content in the storage index 191 func ListContents(w http.ResponseWriter, r *http.Request, s Server) { 192 193 fn := s.Index().List() 194 contents := make([]index.Content, 0) 195 196 for it, err := fn(); err == nil; it, err = fn() { 197 contents = append(contents, it) 198 } 199 200 // add a log 201 logging.Print("List publications, total " + strconv.Itoa(len(contents))) 202 203 w.Header().Set("Content-Type", api.ContentType_JSON) 204 enc := json.NewEncoder(w) 205 err := enc.Encode(contents) 206 if err != nil { 207 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 208 return 209 } 210 211 } 212 213 // GetContent fetches and returns an encrypted content file 214 // selected by it content id (uuid) 215 // This should be called only if the License Server stores the file. 216 // If it is not the case, the file should be fetched from a standard web server 217 func GetContent(w http.ResponseWriter, r *http.Request, s Server) { 218 219 // get the content id from the calling url 220 vars := mux.Vars(r) 221 contentID := vars["content_id"] 222 223 // add a log 224 logging.Print("Fetch content " + contentID) 225 226 content, err := s.Index().Get(contentID) 227 if err != nil { //item probably not found 228 if err == index.ErrNotFound { 229 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusNotFound) 230 } else { 231 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 232 } 233 return 234 } 235 236 // check the existence of the file 237 item, err := s.Store().Get(contentID) 238 if err != nil { //item probably not found 239 if err == storage.ErrNotFound { 240 problem.Error(w, r, problem.Problem{Detail: "Storage:" + err.Error(), Instance: contentID}, http.StatusNotFound) 241 } else { 242 problem.Error(w, r, problem.Problem{Detail: "Storage:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 243 } 244 return 245 } 246 // opens the file 247 contentReadCloser, err := item.Contents() 248 if err != nil { //file probably not found 249 problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: contentID}, http.StatusBadRequest) 250 return 251 } 252 253 defer contentReadCloser.Close() 254 255 // set headers 256 // If this function is called for a file stored by the encrypting tool, we have to provide a sensible 257 // Content-Disposition header, to be used as file name after download. 258 hasPubLink, err := isURL(content.Location) 259 if err != nil { 260 problem.Error(w, r, problem.Problem{Detail: "Content Location:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 261 return 262 } 263 var filename string 264 if hasPubLink { 265 // we have not stored the original file name, therefore we use the content id. 266 filename = content.ID 267 } else { 268 // in the initial version of the server, the filename was in this field. 269 filename = content.Location 270 } 271 w.Header().Set("Content-Disposition", "attachment; filename="+filename) 272 w.Header().Set("Content-Type", content.Type) 273 w.Header().Set("Content-Length", fmt.Sprintf("%d", content.Length)) 274 275 // returns the content of the file to the caller 276 io.Copy(w, contentReadCloser) 277 } 278 279 // DeleteContent deletes a record 280 func DeleteContent(w http.ResponseWriter, r *http.Request, s Server) { 281 282 // get the content id from the calling url 283 vars := mux.Vars(r) 284 contentID := vars["content_id"] 285 286 // add a log 287 logging.Print("Delete publication " + contentID) 288 289 err := s.Index().Delete(contentID) 290 if err != nil { //item probably not found 291 if err == index.ErrNotFound { 292 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusNotFound) 293 } else { 294 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 295 } 296 return 297 } 298 // set the response http code 299 w.WriteHeader(http.StatusOK) 300 301 } 302 303 // getAndOpenFile opens a file from a path, or downloads then opens it if its location is a URL 304 func getAndOpenFile(filePathOrURL string) (*os.File, error) { 305 306 isURL, err := isURL(filePathOrURL) 307 if err != nil { 308 return nil, err 309 } 310 311 if isURL { 312 return downloadAndOpenFile(filePathOrURL) 313 } 314 315 return os.Open(filePathOrURL) 316 } 317 318 func downloadAndOpenFile(url string) (*os.File, error) { 319 file, _ := ioutil.TempFile("", "") 320 fileName := file.Name() 321 322 err := downloadFile(url, fileName) 323 324 if err != nil { 325 return nil, err 326 } 327 328 return os.Open(fileName) 329 } 330 331 func isURL(filePathOrURL string) (bool, error) { 332 url, err := url.Parse(filePathOrURL) 333 if err != nil { 334 return false, errors.New("error parsing input string") 335 } 336 return url.Scheme == "http" || url.Scheme == "https", nil 337 } 338 339 func downloadFile(url string, targetFilePath string) error { 340 out, err := os.Create(targetFilePath) 341 if err != nil { 342 return err 343 } 344 defer out.Close() 345 346 resp, err := http.Get(url) 347 if err != nil { 348 return err 349 } 350 351 if resp.StatusCode >= 300 { 352 return fmt.Errorf("HTTP response: %d %s when downloading %s", resp.StatusCode, resp.Status, url) 353 } 354 355 defer resp.Body.Close() 356 357 _, err = io.Copy(out, resp.Body) 358 if err != nil { 359 return err 360 } 361 362 return nil 363 }