Skip to content

Commit

Permalink
When content is updated, update content info and touches all associat…
Browse files Browse the repository at this point in the history
…ed licenses
  • Loading branch information
llemeurfr committed Dec 28, 2024
1 parent c10afab commit 4e6f850
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 7 deletions.
100 changes: 100 additions & 0 deletions lcpencrypt/get_content_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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()

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
}
55 changes: 49 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
7 changes: 6 additions & 1 deletion lcpserver/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,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
12 changes: 12 additions & 0 deletions license/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Store interface {
UpdateLsdStatus(id string, status int32) error
Add(l License) error
Get(id string) (License, error)
TouchByContentID(ContentID string) error
}

type sqlStore struct {
Expand Down Expand Up @@ -160,6 +161,17 @@ func (s *sqlStore) Get(id string) (License, error) {
return l, err
}

// TouchByContentID updates the updated field of all licenses for a given contentID
func (s *sqlStore) TouchByContentID(contentID string) error {

_, err := s.db.Exec(dbutils.GetParamQuery(config.Config.LcpServer.Database, `UPDATE license SET updated=? WHERE content_fk=?`),
time.Now().UTC().Truncate(time.Second), contentID)
if err != nil {
log.Println("Error touching licenses for contentID", contentID)
}
return err
}

// Open
func Open(db *sql.DB) (store Store, err error) {

Expand Down

0 comments on commit 4e6f850

Please sign in to comment.