github.com/readium/readium-lcp-server@v0.0.0-20240101192032-6e95190e99f1/encrypt/notify_server.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 encrypt 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "net/http" 12 "net/url" 13 "os" 14 "strings" 15 "time" 16 17 apilcp "github.com/readium/readium-lcp-server/lcpserver/api" 18 ) 19 20 // LCPServerMsgV2 is used for notifying an LCP Server V2 21 type LCPServerMsgV2 struct { 22 UUID string `json:"uuid"` 23 Title string `json:"title"` 24 EncryptionKey []byte `json:"encryption_key"` 25 Location string `json:"location"` 26 ContentType string `json:"content_type"` 27 Size uint32 `json:"size"` 28 Checksum string `json:"checksum"` 29 } 30 31 type Coded struct { 32 Code string `json:"code"` 33 } 34 35 type Entity struct { 36 Name string `json:"name"` 37 } 38 39 // CMSMsg is used for notifying a CMS 40 type CMSMsg struct { 41 UUID string `json:"uuid"` 42 Title string `json:"title"` 43 ContentType string `json:"content_type"` 44 DatePublished string `json:"date_published"` 45 Description string `json:"description"` 46 CoverUrl string `json:"cover_url,omitempty"` 47 Language []Coded `json:"language"` 48 Publisher []Entity `json:"publisher"` 49 Author []Entity `json:"author"` 50 Category []Entity `json:"category"` 51 } 52 53 // NotifyLCPServer notifies the License Server of the encryption of a publication 54 func NotifyLCPServer(pub Publication, lcpsv string, v2 bool, username string, password string, verbose bool) error { 55 56 // An empty notify URL is not an error, simply a silent encryption 57 if lcpsv == "" { 58 return nil 59 } 60 61 if !strings.HasPrefix(lcpsv, "http://") && !strings.HasPrefix(lcpsv, "https://") { 62 lcpsv = "http://" + lcpsv 63 } 64 var notifyURL string 65 var err error 66 if v2 { 67 notifyURL, err = url.JoinPath(lcpsv, "publications") 68 } else { 69 notifyURL, err = url.JoinPath(lcpsv, "contents", pub.UUID) 70 } 71 if err != nil { 72 return err 73 } 74 75 // look for the username and password in the url 76 err = getUsernamePassword(¬ifyURL, &username, &password) 77 if err != nil { 78 return err 79 } 80 81 var req *http.Request 82 var jsonBody []byte 83 84 // the payload sent to the server differs from v1 to v2 servers 85 if !v2 { 86 87 var msg apilcp.Encrypted 88 msg.ContentID = pub.UUID 89 msg.ContentKey = pub.EncryptionKey 90 msg.StorageMode = pub.StorageMode 91 msg.Output = pub.Location 92 msg.FileName = pub.FileName 93 msg.ContentType = pub.ContentType 94 msg.Size = int64(pub.Size) 95 msg.Checksum = pub.Checksum 96 97 jsonBody, err = json.Marshal(msg) 98 if err != nil { 99 return err 100 } 101 req, err = http.NewRequest("PUT", notifyURL, bytes.NewReader(jsonBody)) 102 if err != nil { 103 return err 104 } 105 } else { 106 var msg LCPServerMsgV2 107 msg.UUID = pub.UUID 108 msg.Title = pub.Title 109 msg.EncryptionKey = pub.EncryptionKey 110 msg.Location = pub.Location 111 msg.ContentType = pub.ContentType 112 msg.Size = pub.Size 113 msg.Checksum = pub.Checksum 114 115 jsonBody, err = json.Marshal(msg) 116 if err != nil { 117 return err 118 } 119 req, err = http.NewRequest("POST", notifyURL, bytes.NewReader(jsonBody)) 120 if err != nil { 121 return err 122 } 123 } 124 125 // verbose: log the notification 126 if verbose { 127 fmt.Println("LCP Server Notification:") 128 var out bytes.Buffer 129 json.Indent(&out, jsonBody, "", " ") 130 out.WriteTo(os.Stdout) 131 fmt.Println("") 132 } 133 134 req.SetBasicAuth(username, password) 135 client := &http.Client{ 136 Timeout: 15 * time.Second, 137 } 138 resp, err := client.Do(req) 139 if err != nil { 140 return err 141 } 142 if (resp.StatusCode != 302) && (resp.StatusCode/100) != 2 { //302=found or 20x reply = OK 143 return fmt.Errorf("the server returned an error %d", resp.StatusCode) 144 } 145 fmt.Println("The LCP Server was notified") 146 return nil 147 } 148 149 // AbortNotification rolls back the notification of the encryption of a publication 150 func AbortNotification(pub Publication, lcpsv string, v2 bool, username string, password string) error { 151 152 // An empty notify URL is not an error 153 if lcpsv == "" { 154 return nil 155 } 156 157 if !strings.HasPrefix(lcpsv, "http://") && !strings.HasPrefix(lcpsv, "https://") { 158 lcpsv = "http://" + lcpsv 159 } 160 var notifyURL string 161 var err error 162 if v2 { 163 notifyURL, err = url.JoinPath(lcpsv, "publications") 164 } else { 165 notifyURL, err = url.JoinPath(lcpsv, "contents", pub.UUID) 166 } 167 if err != nil { 168 return err 169 } 170 171 // look for the username and password in the url 172 err = getUsernamePassword(¬ifyURL, &username, &password) 173 if err != nil { 174 return err 175 } 176 177 req, err := http.NewRequest("DELETE", notifyURL, nil) 178 if err != nil { 179 return err 180 } 181 182 req.SetBasicAuth(username, password) 183 client := &http.Client{ 184 Timeout: 15 * time.Second, 185 } 186 resp, err := client.Do(req) 187 if err != nil { 188 return err 189 } 190 if (resp.StatusCode != 302) && (resp.StatusCode/100) != 2 { //302=found or 20x reply = OK 191 return fmt.Errorf("the server returned an error %d", resp.StatusCode) 192 } 193 fmt.Println("Encrypted publication deleted from the LCP Server") 194 return nil 195 } 196 197 // NotifyCMS notifies a CMS of the encryption of a publication 198 func NotifyCMS(pub Publication, notifyURL string, verbose bool) error { 199 200 // An empty notify URL is not an error, simply a silent encryption 201 if notifyURL == "" { 202 return nil 203 } 204 205 // look for the clientID and clientSecret in the url 206 var username, password string 207 err := getUsernamePassword(¬ifyURL, &username, &password) 208 if err != nil { 209 return err 210 } 211 212 // set the message sent to the CMS 213 var msg CMSMsg 214 msg.UUID = pub.UUID 215 msg.Title = pub.Title 216 msg.ContentType = pub.ContentType 217 msg.DatePublished = pub.Date 218 msg.Description = pub.Description 219 msg.CoverUrl = pub.CoverUrl 220 var lg Coded 221 for _, v := range pub.Language { 222 lg.Code = v 223 msg.Language = append(msg.Language, lg) 224 } 225 var en Entity 226 for _, en.Name = range pub.Publisher { 227 msg.Publisher = append(msg.Publisher, en) 228 } 229 for _, en.Name = range pub.Author { 230 msg.Author = append(msg.Author, en) 231 } 232 for _, en.Name = range pub.Subject { 233 msg.Category = append(msg.Category, en) 234 } 235 236 var req *http.Request 237 var jsonBody []byte 238 239 jsonBody, err = json.Marshal(msg) 240 if err != nil { 241 return err 242 } 243 req, err = http.NewRequest("POST", notifyURL, bytes.NewReader(jsonBody)) 244 if err != nil { 245 return err 246 } 247 248 // verbose: display the notification 249 if verbose { 250 fmt.Println("CMS Notification:") 251 var out bytes.Buffer 252 json.Indent(&out, jsonBody, "", " ") 253 out.WriteTo(os.Stdout) 254 fmt.Println("") 255 } 256 257 req.SetBasicAuth(username, password) 258 client := &http.Client{ 259 Timeout: 15 * time.Second, 260 } 261 resp, err := client.Do(req) 262 if err != nil { 263 return err 264 } 265 if (resp.StatusCode != 302) && (resp.StatusCode/100) != 2 { //302=found or 20x reply = OK 266 return fmt.Errorf("the server returned an error %d", resp.StatusCode) 267 } 268 fmt.Println("The CMS was notified") 269 return nil 270 } 271 272 // Look for the username and password in the url 273 func getUsernamePassword(notifyURL, username, password *string) error { 274 u, err := url.Parse(*notifyURL) 275 if err != nil { 276 return err 277 } 278 un := u.User.Username() 279 pw, pwfound := u.User.Password() 280 if un != "" && pwfound { 281 *username = un 282 *password = pw 283 u.User = nil 284 *notifyURL = u.String() // notifyURL is updated 285 } 286 return nil 287 }