github.com/readium/readium-lcp-server@v0.0.0-20240509124024-799e77a0bbd6/lcpserver/api/license.go (about)

     1  // Copyright 2017 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  	"archive/zip"
     9  	"bytes"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"log"
    16  	"net/http"
    17  	"strconv"
    18  	"time"
    19  
    20  	"github.com/gorilla/mux"
    21  
    22  	"github.com/readium/readium-lcp-server/api"
    23  	"github.com/readium/readium-lcp-server/config"
    24  	"github.com/readium/readium-lcp-server/epub"
    25  	"github.com/readium/readium-lcp-server/index"
    26  	"github.com/readium/readium-lcp-server/license"
    27  	"github.com/readium/readium-lcp-server/logging"
    28  	"github.com/readium/readium-lcp-server/problem"
    29  	"github.com/readium/readium-lcp-server/storage"
    30  )
    31  
    32  // ErrMandatoryInfoMissing sets an error message returned to the caller
    33  var ErrMandatoryInfoMissing = errors.New("mandatory info missing in the input body")
    34  
    35  // ErrBadHexValue sets an error message returned to the caller
    36  var ErrBadHexValue = errors.New("erroneous user_key.hex_value can't be decoded")
    37  
    38  // ErrBadValue sets an error message returned to the caller
    39  var ErrBadValue = errors.New("erroneous user_key.value, can't be decoded")
    40  
    41  // lastGeneratedLicense is for tests only
    42  var lastGeneratedLicense license.License
    43  
    44  // checkGetLicenseInput: if we generate or get a license, check mandatory information in the input body
    45  // and compute request parameters
    46  func checkGetLicenseInput(l *license.License) error {
    47  
    48  	// the user hint is mandatory
    49  	if l.Encryption.UserKey.Hint == "" {
    50  		log.Println("User hint is missing")
    51  		return ErrMandatoryInfoMissing
    52  	}
    53  	// Value or HexValue are mandatory
    54  	// HexValue (hex encoded passphrase hash) takes precedence over Value (kept for backward compatibility)
    55  	// Value is computed from HexValue if set
    56  	if l.Encryption.UserKey.HexValue != "" {
    57  		// compute a byte array from a string
    58  		value, err := hex.DecodeString(l.Encryption.UserKey.HexValue)
    59  		if err != nil {
    60  			return ErrBadHexValue
    61  		}
    62  		l.Encryption.UserKey.Value = value
    63  	} else if l.Encryption.UserKey.Value == nil {
    64  		log.Println("User hashed passphrase is missing")
    65  		return ErrMandatoryInfoMissing
    66  	}
    67  	// check the size of Value (32 bytes), to avoid weird errors in the crypto code
    68  	if len(l.Encryption.UserKey.Value) != 32 {
    69  		return ErrBadValue
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  // checkGenerateLicenseInput: if we generate a license, check mandatory information in the input body
    76  func checkGenerateLicenseInput(l *license.License) error {
    77  
    78  	if l.User.ID == "" {
    79  		log.Println("User identification is missing")
    80  		return ErrMandatoryInfoMissing
    81  	}
    82  	// check user hint, passphrase hash and hash algorithm
    83  	err := checkGetLicenseInput(l)
    84  	return err
    85  }
    86  
    87  // get license, copy useful data from licIn to LicOut
    88  func copyInputToLicense(licIn *license.License, licOut *license.License) {
    89  
    90  	// copy the user hint and hashed passphrase
    91  	licOut.Encryption.UserKey.Hint = licIn.Encryption.UserKey.Hint
    92  	licOut.Encryption.UserKey.Value = licIn.Encryption.UserKey.Value
    93  	licOut.Encryption.UserKey.HexValue = licIn.Encryption.UserKey.HexValue
    94  
    95  	// copy optional user information
    96  	licOut.User.Email = licIn.User.Email
    97  	licOut.User.Name = licIn.User.Name
    98  	licOut.User.Encrypted = licIn.User.Encrypted
    99  	licOut.Links = licIn.Links
   100  }
   101  
   102  // normalize the start and end date, UTC, no milliseconds
   103  func setRights(lic *license.License) {
   104  
   105  	// a rights object is needed before adding a record to the db
   106  	if lic.Rights == nil {
   107  		lic.Rights = new(license.UserRights)
   108  	}
   109  	if lic.Rights.Start != nil {
   110  		start := lic.Rights.Start.UTC().Truncate(time.Second)
   111  		lic.Rights.Start = &start
   112  	}
   113  	if lic.Rights.End != nil {
   114  		end := lic.Rights.End.UTC().Truncate(time.Second)
   115  		lic.Rights.End = &end
   116  	}
   117  }
   118  
   119  // build a license, common to get and generate license, get and generate a protected publication
   120  func buildLicense(lic *license.License, s Server, updatefix bool) error {
   121  
   122  	// set the LCP profile
   123  	err := license.SetLicenseProfile(lic)
   124  	if err != nil {
   125  		log.Println("Build License: " + err.Error())
   126  		return err
   127  	}
   128  
   129  	// force the algorithm to the one defined by the current profiles
   130  	lic.Encryption.UserKey.Algorithm = "http://www.w3.org/2001/04/xmlenc#sha256"
   131  
   132  	// get content info from the db
   133  	content, err := s.Index().Get(lic.ContentID)
   134  	if err != nil {
   135  		log.Println("No content with id", lic.ContentID)
   136  		return err
   137  	}
   138  
   139  	// set links
   140  	err = license.SetLicenseLinks(lic, content)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	// encrypt the content key, user fieds, set the key check
   145  	err = license.EncryptLicenseFields(lic, content)
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	// fix an issue with clients which test that the date of last update of the license
   151  	// is after the date of creation of the X509 certificate.
   152  	// Because of this, when a cert is replaced, fresh licenses are not accepted by such clients
   153  	// when they have been created / updated before the cert update.
   154  	if updatefix && config.Config.LcpServer.CertDate != "" {
   155  		certDate, err := time.Parse("2006-01-02", config.Config.LcpServer.CertDate)
   156  		if err != nil {
   157  			return err
   158  		}
   159  		if lic.Issued.Before(certDate) && (lic.Updated == nil || lic.Updated.Before(certDate)) {
   160  			lic.Updated = &certDate
   161  		}
   162  	}
   163  
   164  	// sign the license
   165  	err = license.SignLicense(lic, s.Certificate())
   166  	if err != nil {
   167  		return err
   168  	}
   169  	return nil
   170  }
   171  
   172  // copyZipFiles copies every file from one zip archive to another
   173  func copyZipFiles(out *zip.Writer, in *zip.Reader) error {
   174  
   175  	for _, file := range in.File {
   176  		newFile, err := out.CreateHeader(&file.FileHeader)
   177  		if err != nil {
   178  			return err
   179  		}
   180  
   181  		r, err := file.Open()
   182  		if err != nil {
   183  			return err
   184  		}
   185  
   186  		_, err = io.Copy(newFile, r)
   187  		r.Close()
   188  		if err != nil {
   189  			return err
   190  		}
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  // isWebPub checks the presence of a REadium manifest is a zip package
   197  func isWebPub(in *zip.Reader) bool {
   198  
   199  	for _, f := range in.File {
   200  		if f.Name == "manifest.json" {
   201  			return true
   202  		}
   203  	}
   204  
   205  	return false
   206  }
   207  
   208  // buildProtectedPublication builds a protected publication, common to get and generate protected publication
   209  func buildProtectedPublication(lic *license.License, s Server) (buf bytes.Buffer, err error) {
   210  
   211  	// get content info from the bd
   212  	item, err := s.Store().Get(lic.ContentID)
   213  	if err != nil {
   214  		return
   215  	}
   216  	// read the content into a buffer
   217  	contents, err := item.Contents()
   218  	if err != nil {
   219  		return buf, err
   220  	}
   221  	b, err := io.ReadAll(contents)
   222  	if err != nil {
   223  		return buf, err
   224  	}
   225  	// create a zip reader
   226  	zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
   227  	if err != nil {
   228  		return buf, err
   229  	}
   230  
   231  	zipWriter := zip.NewWriter(&buf)
   232  	err = copyZipFiles(zipWriter, zr)
   233  	if err != nil {
   234  		return buf, err
   235  	}
   236  
   237  	// Encode the license to JSON, remove the trailing newline
   238  	// write the buffer in the zip
   239  	licenseBytes, err := json.Marshal(lic)
   240  	if err != nil {
   241  		return buf, err
   242  	}
   243  
   244  	licenseBytes = bytes.TrimRight(licenseBytes, "\n")
   245  
   246  	location := epub.LicenseFile
   247  	if isWebPub(zr) {
   248  		location = "license.lcpl"
   249  	}
   250  
   251  	licenseWriter, err := zipWriter.Create(location)
   252  	if err != nil {
   253  		return buf, err
   254  	}
   255  
   256  	_, err = licenseWriter.Write(licenseBytes)
   257  	if err != nil {
   258  		return
   259  	}
   260  
   261  	return buf, zipWriter.Close()
   262  }
   263  
   264  // GetTestLicense returns an existing license,
   265  // selected by a license id and a partial license
   266  // set to the last information generated on this server. This is only for test.
   267  func GetTestLicense(w http.ResponseWriter, r *http.Request, s Server) {
   268  
   269  	// Active only in test mode, if
   270  	if !config.Config.TestMode {
   271  		problem.Error(w, r, problem.Problem{Detail: "The server is not in test mode"}, http.StatusBadRequest)
   272  		return
   273  	}
   274  	if lastGeneratedLicense.User.ID == "" {
   275  		problem.Error(w, r, problem.Problem{Detail: "No previous generated license"}, http.StatusBadRequest)
   276  		return
   277  	}
   278  
   279  	vars := mux.Vars(r)
   280  	// get the license id from the request URL
   281  	licenseID := vars["license_id"]
   282  
   283  	// add a log
   284  	logging.Print("Get a Test License " + licenseID)
   285  
   286  	// initialize the license from the info stored in the db.
   287  	var licOut license.License
   288  	licOut, e := s.Licenses().Get(licenseID)
   289  	// process license not found etc.
   290  	if e == license.ErrNotFound {
   291  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusNotFound)
   292  		return
   293  	} else if e != nil {
   294  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusBadRequest)
   295  		return
   296  	}
   297  
   298  	// It contains the hashed passphrase, user hint
   299  	// and other optional user data the provider wants to see embedded in the license
   300  	licIn := lastGeneratedLicense
   301  
   302  	// an input body was sent with the request:
   303  	// check mandatory information in the partial license
   304  	err := checkGetLicenseInput(&licIn)
   305  	if err != nil {
   306  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   307  		return
   308  	}
   309  	// copy useful data from licIn to LicOut
   310  	copyInputToLicense(&licIn, &licOut)
   311  	// build the license
   312  	err = buildLicense(&licOut, s, true)
   313  	if err != nil {
   314  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   315  		return
   316  	}
   317  
   318  	// set the http headers
   319  	w.Header().Add("Content-Type", api.ContentType_LCP_JSON)
   320  	w.Header().Add("Content-Disposition", `attachment; filename="license.lcpl"`)
   321  	w.WriteHeader(http.StatusOK)
   322  	// send back the license
   323  	// do not escape characters in the json payload
   324  	enc := json.NewEncoder(w)
   325  	enc.SetEscapeHTML(false)
   326  	enc.Encode(licOut)
   327  }
   328  
   329  // GetLicense returns an existing license,
   330  // selected by a license id and a partial license both given as input.
   331  // If the input partial license is absent a partial license is returned
   332  func GetLicense(w http.ResponseWriter, r *http.Request, s Server) {
   333  
   334  	vars := mux.Vars(r)
   335  	// get the license id from the request URL
   336  	licenseID := vars["license_id"]
   337  
   338  	// add a log
   339  	logging.Print("Get the License " + licenseID)
   340  
   341  	// initialize the license from the info stored in the db.
   342  	var licOut license.License
   343  	licOut, e := s.Licenses().Get(licenseID)
   344  	// process license not found etc.
   345  	if e == license.ErrNotFound {
   346  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusNotFound)
   347  		return
   348  	} else if e != nil {
   349  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusBadRequest)
   350  		return
   351  	}
   352  	// get the input body.
   353  	// It contains the hashed passphrase, user hint
   354  	// and other optional user data the provider wants to see embedded in the license
   355  	var err error
   356  	var licIn license.License
   357  	err = DecodeJSONLicense(r, &licIn)
   358  	// error parsing the input body
   359  	if err != nil {
   360  		// if there was no partial license given as payload,
   361  		if err.Error() == "EOF" {
   362  			// The use case is a frontend that needs to get up to date license rights
   363  			log.Println("No payload, get a partial license")
   364  
   365  			// add useful http headers
   366  			w.Header().Add("Content-Type", api.ContentType_LCP_JSON)
   367  			w.WriteHeader(http.StatusPartialContent)
   368  			// send back the partial license
   369  			// do not escape characters
   370  			enc := json.NewEncoder(w)
   371  			enc.SetEscapeHTML(false)
   372  			enc.Encode(licOut)
   373  			return
   374  		} else {
   375  			// unknown error
   376  			problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   377  			return
   378  		}
   379  	}
   380  
   381  	// an input body was sent with the request:
   382  	// check mandatory information in the partial license
   383  	err = checkGetLicenseInput(&licIn)
   384  	if err != nil {
   385  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   386  		return
   387  	}
   388  	// copy useful data from licIn to LicOut
   389  	copyInputToLicense(&licIn, &licOut)
   390  	// build the license
   391  	err = buildLicense(&licOut, s, true)
   392  	if err != nil {
   393  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   394  		return
   395  	}
   396  
   397  	// set the http headers
   398  	w.Header().Add("Content-Type", api.ContentType_LCP_JSON)
   399  	w.Header().Add("Content-Disposition", `attachment; filename="license.lcpl"`)
   400  	w.WriteHeader(http.StatusOK)
   401  	// send back the license
   402  	// do not escape characters in the json payload
   403  	enc := json.NewEncoder(w)
   404  	enc.SetEscapeHTML(false)
   405  	enc.Encode(licOut)
   406  }
   407  
   408  // GenerateLicense generates and returns a new license,
   409  // for a given content identified by its id
   410  // plus a partial license given as input
   411  func GenerateLicense(w http.ResponseWriter, r *http.Request, s Server) {
   412  
   413  	vars := mux.Vars(r)
   414  	// get the content id from the request URL
   415  	contentID := vars["content_id"]
   416  
   417  	// get the input body
   418  	// note: no need to create licIn / licOut here, as the input body contains
   419  	// info that we want to keep in the full license.
   420  	var lic license.License
   421  	err := DecodeJSONLicense(r, &lic)
   422  	if err != nil {
   423  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   424  		return
   425  	}
   426  	if config.Config.TestMode {
   427  		// save the partial license for later use on get license
   428  		lastGeneratedLicense = lic
   429  	}
   430  
   431  	// check mandatory information in the input body
   432  	err = checkGenerateLicenseInput(&lic)
   433  	if err != nil {
   434  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   435  		return
   436  	}
   437  	// init the license with an id and issue date
   438  	license.Initialize(contentID, &lic)
   439  
   440  	// add a log
   441  	logging.Print("Generate the License " + lic.ID + " for Content " + contentID + " and User " + lic.User.ID)
   442  
   443  	// normalize the start and end date, UTC, no milliseconds
   444  	setRights(&lic)
   445  
   446  	// build the license
   447  	err = buildLicense(&lic, s, false)
   448  	if err != nil {
   449  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   450  		return
   451  	}
   452  
   453  	// store the license in the db
   454  	err = s.Licenses().Add(lic)
   455  	if err != nil {
   456  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   457  		//problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: contentID}, http.StatusInternalServerError)
   458  		return
   459  	}
   460  
   461  	// set http headers
   462  	w.Header().Add("Content-Type", api.ContentType_LCP_JSON)
   463  	w.Header().Add("Content-Disposition", `attachment; filename="license.lcpl"`)
   464  	w.WriteHeader(http.StatusCreated)
   465  	// send back the license
   466  	// do not escape characters
   467  	enc := json.NewEncoder(w)
   468  	enc.SetEscapeHTML(false)
   469  	enc.Encode(lic)
   470  
   471  	// notify the lsd server of the creation of the license.
   472  	// this is an asynchronous call.
   473  	go notifyLsdServer(lic, s)
   474  }
   475  
   476  // GetProtectedPublication returns a protected publication
   477  // for a given license identified by its id
   478  // plus a partial license given as input
   479  func GetProtectedPublication(w http.ResponseWriter, r *http.Request, s Server) {
   480  
   481  	vars := mux.Vars(r)
   482  	licenseID := vars["license_id"]
   483  
   484  	// add a log
   485  	logging.Print("Get a Licensed publication for License " + licenseID)
   486  
   487  	// get the input body
   488  	var licIn license.License
   489  	err := DecodeJSONLicense(r, &licIn)
   490  	if err != nil {
   491  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   492  		return
   493  	}
   494  	// check mandatory information in the input body
   495  	err = checkGetLicenseInput(&licIn)
   496  	if err != nil {
   497  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   498  		return
   499  	}
   500  	// initialize the license from the info stored in the db.
   501  	licOut, e := s.Licenses().Get(licenseID)
   502  	// process license not found etc.
   503  	if e == license.ErrNotFound {
   504  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusNotFound)
   505  		return
   506  	} else if e != nil {
   507  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusBadRequest)
   508  		return
   509  	}
   510  	// copy useful data from licIn to LicOut
   511  	copyInputToLicense(&licIn, &licOut)
   512  	// build the license
   513  	err = buildLicense(&licOut, s, true)
   514  	if err != nil {
   515  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   516  		return
   517  	}
   518  	// build a protected publication
   519  	buf, err := buildProtectedPublication(&licOut, s)
   520  	if err == storage.ErrNotFound {
   521  		problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: licOut.ContentID}, http.StatusNotFound)
   522  		return
   523  	} else if err != nil {
   524  		problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: licOut.ContentID}, http.StatusInternalServerError)
   525  		return
   526  	}
   527  	// get the content location to fill an http header; this will be the name of the downloaded file.
   528  	content, err1 := s.Index().Get(licOut.ContentID)
   529  	if err1 != nil {
   530  		problem.Error(w, r, problem.Problem{Detail: err1.Error(), Instance: licOut.ContentID}, http.StatusInternalServerError)
   531  		return
   532  	}
   533  	location := content.Location
   534  
   535  	// set HTTP headers
   536  	w.Header().Add("Content-Type", epub.ContentType_EPUB)
   537  	w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, location))
   538  	// FIXME: check the use of X-Lcp-License by the caller (frontend?)
   539  	w.Header().Add("X-Lcp-License", licOut.ID)
   540  	// must come *after* w.Header().Add()/Set(), but before w.Write()
   541  	w.WriteHeader(http.StatusCreated)
   542  	// return the full protected publication to the caller
   543  	io.Copy(w, &buf)
   544  }
   545  
   546  // GenerateProtectedPublication generates and returns a protected publication
   547  // for a given content identified by its id
   548  // plus a partial license given as input
   549  func GenerateProtectedPublication(w http.ResponseWriter, r *http.Request, s Server) {
   550  
   551  	vars := mux.Vars(r)
   552  	contentID := vars["content_id"]
   553  
   554  	logging.Print("Generate a Licensed publication for Content " + contentID)
   555  
   556  	// get the input body
   557  	var lic license.License
   558  	err := DecodeJSONLicense(r, &lic)
   559  	if err != nil {
   560  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   561  		return
   562  	}
   563  	// check mandatory information in the input body
   564  	err = checkGenerateLicenseInput(&lic)
   565  	if err != nil {
   566  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   567  		return
   568  	}
   569  	// init the license with an id and issue date
   570  	license.Initialize(contentID, &lic)
   571  	// normalize the start and end date, UTC, no milliseconds
   572  	setRights(&lic)
   573  
   574  	// build the license
   575  	err = buildLicense(&lic, s, false)
   576  	if err != nil {
   577  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   578  		return
   579  	}
   580  	// store the license in the db
   581  	err = s.Licenses().Add(lic)
   582  	if err != nil {
   583  		problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: contentID}, http.StatusInternalServerError)
   584  		return
   585  	}
   586  
   587  	// notify the lsd server of the creation of the license
   588  	go notifyLsdServer(lic, s)
   589  
   590  	// build a licenced publication
   591  	buf, err := buildProtectedPublication(&lic, s)
   592  	if err == storage.ErrNotFound {
   593  		problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: lic.ContentID}, http.StatusNotFound)
   594  		return
   595  	} else if err != nil {
   596  		problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: lic.ContentID}, http.StatusInternalServerError)
   597  		return
   598  	}
   599  
   600  	// get the content location to fill an http header
   601  	content, err1 := s.Index().Get(lic.ContentID)
   602  	if err1 != nil {
   603  		problem.Error(w, r, problem.Problem{Detail: err1.Error(), Instance: lic.ContentID}, http.StatusInternalServerError)
   604  		return
   605  	}
   606  	location := content.Location
   607  
   608  	// set HTTP headers
   609  	w.Header().Add("Content-Type", epub.ContentType_EPUB)
   610  	w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, location))
   611  	// FIXME: check the use of X-Lcp-License by the caller (frontend?)
   612  	w.Header().Add("X-Lcp-License", lic.ID)
   613  	// must come *after* w.Header().Add()/Set(), but before w.Write()
   614  	w.WriteHeader(http.StatusCreated)
   615  	// return the full protected publication to the caller
   616  	io.Copy(w, &buf)
   617  }
   618  
   619  // UpdateLicense updates an existing license.
   620  // parameters:
   621  //
   622  //	{license_id} in the calling URL
   623  //	partial license containing properties which should be updated (and only these)
   624  //
   625  // return: an http status code (200, 400 or 404)
   626  // Usually called from the License Status Server after a renew, return or cancel/revoke action
   627  // -> updates the end date.
   628  func UpdateLicense(w http.ResponseWriter, r *http.Request, s Server) {
   629  
   630  	vars := mux.Vars(r)
   631  	// get the license id from the request URL
   632  	licenseID := vars["license_id"]
   633  
   634  	// add a log
   635  	logging.Print("Update the License " + licenseID)
   636  
   637  	var licIn license.License
   638  	err := DecodeJSONLicense(r, &licIn)
   639  	if err != nil { // no or incorrect (json) partial license found in the body
   640  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   641  		return
   642  	}
   643  	// initialize the license from the info stored in the db.
   644  	var licOut license.License
   645  	licOut, e := s.Licenses().Get(licenseID)
   646  	// process license not found etc.
   647  	if e == license.ErrNotFound {
   648  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusNotFound)
   649  		return
   650  	} else if e != nil {
   651  		problem.Error(w, r, problem.Problem{Detail: e.Error()}, http.StatusBadRequest)
   652  		return
   653  	}
   654  	// update licOut using information found in licIn
   655  	if licIn.User.ID != "" {
   656  		log.Println("new user id: ", licIn.User.ID)
   657  		licOut.User.ID = licIn.User.ID
   658  	}
   659  	if licIn.Provider != "" {
   660  		log.Println("new provider: ", licIn.Provider)
   661  		licOut.Provider = licIn.Provider
   662  	}
   663  	if licIn.ContentID != "" {
   664  		log.Println("new content id: ", licIn.ContentID)
   665  		licOut.ContentID = licIn.ContentID
   666  	}
   667  	if licIn.Rights.Print != nil {
   668  		log.Println("new right, print: ", *licIn.Rights.Print)
   669  		licOut.Rights.Print = licIn.Rights.Print
   670  	}
   671  	if licIn.Rights.Copy != nil {
   672  		log.Println("new right, copy: ", *licIn.Rights.Copy)
   673  		licOut.Rights.Copy = licIn.Rights.Copy
   674  	}
   675  	if licIn.Rights.Start != nil {
   676  		log.Println("new right, start: ", *licIn.Rights.Start)
   677  		licOut.Rights.Start = licIn.Rights.Start
   678  	}
   679  	if licIn.Rights.End != nil {
   680  		log.Println("new right, end: ", *licIn.Rights.End)
   681  		licOut.Rights.End = licIn.Rights.End
   682  	}
   683  	// update the license in the database
   684  	err = s.Licenses().Update(licOut)
   685  	if err != nil {
   686  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
   687  		return
   688  	}
   689  }
   690  
   691  // ListLicenses returns a JSON struct with information about the existing licenses
   692  // parameters:
   693  //
   694  //	page: page number
   695  //	per_page: number of items par page
   696  func ListLicenses(w http.ResponseWriter, r *http.Request, s Server) {
   697  
   698  	var page int64
   699  	var perPage int64
   700  	var err error
   701  	if r.FormValue("page") != "" {
   702  		page, err = strconv.ParseInt((r).FormValue("page"), 10, 32)
   703  		if err != nil {
   704  			problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   705  			return
   706  		}
   707  	} else {
   708  		page = 1
   709  	}
   710  	if r.FormValue("per_page") != "" {
   711  		perPage, err = strconv.ParseInt((r).FormValue("per_page"), 10, 32)
   712  		if err != nil {
   713  			problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   714  			return
   715  		}
   716  	} else {
   717  		perPage = 30
   718  	}
   719  	if page > 0 { //pagenum starting at 0 in code, but user interface starting at 1
   720  		page--
   721  	}
   722  	if page < 0 {
   723  		problem.Error(w, r, problem.Problem{Detail: "page must be positive integer"}, http.StatusBadRequest)
   724  		return
   725  	}
   726  	licenses := make([]license.LicenseReport, 0)
   727  
   728  	// add a log
   729  	logging.Print("List Licenses (page " + strconv.Itoa(int(page)) + ", count " + strconv.Itoa(int(perPage)) + ")")
   730  
   731  	fn := s.Licenses().ListAll(int(perPage), int(page))
   732  	for it, err := fn(); err == nil; it, err = fn() {
   733  		licenses = append(licenses, it)
   734  	}
   735  	if len(licenses) > 0 {
   736  		nextPage := strconv.Itoa(int(page) + 1)
   737  		w.Header().Set("Link", "</licenses/?page="+nextPage+">; rel=\"next\"; title=\"next\"")
   738  	}
   739  	if page > 1 {
   740  		previousPage := strconv.Itoa(int(page) - 1)
   741  		w.Header().Set("Link", "</licenses/?page="+previousPage+">; rel=\"previous\"; title=\"previous\"")
   742  	}
   743  	w.Header().Set("Content-Type", api.ContentType_JSON)
   744  
   745  	enc := json.NewEncoder(w)
   746  	// do not escape characters
   747  	enc.SetEscapeHTML(false)
   748  	err = enc.Encode(licenses)
   749  	if err != nil {
   750  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   751  		return
   752  	}
   753  }
   754  
   755  // ListLicensesForContent lists all licenses associated with a given content
   756  // parameters:
   757  //
   758  //	content_id: content identifier
   759  //	page: page number (default 1)
   760  //	per_page: number of items par page (default 30)
   761  func ListLicensesForContent(w http.ResponseWriter, r *http.Request, s Server) {
   762  
   763  	vars := mux.Vars(r)
   764  	var page int64
   765  	var perPage int64
   766  	var err error
   767  	contentID := vars["content_id"]
   768  
   769  	//check if the license exists
   770  	_, err = s.Index().Get(contentID)
   771  	if err == index.ErrNotFound {
   772  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
   773  		return
   774  	} //other errors pass, but will probably reoccur
   775  	if r.FormValue("page") != "" {
   776  		page, err = strconv.ParseInt(r.FormValue("page"), 10, 32)
   777  		if err != nil {
   778  			problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   779  			return
   780  		}
   781  	} else {
   782  		page = 1
   783  	}
   784  
   785  	if r.FormValue("per_page") != "" {
   786  		perPage, err = strconv.ParseInt((r).FormValue("per_page"), 10, 32)
   787  		if err != nil {
   788  			problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   789  			return
   790  		}
   791  	} else {
   792  		perPage = 30
   793  	}
   794  	if page > 0 {
   795  		page-- //pagenum starting at 0 in code, but user interface starting at 1
   796  	}
   797  	if page < 0 {
   798  		problem.Error(w, r, problem.Problem{Detail: "page must be positive integer"}, http.StatusBadRequest)
   799  		return
   800  	}
   801  	licenses := make([]license.LicenseReport, 0)
   802  
   803  	// add a log
   804  	logging.Print("List Licenses for publication " + contentID + " (page " + strconv.Itoa(int(page)) + ", count " + strconv.Itoa(int(perPage)) + ")")
   805  
   806  	fn := s.Licenses().ListByContentID(contentID, int(perPage), int(page))
   807  	for it, err := fn(); err == nil; it, err = fn() {
   808  		licenses = append(licenses, it)
   809  	}
   810  	if len(licenses) > 0 {
   811  		nextPage := strconv.Itoa(int(page) + 1)
   812  		w.Header().Set("Link", "</licenses/?page="+nextPage+">; rel=\"next\"; title=\"next\"")
   813  	}
   814  	if page > 1 {
   815  		previousPage := strconv.Itoa(int(page) - 1)
   816  		w.Header().Set("Link", "</licenses/?page="+previousPage+">; rel=\"previous\"; title=\"previous\"")
   817  	}
   818  	w.Header().Set("Content-Type", api.ContentType_JSON)
   819  	enc := json.NewEncoder(w)
   820  	// do not escape characters
   821  	enc.SetEscapeHTML(false)
   822  	err = enc.Encode(licenses)
   823  	if err != nil {
   824  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
   825  		return
   826  	}
   827  
   828  }
   829  
   830  // DecodeJSONLicense decodes a license formatted in json and returns a license object
   831  func DecodeJSONLicense(r *http.Request, lic *license.License) error {
   832  
   833  	var dec *json.Decoder
   834  
   835  	if ctype := r.Header["Content-Type"]; len(ctype) > 0 && ctype[0] == api.ContentType_FORM_URL_ENCODED {
   836  		buf := bytes.NewBufferString(r.PostFormValue("data"))
   837  		dec = json.NewDecoder(buf)
   838  	} else {
   839  		dec = json.NewDecoder(r.Body)
   840  	}
   841  
   842  	err := dec.Decode(&lic)
   843  
   844  	if err != nil && err.Error() != "EOF" {
   845  		log.Print("Decode license: invalid json structure")
   846  	}
   847  	return err
   848  }
   849  
   850  // notifyLsdServer informs the License Status Server of the creation of a new license
   851  // and saves the result of the http request in the DB (using *Store)
   852  func notifyLsdServer(l license.License, s Server) {
   853  
   854  	if config.Config.LsdServer.PublicBaseUrl != "" {
   855  		var lsdClient = &http.Client{
   856  			Timeout: time.Second * 10,
   857  		}
   858  		pr, pw := io.Pipe()
   859  		defer pr.Close()
   860  		go func() {
   861  			_ = json.NewEncoder(pw).Encode(l)
   862  			pw.Close() // signal end writing
   863  		}()
   864  		req, err := http.NewRequest("PUT", config.Config.LsdServer.PublicBaseUrl+"/licenses", pr)
   865  		if err != nil {
   866  			return
   867  		}
   868  		// set credentials on lsd request
   869  		notifyAuth := config.Config.LsdNotifyAuth
   870  		if notifyAuth.Username != "" {
   871  			req.SetBasicAuth(notifyAuth.Username, notifyAuth.Password)
   872  		}
   873  
   874  		req.Header.Add("Content-Type", api.ContentType_LCP_JSON)
   875  
   876  		response, err := lsdClient.Do(req)
   877  		if err != nil {
   878  			log.Println("Error Notify LsdServer of new License (" + l.ID + "):" + err.Error())
   879  			_ = s.Licenses().UpdateLsdStatus(l.ID, -1)
   880  		} else {
   881  			defer req.Body.Close()
   882  			_ = s.Licenses().UpdateLsdStatus(l.ID, int32(response.StatusCode))
   883  		}
   884  	}
   885  }