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(&notifyURL, &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(&notifyURL, &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(&notifyURL, &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  }