github.com/readium/readium-lcp-server@v0.0.0-20240101192032-6e95190e99f1/frontend/webpurchase/webpurchase.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 webpurchase 6 7 import ( 8 "bytes" 9 "database/sql" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "log" 14 "net/http" 15 "time" 16 17 "github.com/readium/readium-lcp-server/api" 18 "github.com/readium/readium-lcp-server/config" 19 "github.com/readium/readium-lcp-server/frontend/webpublication" 20 "github.com/readium/readium-lcp-server/frontend/webuser" 21 "github.com/readium/readium-lcp-server/license" 22 licensestatuses "github.com/readium/readium-lcp-server/license_statuses" 23 uuid "github.com/satori/go.uuid" 24 ) 25 26 //ErrNotFound Error is thrown when a purchase is not found 27 var ErrNotFound = errors.New("purchase not found") 28 29 //ErrNoChange is thrown when an update action does not change any rows (not found) 30 var ErrNoChange = errors.New("no lines were updated") 31 32 // WebPurchase defines possible interactions with the db 33 type WebPurchase interface { 34 Get(id int64) (Purchase, error) 35 GenerateOrGetLicense(purchase Purchase) (license.License, error) 36 GetPartialLicense(purchase Purchase) (license.License, error) 37 GetLicenseStatusDocument(purchase Purchase) (licensestatuses.LicenseStatus, error) 38 GetByLicenseID(licenseID string) (Purchase, error) 39 List(page int, pageNum int) func() (Purchase, error) 40 ListByUser(userID int64, page int, pageNum int) func() (Purchase, error) 41 Add(p Purchase) error 42 Update(p Purchase) error 43 } 44 45 // Purchase status 46 const ( 47 StatusToBeRenewed string = "to-be-renewed" 48 StatusToBeReturned string = "to-be-returned" 49 StatusError string = "error" 50 StatusOk string = "ok" 51 ) 52 53 // Enumeration of PurchaseType 54 const ( 55 BUY string = "BUY" 56 LOAN string = "LOAN" 57 ) 58 59 //Purchase struct defines a user in json and database 60 //PurchaseType: BUY or LOAN 61 type Purchase struct { 62 ID int64 `json:"id,omitempty"` 63 UUID string `json:"uuid"` 64 Publication webpublication.Publication `json:"publication"` 65 User webuser.User `json:"user"` 66 LicenseUUID *string `json:"licenseUuid,omitempty"` 67 Type string `json:"type"` 68 TransactionDate time.Time `json:"transactionDate,omitempty"` 69 StartDate *time.Time `json:"startDate,omitempty"` 70 EndDate *time.Time `json:"endDate,omitempty"` 71 Status string `json:"status"` 72 MaxEndDate *time.Time `json:"maxEndDate,omitempty"` 73 } 74 75 type PurchaseManager struct { 76 db *sql.DB 77 dbGetByID *sql.Stmt 78 dbGetByLicenseID *sql.Stmt 79 dbList *sql.Stmt 80 dbListByUser *sql.Stmt 81 } 82 83 func convertRecordsToPurchases(rows *sql.Rows) func() (Purchase, error) { 84 85 return func() (Purchase, error) { 86 var err error 87 var purchase Purchase 88 if rows == nil { 89 return purchase, ErrNotFound 90 } 91 if rows.Next() { 92 purchase, err = convertRecordToPurchase(rows) 93 if err != nil { 94 return purchase, err 95 } 96 } else { 97 rows.Close() 98 err = ErrNotFound 99 } 100 return purchase, err 101 } 102 } 103 104 func convertRecordToPurchase(rows *sql.Rows) (Purchase, error) { 105 purchase := Purchase{} 106 user := webuser.User{} 107 pub := webpublication.Publication{} 108 109 err := rows.Scan( 110 &purchase.ID, 111 &purchase.UUID, 112 &purchase.Type, 113 &purchase.TransactionDate, 114 &purchase.LicenseUUID, 115 &purchase.StartDate, 116 &purchase.EndDate, 117 &purchase.Status, 118 &user.ID, 119 &user.UUID, 120 &user.Name, 121 &user.Email, 122 &user.Password, 123 &user.Hint, 124 &pub.ID, 125 &pub.UUID, 126 &pub.Title, 127 &pub.Status) 128 129 if err != nil { 130 return Purchase{}, err 131 } 132 133 // Load relations 134 purchase.User = user 135 purchase.Publication = pub 136 return purchase, nil 137 } 138 139 // Get a purchase using its id 140 func (pManager PurchaseManager) Get(id int64) (Purchase, error) { 141 142 // note: does not use QueryRow here, as converRecordToPurchase used in converRecordsToPurchase. 143 rows, err := pManager.dbGetByID.Query(id) 144 if err != nil { 145 return Purchase{}, err 146 } 147 defer rows.Close() 148 149 if rows.Next() { 150 purchase, err := convertRecordToPurchase(rows) 151 if err != nil { 152 return Purchase{}, err 153 } 154 155 if purchase.LicenseUUID != nil { 156 // Query LSD Server to retrieve max end date (PotentialRights.End) 157 // FIXME: calling the lsd server at this point is too heavy: the max end date should be in the db. 158 statusDocument, err := pManager.GetLicenseStatusDocument(purchase) 159 160 if err != nil { 161 return Purchase{}, err 162 } 163 164 if statusDocument.PotentialRights != nil && statusDocument.PotentialRights.End != nil && !(*statusDocument.PotentialRights.End).IsZero() { 165 purchase.MaxEndDate = statusDocument.PotentialRights.End 166 } 167 } 168 return purchase, nil 169 } 170 return Purchase{}, ErrNotFound 171 } 172 173 // GetByLicenseID gets a purchase by the associated license id 174 func (pManager PurchaseManager) GetByLicenseID(licenseID string) (Purchase, error) { 175 176 // note: does not use QueryRow here, as convertRecordToPurchase used in converRecordsToPurchase. 177 rows, err := pManager.dbGetByLicenseID.Query(licenseID) 178 if err != nil { 179 return Purchase{}, err 180 } 181 defer rows.Close() 182 183 if rows.Next() { 184 return convertRecordToPurchase(rows) 185 // FIXME: difference with Get(), we don't retrieve the potential end date from the LSD server 186 } 187 return Purchase{}, ErrNotFound 188 } 189 190 // GenerateOrGetLicense generates a new license associated with a purchase, 191 // or gets an existing license, 192 // depending on the value of the license id in the purchase. 193 func (pManager PurchaseManager) GenerateOrGetLicense(purchase Purchase) (license.License, error) { 194 // create a partial license 195 partialLicense := license.License{} 196 197 // set the mandatory provider URI 198 if config.Config.FrontendServer.ProviderUri == "" { 199 return license.License{}, errors.New("mandatory provider URI missing in the configuration") 200 } 201 partialLicense.Provider = config.Config.FrontendServer.ProviderUri 202 203 // get user info from the purchase info 204 encryptedAttrs := []string{"email", "name"} 205 partialLicense.User.Email = purchase.User.Email 206 partialLicense.User.Name = purchase.User.Name 207 partialLicense.User.ID = purchase.User.UUID 208 partialLicense.User.Encrypted = encryptedAttrs 209 210 // get the hashed passphrase from the purchase 211 userKeyValue, err := hex.DecodeString(purchase.User.Password) 212 213 if err != nil { 214 return license.License{}, err 215 } 216 217 userKey := license.UserKey{} 218 userKey.Algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" 219 userKey.Hint = purchase.User.Hint 220 userKey.Value = userKeyValue 221 partialLicense.Encryption.UserKey = userKey 222 223 // In case of a creation of license, add the user rights 224 var copy, print int32 225 if purchase.LicenseUUID == nil { 226 // in case of undefined conf values for copy and print rights, 227 // these rights will be set to zero 228 copy = config.Config.FrontendServer.RightCopy 229 print = config.Config.FrontendServer.RightPrint 230 userRights := license.UserRights{} 231 userRights.Copy = © 232 userRights.Print = &print 233 234 // if this is a loan, include start and end dates from the purchase info 235 if purchase.Type == LOAN { 236 userRights.Start = purchase.StartDate 237 userRights.End = purchase.EndDate 238 } 239 partialLicense.Rights = &userRights 240 } 241 242 // encode in json 243 jsonBody, err := json.Marshal(partialLicense) 244 if err != nil { 245 return license.License{}, err 246 } 247 248 // get the url of the lcp server 249 lcpServerConfig := config.Config.LcpServer 250 var lcpURL string 251 252 if purchase.LicenseUUID == nil { 253 // if the purchase contains no license id, generate a new license 254 lcpURL = lcpServerConfig.PublicBaseUrl + "/contents/" + purchase.Publication.UUID + "/license" 255 } else { 256 // if the purchase contains a license id, fetch an existing license 257 // note: this will not update the license rights 258 lcpURL = lcpServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID 259 } 260 // message to the console 261 log.Println("POST " + lcpURL) 262 263 // add the partial license to the POST request 264 req, err := http.NewRequest("POST", lcpURL, bytes.NewReader(jsonBody)) 265 if err != nil { 266 return license.License{}, err 267 } 268 lcpUpdateAuth := config.Config.LcpUpdateAuth 269 if config.Config.LcpUpdateAuth.Username != "" { 270 req.SetBasicAuth(lcpUpdateAuth.Username, lcpUpdateAuth.Password) 271 } 272 // the body is a partial license in json format 273 req.Header.Add("Content-Type", api.ContentType_LCP_JSON) 274 275 var lcpClient = &http.Client{ 276 Timeout: time.Second * 10, 277 } 278 // POST the request 279 resp, err := lcpClient.Do(req) 280 if err != nil { 281 return license.License{}, err 282 } 283 284 defer resp.Body.Close() 285 286 // if the status code from the request to the lcp server 287 // is neither 201 Created or 200 ok, return an internal error 288 if (purchase.LicenseUUID == nil && resp.StatusCode != 201) || 289 (purchase.LicenseUUID != nil && resp.StatusCode != 200) { 290 return license.License{}, errors.New("the License Server returned an error") 291 } 292 293 // decode the full license 294 fullLicense := license.License{} 295 dec := json.NewDecoder(resp.Body) 296 err = dec.Decode(&fullLicense) 297 298 if err != nil { 299 return license.License{}, errors.New("unable to decode license") 300 } 301 302 // store the license id if it was not already set 303 if purchase.LicenseUUID == nil { 304 purchase.LicenseUUID = &fullLicense.ID 305 pManager.Update(purchase) 306 if err != nil { 307 return license.License{}, errors.New("unable to update the license id") 308 } 309 } 310 311 return fullLicense, nil 312 } 313 314 // GetPartialLicense gets the license associated with a purchase, from the license server 315 func (pManager PurchaseManager) GetPartialLicense(purchase Purchase) (license.License, error) { 316 317 if purchase.LicenseUUID == nil { 318 return license.License{}, errors.New("no license has been yet delivered") 319 } 320 321 lcpServerConfig := config.Config.LcpServer 322 lcpURL := lcpServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID 323 // message to the console 324 log.Println("GET " + lcpURL) 325 // prepare the request 326 req, err := http.NewRequest("GET", lcpURL, nil) 327 if err != nil { 328 return license.License{}, err 329 } 330 // set credentials 331 lcpUpdateAuth := config.Config.LcpUpdateAuth 332 if config.Config.LcpUpdateAuth.Username != "" { 333 req.SetBasicAuth(lcpUpdateAuth.Username, lcpUpdateAuth.Password) 334 } 335 // send the request 336 var lcpClient = &http.Client{ 337 Timeout: time.Second * 10, 338 } 339 resp, err := lcpClient.Do(req) 340 if err != nil { 341 return license.License{}, err 342 } 343 defer resp.Body.Close() 344 345 // the call must return 206 (partial content) because there is no input partial license 346 if resp.StatusCode != 206 { 347 // bad status code 348 return license.License{}, errors.New("the License Server returned an error") 349 } 350 // decode the license 351 partialLicense := license.License{} 352 dec := json.NewDecoder(resp.Body) 353 err = dec.Decode(&partialLicense) 354 355 if err != nil { 356 return license.License{}, errors.New("unable to decode the license") 357 } 358 359 return partialLicense, nil 360 } 361 362 // GetLicenseStatusDocument gets a license status document associated with a purchase 363 func (pManager PurchaseManager) GetLicenseStatusDocument(purchase Purchase) (licensestatuses.LicenseStatus, error) { 364 if purchase.LicenseUUID == nil { 365 return licensestatuses.LicenseStatus{}, errors.New("no license has been yet delivered") 366 } 367 368 lsdServerConfig := config.Config.LsdServer 369 lsdURL := lsdServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID + "/status" 370 log.Println("GET " + lsdURL) 371 req, err := http.NewRequest("GET", lsdURL, nil) 372 if err != nil { 373 return licensestatuses.LicenseStatus{}, err 374 } 375 req.Header.Add("Content-Type", api.ContentType_JSON) 376 377 var lsdClient = &http.Client{ 378 Timeout: time.Second * 10, 379 } 380 381 resp, err := lsdClient.Do(req) 382 if err != nil { 383 return licensestatuses.LicenseStatus{}, err 384 } 385 386 if resp.StatusCode != 200 { 387 // Bad status code 388 return licensestatuses.LicenseStatus{}, errors.New("the License Status Document server returned an error") 389 } 390 391 // Decode status document 392 statusDocument := licensestatuses.LicenseStatus{} 393 dec := json.NewDecoder(resp.Body) 394 err = dec.Decode(&statusDocument) 395 396 if err != nil { 397 return licensestatuses.LicenseStatus{}, err 398 } 399 400 defer resp.Body.Close() 401 402 return statusDocument, nil 403 } 404 405 // List all purchases, with pagination 406 func (pManager PurchaseManager) List(page int, pageNum int) func() (Purchase, error) { 407 408 var rows *sql.Rows 409 var err error 410 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 411 if driver == "mssql" { 412 rows, err = pManager.dbList.Query(pageNum*page, page) 413 } else { 414 rows, err = pManager.dbList.Query(page, pageNum*page) 415 } 416 if err != nil { 417 log.Printf("Failed to get the full list of purchases: %s", err.Error()) 418 } 419 return convertRecordsToPurchases(rows) 420 } 421 422 // ListByUser lists the purchases of a given user, with pagination 423 func (pManager PurchaseManager) ListByUser(userID int64, page int, pageNum int) func() (Purchase, error) { 424 425 var rows *sql.Rows 426 var err error 427 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 428 if driver == "mssql" { 429 rows, err = pManager.dbListByUser.Query(userID, pageNum*page, page) 430 } else { 431 rows, err = pManager.dbListByUser.Query(userID, page, pageNum*page) 432 } 433 if err != nil { 434 log.Printf("Failed to get the user list of purchases: %s", err.Error()) 435 } 436 return convertRecordsToPurchases(rows) 437 } 438 439 // Add a purchase 440 func (pManager PurchaseManager) Add(p Purchase) error { 441 442 // Fill default values 443 if p.TransactionDate.IsZero() { 444 p.TransactionDate = time.Now().UTC().Truncate(time.Second) 445 } 446 447 if p.Type == LOAN && p.StartDate == nil { 448 p.StartDate = &p.TransactionDate 449 } 450 451 // Create uuid 452 uid, err_u := uuid.NewV4() 453 if err_u != nil { 454 return err_u 455 } 456 p.UUID = uid.String() 457 458 _, err := pManager.db.Exec(`INSERT INTO purchase 459 (uuid, publication_id, user_id, type, transaction_date, start_date, end_date, status) 460 VALUES (?, ?, ?, ?, ?, ?, ?, 'ok')`, 461 p.UUID, p.Publication.ID, p.User.ID, string(p.Type), p.TransactionDate, p.StartDate, p.EndDate) 462 463 return err 464 } 465 466 // Update modifies a purchase on a renew or return request 467 // parameters: a Purchase structure withID, LicenseUUID, StartDate, EndDate, Status 468 // EndDate may be undefined (nil), in which case the lsd server will choose the renew period 469 func (pManager PurchaseManager) Update(p Purchase) error { 470 // Get the original purchase from the db 471 origPurchase, err := pManager.Get(p.ID) 472 473 if err != nil { 474 return ErrNotFound 475 } 476 if origPurchase.Status != StatusOk { 477 return errors.New("cannot update an invalid purchase") 478 } 479 if p.Status == StatusToBeRenewed || 480 p.Status == StatusToBeReturned { 481 482 if p.LicenseUUID == nil { 483 return errors.New("cannot return or renew a purchase when no license has been delivered") 484 } 485 486 lsdServerConfig := config.Config.LsdServer 487 lsdURL := lsdServerConfig.PublicBaseUrl + "/licenses/" + *p.LicenseUUID 488 489 if p.Status == StatusToBeRenewed { 490 lsdURL += "/renew" 491 492 if p.EndDate != nil { 493 lsdURL += "?end=" + p.EndDate.Format(time.RFC3339) 494 } 495 496 // Next status if LSD raises no error 497 p.Status = StatusOk 498 } else if p.Status == StatusToBeReturned { 499 lsdURL += "/return" 500 501 // Next status if LSD raises no error 502 p.Status = StatusOk 503 } 504 // message to the console 505 log.Println("PUT " + lsdURL) 506 // prepare the request for renew or return to the license status server 507 req, err := http.NewRequest("PUT", lsdURL, nil) 508 if err != nil { 509 return err 510 } 511 // set credentials 512 lsdAuth := config.Config.LsdNotifyAuth 513 if lsdAuth.Username != "" { 514 req.SetBasicAuth(lsdAuth.Username, lsdAuth.Password) 515 } 516 // call the lsd server 517 var lsdClient = &http.Client{ 518 Timeout: time.Second * 10, 519 } 520 resp, err := lsdClient.Do(req) 521 if err != nil { 522 return err 523 } 524 defer resp.Body.Close() 525 526 // if the renew/return request was refused by the License Status server 527 if resp.StatusCode != 200 { 528 return ErrNoChange 529 } 530 531 // get the new end date from the license server 532 // FIXME: is there a lighter solution to get the new end date? 533 license, err := pManager.GetPartialLicense(origPurchase) 534 if err != nil { 535 return err 536 } 537 p.EndDate = license.Rights.End 538 } else { 539 // status is not "to be renewed" 540 p.Status = StatusOk 541 } 542 // update the db with the updated license id, start date, end date, status 543 result, err := pManager.db.Exec(`UPDATE purchase SET license_uuid=?, start_date=?, end_date=?, status=? WHERE id=?`, 544 p.LicenseUUID, p.StartDate, p.EndDate, p.Status, p.ID) 545 if err != nil { 546 return err 547 } 548 if changed, err := result.RowsAffected(); err == nil { 549 if changed != 1 { 550 return ErrNoChange 551 } 552 } 553 return err 554 } 555 556 // Init initializes the PurchaseManager 557 func Init(db *sql.DB) (i WebPurchase, err error) { 558 559 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 560 561 // if sqlite, create the content table in the frontend db if it does not exist 562 if driver == "sqlite3" { 563 _, err = db.Exec(tableDef) 564 if err != nil { 565 log.Println("Error creating purchase table") 566 return 567 } 568 } 569 570 selectQuery := `SELECT p.id, p.uuid, p.type, p.transaction_date, p.license_uuid, p.start_date, p.end_date, p.status, 571 u.id, u.uuid, u.name, u.email, u.password, u.hint, 572 pu.id, pu.uuid, pu.title, pu.status 573 FROM purchase p JOIN "user" u ON (p.user_id=u.id) JOIN publication pu ON (p.publication_id=pu.id)` 574 575 var dbGetByID *sql.Stmt 576 dbGetByID, err = db.Prepare(selectQuery + `WHERE p.id = ?`) 577 if err != nil { 578 return 579 } 580 581 var dbGetByLicenseID *sql.Stmt 582 dbGetByLicenseID, err = db.Prepare(selectQuery + ` WHERE p.license_uuid = ?`) 583 if err != nil { 584 return 585 } 586 587 var dbList *sql.Stmt 588 if driver == "mssql" { 589 dbList, err = db.Prepare(selectQuery + ` ORDER BY p.transaction_date desc OFFSET ? ROWS FETCH NEXT ? ROWS ONLY`) 590 } else { 591 dbList, err = db.Prepare(selectQuery + ` ORDER BY p.transaction_date desc LIMIT ? OFFSET ?`) 592 } 593 if err != nil { 594 return 595 } 596 597 var dbListByUser *sql.Stmt 598 if driver == "mssql" { 599 dbListByUser, err = db.Prepare(selectQuery + ` WHERE u.id = ? ORDER BY p.transaction_date desc OFFSET ? ROWS FETCH NEXT ? ROWS ONLY`) 600 } else { 601 dbListByUser, err = db.Prepare(selectQuery + ` WHERE u.id = ? ORDER BY p.transaction_date desc LIMIT ? OFFSET ?`) 602 } 603 if err != nil { 604 return 605 } 606 607 i = PurchaseManager{db, dbGetByID, dbGetByLicenseID, dbList, dbListByUser} 608 return 609 } 610 611 const tableDef = "CREATE TABLE IF NOT EXISTS purchase (" + 612 "id integer NOT NULL PRIMARY KEY," + 613 "uuid varchar(255) NOT NULL," + 614 "publication_id integer NOT NULL," + 615 "user_id integer NOT NULL," + 616 "license_uuid varchar(255) NULL," + 617 "type varchar(32) NOT NULL," + 618 "transaction_date datetime," + 619 "start_date datetime," + 620 "end_date datetime," + 621 "status varchar(255) NOT NULL," + 622 "FOREIGN KEY (publication_id) REFERENCES publication(id)," + 623 "FOREIGN KEY (user_id) REFERENCES user(id)" + 624 ");" + 625 "CREATE INDEX IF NOT EXISTS idx_purchase ON purchase (license_uuid)"