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 }