github.com/readium/readium-lcp-server@v0.0.0-20240101192032-6e95190e99f1/lsdserver/api/freshlicense.go (about)

     1  // Copyright 2021 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 apilsd
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"errors"
    11  	"io/ioutil"
    12  	"log"
    13  	"net/http"
    14  	"strings"
    15  
    16  	"github.com/gorilla/mux"
    17  
    18  	"github.com/readium/readium-lcp-server/api"
    19  	"github.com/readium/readium-lcp-server/config"
    20  	"github.com/readium/readium-lcp-server/license"
    21  	"github.com/readium/readium-lcp-server/problem"
    22  )
    23  
    24  // UserData represents the payload requested to the CMS.
    25  // This is a simplified version of a partial license, easy to generate for any CMS developer.
    26  // PassphraseHash is the result of the hash calculation, as an hex-encoded string
    27  type UserData struct {
    28  	ID             string `json:"id"`
    29  	Name           string `json:"name"`
    30  	Email          string `json:"email"`
    31  	PassphraseHash string `json:"passphrasehash"`
    32  	Hint           string `json:"hint"`
    33  }
    34  
    35  const Sha256_URL string = "http://www.w3.org/2001/04/xmlenc#sha256"
    36  
    37  // GetFreshLicense gets a fresh license from the License Server
    38  // after requesting user data (for this license) from the CMS.
    39  func GetFreshLicense(w http.ResponseWriter, r *http.Request, s Server) {
    40  
    41  	// get the licenseID parameter
    42  	vars := mux.Vars(r)
    43  	licenseID := vars["key"]
    44  
    45  	// check if the license is known from the Status Document Server
    46  	statusDoc, err := s.LicenseStatuses().GetByLicenseID(licenseID)
    47  	if err != nil {
    48  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
    49  	}
    50  
    51  	// get a fresh license from the License Server (as []byte)
    52  	freshLicense, err := getLicense(licenseID)
    53  	if err != nil {
    54  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
    55  		return
    56  	}
    57  
    58  	// return the fresh license to the caller
    59  	w.Header().Set("Content-Type", api.ContentType_LCP_JSON)
    60  	w.Header().Set("Content-Disposition", "attachment; filename=\"license.lcpl\"")
    61  
    62  	_, err = w.Write(freshLicense)
    63  	if err != nil {
    64  		problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
    65  		return
    66  	}
    67  
    68  	log.Println("Get license / id " + licenseID + " / status " + statusDoc.Status)
    69  }
    70  
    71  // GetLicense gets a fresh license from the License Server
    72  func getLicense(licenseID string) (lic []byte, err error) {
    73  
    74  	// get user data from the CMS
    75  	var userData UserData
    76  	userData, err = getUserData(licenseID)
    77  	if err != nil {
    78  		return
    79  	}
    80  
    81  	// init the partial license to be sent to the License Server
    82  	var plic license.License
    83  	plic, err = initPartialLicense(licenseID, userData)
    84  	if err != nil {
    85  		return
    86  	}
    87  
    88  	// fetch the license from the License Server
    89  	lic, err = fetchLicense(plic)
    90  	if err != nil {
    91  		return
    92  	}
    93  	return
    94  }
    95  
    96  // getUserData gets user data from the CMS, as a partial license
    97  func getUserData(licenseID string) (userData UserData, err error) {
    98  
    99  	// get the url of the CMS
   100  	userURL := strings.Replace(config.Config.LsdServer.UserDataUrl, "{license_id}", licenseID, -1)
   101  	if userURL == "" {
   102  		err = errors.New("get User Data from License ID: UserDataUrl missing from the server configuration")
   103  		return
   104  	}
   105  
   106  	// fetch user data
   107  	client := &http.Client{}
   108  	req, err := http.NewRequest("GET", userURL, nil)
   109  	if err != nil {
   110  		return
   111  	}
   112  	auth := config.Config.CMSAccessAuth
   113  	if auth.Username != "" {
   114  		req.SetBasicAuth(auth.Username, auth.Password)
   115  	}
   116  	resp, err := client.Do(req)
   117  	if err != nil {
   118  		return
   119  	}
   120  	defer resp.Body.Close()
   121  
   122  	dec := json.NewDecoder(resp.Body)
   123  	if resp.StatusCode != 200 {
   124  		var errStatus problem.Problem
   125  		err = dec.Decode(&errStatus)
   126  		if err != nil {
   127  			return
   128  		}
   129  		err = errors.New("Get User Data from License ID: " + errStatus.Title + " - " + errStatus.Detail)
   130  		return
   131  	} else {
   132  		// decode user data
   133  		err = dec.Decode(&userData)
   134  		if err != nil {
   135  			return
   136  		}
   137  	}
   138  
   139  	return
   140  }
   141  
   142  // initPartialLicense inits the partial license to be sent to the License Server
   143  func initPartialLicense(licenseID string, userData UserData) (plic license.License, err error) {
   144  	plic.ID = licenseID
   145  	plic.User.ID = userData.ID
   146  	plic.User.Name = userData.Name
   147  	plic.User.Email = userData.Email
   148  	// we decide that name and email will be encrypted
   149  	encryptedAttrs := []string{"name", "email"}
   150  	plic.User.Encrypted = encryptedAttrs
   151  
   152  	plic.Encryption.UserKey.Algorithm = Sha256_URL
   153  	plic.Encryption.UserKey.Hint = userData.Hint
   154  	plic.Encryption.UserKey.HexValue = userData.PassphraseHash
   155  	return
   156  }
   157  
   158  // fetchLicense fetches a license from the License Server
   159  func fetchLicense(plic license.License) (lic []byte, err error) {
   160  	// json encode the partial license
   161  	jplic, err := json.Marshal(plic)
   162  	if err != nil {
   163  		return
   164  	}
   165  
   166  	// send the partial license to the License Server and get back a fresh license
   167  	licenseUrl := config.Config.LcpServer.PublicBaseUrl + "/licenses/" + plic.ID
   168  	client := &http.Client{}
   169  	req, err := http.NewRequest("POST", licenseUrl, bytes.NewReader(jplic))
   170  	if err != nil {
   171  		return
   172  	}
   173  	auth := config.Config.LcpUpdateAuth
   174  	if auth.Username != "" {
   175  		req.SetBasicAuth(auth.Username, auth.Password)
   176  	}
   177  	req.Header.Add("Content-Type", api.ContentType_LCP_JSON)
   178  	resp, err := client.Do(req)
   179  	if err != nil {
   180  		return
   181  	}
   182  	defer resp.Body.Close()
   183  
   184  	if resp.StatusCode != 200 {
   185  		dec := json.NewDecoder(resp.Body)
   186  		var errStatus problem.Problem
   187  		err = dec.Decode(&errStatus)
   188  		if err != nil {
   189  			return
   190  		}
   191  		err = errors.New("Fetch License: " + errStatus.Title + " - " + errStatus.Detail)
   192  		return
   193  	} else {
   194  		// return the json body as []byte
   195  		lic, err = ioutil.ReadAll(resp.Body)
   196  		if err != nil {
   197  			return
   198  		}
   199  	}
   200  
   201  	return
   202  }