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