Skip to content

Commit

Permalink
Merge pull request #347 from readium/dev
Browse files Browse the repository at this point in the history
Evolutions for version 1.10
  • Loading branch information
llemeurfr authored Dec 31, 2024
2 parents 0053fef + aadb3b8 commit 39684b9
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 33 deletions.
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ type LicenseStatus struct {
RenewDays int `yaml:"renew_days"`
RenewPageUrl string `yaml:"renew_page_url,omitempty"`
RenewCustomUrl string `yaml:"renew_custom_url,omitempty"`
RenewExpired bool `yaml:"renew_expired"`
RenewFromNow bool `yaml:"renew_from_now"`
}

type Localization struct {
Expand Down
6 changes: 6 additions & 0 deletions encrypt/notify_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ func NotifyLCPServer(pub Publication, lcpsv string, v2 bool, username string, pa
if err != nil {
return err
}
defer resp.Body.Close()

if (resp.StatusCode != 302) && (resp.StatusCode/100) != 2 { //302=found or 20x reply = OK
return fmt.Errorf("the server returned an error %d", resp.StatusCode)
}
Expand Down Expand Up @@ -187,6 +189,8 @@ func AbortNotification(pub Publication, lcpsv string, v2 bool, username string,
if err != nil {
return err
}
defer resp.Body.Close()

if (resp.StatusCode != 302) && (resp.StatusCode/100) != 2 { //302=found or 20x reply = OK
return fmt.Errorf("the server returned an error %d", resp.StatusCode)
}
Expand Down Expand Up @@ -262,6 +266,8 @@ func NotifyCMS(pub Publication, notifyURL string, verbose bool) error {
if err != nil {
return err
}
defer resp.Body.Close()

if (resp.StatusCode != 302) && (resp.StatusCode/100) != 2 { //302=found or 20x reply = OK
return fmt.Errorf("the server returned an error %d", resp.StatusCode)
}
Expand Down
12 changes: 6 additions & 6 deletions frontend/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import (
"github.com/readium/readium-lcp-server/frontend/webuser"
)

//Server struct contains server info and db interfaces
// Server struct contains server info and db interfaces
type Server struct {
http.Server
readonly bool
Expand Down Expand Up @@ -197,14 +197,14 @@ func fetchLicenseStatusesTask(s *Server) {
log.Println("No http connection - no fetch this time")
return
}
defer res.Body.Close()

// get all licence status documents from the lsd server
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println("Failed to read from the http connection - no fetch this time")
return
}
defer res.Body.Close()

// clear the db
err = s.license.PurgeDataBase()
Expand All @@ -229,22 +229,22 @@ func (server *Server) PublicationAPI() webpublication.WebPublication {
return server.publications
}

//UserAPI ( staticapi.IServer )returns DB interface for users
// UserAPI ( staticapi.IServer )returns DB interface for users
func (server *Server) UserAPI() webuser.WebUser {
return server.users
}

//PurchaseAPI ( staticapi.IServer )returns DB interface for purchases
// PurchaseAPI ( staticapi.IServer )returns DB interface for purchases
func (server *Server) PurchaseAPI() webpurchase.WebPurchase {
return server.purchases
}

//DashboardAPI ( staticapi.IServer )returns DB interface for dashboard
// DashboardAPI ( staticapi.IServer )returns DB interface for dashboard
func (server *Server) DashboardAPI() webdashboard.WebDashboard {
return server.dashboard
}

//LicenseAPI ( staticapi.IServer )returns DB interface for license
// LicenseAPI ( staticapi.IServer )returns DB interface for license
func (server *Server) LicenseAPI() weblicense.WebLicense {
return server.license
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/webpurchase/webpurchase.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func (pManager PurchaseManager) GenerateOrGetLicense(purchase Purchase) (license
// store the license id if it was not already set
if purchase.LicenseUUID == nil {
purchase.LicenseUUID = &fullLicense.ID
pManager.Update(purchase)
err = pManager.Update(purchase)
if err != nil {
return license.License{}, errors.New("unable to update the license id")
}
Expand Down
6 changes: 3 additions & 3 deletions index/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ type Index interface {
// Content represents an encrypted resource
type Content struct {
ID string `json:"id"`
EncryptionKey []byte `json:"-"`
EncryptionKey []byte `json:"key,omitempty"` // warning, sensitive info
Location string `json:"location"`
Length int64 `json:"length"` //not exported in license spec?
Sha256 string `json:"sha256"` //not exported in license spec?
Length int64 `json:"length"`
Sha256 string `json:"sha256"`
Type string `json:"type"`
}

Expand Down
102 changes: 102 additions & 0 deletions lcpencrypt/get_content_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2024 Readium Foundation. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file exposed on Github (readium) in the project repository.

package main

import (
b64 "encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)

type ContentInfo struct {
ID string `json:"id"`
EncryptionKey []byte `json:"key,omitempty"`
Location string `json:"location"`
Length int64 `json:"length"`
Sha256 string `json:"sha256"`
Type string `json:"type"`
}

// getContentKey gets content information from the License Server
// for a given content id,
// and returns the associated content key.
func getContentKey(contentKey *string, contentID, lcpsv string, v2 bool, username, password string) error {

// An empty notify URL is not an error, simply a silent encryption
if lcpsv == "" {
return nil
}

if !strings.HasPrefix(lcpsv, "http://") && !strings.HasPrefix(lcpsv, "https://") {
lcpsv = "http://" + lcpsv
}
var getInfoURL string
var err error
if v2 {
getInfoURL, err = url.JoinPath(lcpsv, "publications", contentID, "info")
} else {
getInfoURL, err = url.JoinPath(lcpsv, "contents", contentID, "info")
}
if err != nil {
return err
}

// look for the username and password in the url
err = getUsernamePassword(&getInfoURL, &username, &password)
if err != nil {
return err
}

req, err := http.NewRequest("GET", getInfoURL, nil)
if err != nil {
return err
}

req.SetBasicAuth(username, password)
client := &http.Client{
Timeout: 15 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

// if the content is found, the content key is updated
if resp.StatusCode == http.StatusOK {
contentInfo := ContentInfo{}
dec := json.NewDecoder(resp.Body)
err = dec.Decode(&contentInfo)
if err != nil {
return errors.New("unable to decode content information")
}

*contentKey = b64.StdEncoding.EncodeToString(contentInfo.EncryptionKey)
fmt.Println("Existing encryption key retrieved")
}
return nil
}

// Look for the username and password in the url
func getUsernamePassword(notifyURL, username, password *string) error {
u, err := url.Parse(*notifyURL)
if err != nil {
return err
}
un := u.User.Username()
pw, pwfound := u.User.Password()
if un != "" && pwfound {
*username = un
*password = pw
u.User = nil
*notifyURL = u.String() // notifyURL is updated
}
return nil
}
13 changes: 13 additions & 0 deletions lcpencrypt/lcpencrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ func main() {

start := time.Now()

// if contentid is set but not contentkey, check if the content already exists in the License Server.
// If this is the case, get the content encryption key for the server, so that the new encryption
// keeps the same key. This is necessary to allow fresh licenses being capable
// of decrypting previously downloaded content.
if *contentid != "" && *contentkey == "" {
// warning: this is a synchronous REST call
// contentKey is not initialized if the content does not exist in the License Server
err := getContentKey(contentkey, *contentid, *lcpsv, *v2, *username, *password)
if err != nil {
exitWithError("Error retrieving content info", err)
}
}

// encrypt the publication
publication, err := encrypt.ProcessEncryption(*contentid, *contentkey, *inputPath, *tempRepo, *outputRepo, *storageRepo, *storageURL, *storageFilename, *cover)
if err != nil {
Expand Down
60 changes: 54 additions & 6 deletions lcpserver/api/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -56,7 +56,7 @@ const (

func writeRequestFileToTemp(r io.Reader) (int64, *os.File, error) {
dir := os.TempDir()
file, err := ioutil.TempFile(dir, "readium-lcp")
file, err := os.CreateTemp(dir, "readium-lcp")
if err != nil {
return 0, file, err
}
Expand Down Expand Up @@ -155,9 +155,10 @@ func AddContent(w http.ResponseWriter, r *http.Request, s Server) {
}

// insert a row in the database if the content id does not already exist
// or update the database with a new content key and file location if the content id already exists
// or update the database with new information if the content id already exists
var c index.Content
c, err = s.Index().Get(contentID)
// err checked later ...
c.EncryptionKey = encrypted.ContentKey
// the Location field contains either the file name (useful during download)
// or the storage URL of the encrypted, depending the storage mode.
Expand All @@ -174,9 +175,16 @@ func AddContent(w http.ResponseWriter, r *http.Request, s Server) {
if err == index.ErrNotFound { //insert into database
c.ID = contentID
err = s.Index().Add(c)
// the content id was found in the database
} else { //update the encryption key for c.ID = encrypted.ContentID
err = s.Index().Update(c)
code = http.StatusOK

if err == nil {
log.Println("Update all license timestamps associated with this publication")
err = s.Licenses().TouchByContentID(contentID) // update all licenses update timestamps
}

}
if err != nil { //if db not updated
problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
Expand All @@ -193,7 +201,9 @@ func ListContents(w http.ResponseWriter, r *http.Request, s Server) {
fn := s.Index().List()
contents := make([]index.Content, 0)

var razkey []byte // in a list, we don't return the encryption key.
for it, err := fn(); err == nil; it, err = fn() {
it.EncryptionKey = razkey
contents = append(contents, it)
}

Expand All @@ -210,11 +220,44 @@ func ListContents(w http.ResponseWriter, r *http.Request, s Server) {

}

// GetContent fetches and returns an encrypted content file
// GetContentInfo returns information about the encrypted content,
// especially the encryption key.
// Used by the encryption utility when the file to encrypt is an update of an existing encrypted publication.
func GetContentInfo(w http.ResponseWriter, r *http.Request, s Server) {
// get the content id from the calling url
vars := mux.Vars(r)
contentID := vars["content_id"]

// add a log
logging.Print("Get content info " + contentID)

// get the info
content, err := s.Index().Get(contentID)
if err != nil { //item probably not found
if err == index.ErrNotFound {
problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusNotFound)
} else {
problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusInternalServerError)
}
return
}

// return the info
w.Header().Set("Content-Type", api.ContentType_JSON)
enc := json.NewEncoder(w)
err = enc.Encode(content)
if err != nil {
problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
return
}

}

// GetContentFile fetches and returns an encrypted content file
// selected by it content id (uuid)
// This should be called only if the License Server stores the file.
// If it is not the case, the file should be fetched from a standard web server
func GetContent(w http.ResponseWriter, r *http.Request, s Server) {
func GetContentFile(w http.ResponseWriter, r *http.Request, s Server) {

// get the content id from the calling url
vars := mux.Vars(r)
Expand Down Expand Up @@ -316,7 +359,7 @@ func getAndOpenFile(filePathOrURL string) (*os.File, error) {
}

func downloadAndOpenFile(url string) (*os.File, error) {
file, _ := ioutil.TempFile("", "")
file, _ := os.CreateTemp("", "")
fileName := file.Name()

err := downloadFile(url, fileName)
Expand Down Expand Up @@ -361,3 +404,8 @@ func downloadFile(url string, targetFilePath string) error {

return nil
}

// Ping is a simple health check
func Ping(w http.ResponseWriter, r *http.Request, s Server) {
w.WriteHeader(http.StatusOK)
}
10 changes: 9 additions & 1 deletion lcpserver/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ func New(bindAddr string, readonly bool, idx *index.Index, st *storage.Store, ls
// Route.Subrouter: http://www.gorillatoolkit.org/pkg/mux#Route.Subrouter
// Router.StrictSlash: http://www.gorillatoolkit.org/pkg/mux#Router.StrictSlash

// Ping endpoint
s.handleFunc(sr.R, "/ping", apilcp.Ping).Methods("GET")

// Serve static resources from a configurable directory.
// This is used when lcpencrypt sends encrypted resources and cover images to an fs storage,
// and we want this http server to provide such resources to the outside world (e.g. PubStore).
Expand All @@ -114,10 +117,15 @@ func New(bindAddr string, readonly bool, idx *index.Index, st *storage.Store, ls

s.handleFunc(sr.R, contentRoutesPathPrefix, apilcp.ListContents).Methods("GET")

// Public routes
// get encrypted content by content id (a uuid)
s.handleFunc(contentRoutes, "/{content_id}", apilcp.GetContent).Methods("GET")
s.handleFunc(contentRoutes, "/{content_id}", apilcp.GetContentFile).Methods("GET")

// Private routes
// get all licenses associated with a given content
s.handlePrivateFunc(contentRoutes, "/{content_id}/licenses", apilcp.ListLicensesForContent, basicAuth).Methods("GET")
// get content information by content id (a uuid)
s.handlePrivateFunc(contentRoutes, "/{content_id}/info", apilcp.GetContentInfo, basicAuth).Methods("GET")

if !readonly {
// create a publication
Expand Down
Loading

0 comments on commit 39684b9

Please sign in to comment.