github.com/prebid/prebid-server/v2@v2.18.0/stored_requests/events/http/http.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	httpCore "net/http"
     9  	"net/url"
    10  	"time"
    11  
    12  	"golang.org/x/net/context/ctxhttp"
    13  
    14  	"github.com/buger/jsonparser"
    15  	"github.com/prebid/prebid-server/v2/stored_requests/events"
    16  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    17  
    18  	"github.com/golang/glog"
    19  )
    20  
    21  // NewHTTPEvents makes an EventProducer which creates events by pinging an external HTTP API
    22  // for updates periodically. If refreshRate is negative, then the data will never be refreshed.
    23  //
    24  // It expects the following endpoint to exist remotely:
    25  //
    26  // GET {endpoint}
    27  //
    28  //	-- Returns all the known Stored Requests and Stored Imps.
    29  //
    30  // GET {endpoint}?last-modified={timestamp}
    31  //
    32  //	-- Returns the Stored Requests and Stored Imps which have been updated since the last timestamp.
    33  //	   This timestamp will be sent in the rfc3339 format, using UTC and no timezone shift.
    34  //	   For more info, see: https://tools.ietf.org/html/rfc3339
    35  //
    36  // The responses should be JSON like this:
    37  //
    38  //	{
    39  //	  "requests": {
    40  //	    "request1": { ... stored request data ... },
    41  //	    "request2": { ... stored request data ... },
    42  //	    "request3": { ... stored request data ... },
    43  //	  },
    44  //	  "imps": {
    45  //	    "imp1": { ... stored data for imp1 ... },
    46  //	    "imp2": { ... stored data for imp2 ... },
    47  //	  },
    48  //	  "responses": {
    49  //	    "resp1": { ... stored data for resp1 ... },
    50  //	    "resp2": { ... stored data for resp2 ... },
    51  //	  }
    52  //	}
    53  //
    54  // or
    55  //
    56  //	{
    57  //	  "accounts": {
    58  //	    "acc1": { ... config data for acc1 ... },
    59  //	    "acc2": { ... config data for acc2 ... },
    60  //	  },
    61  //	}
    62  //
    63  // To signal deletions, the endpoint may return { "deleted": true }
    64  // in place of the Stored Data if the "last-modified" param existed.
    65  func NewHTTPEvents(client *httpCore.Client, endpoint string, ctxProducer func() (ctx context.Context, canceller func()), refreshRate time.Duration) *HTTPEvents {
    66  	// If we're not given a function to produce Contexts, use the Background one.
    67  	if ctxProducer == nil {
    68  		ctxProducer = func() (ctx context.Context, canceller func()) {
    69  			return context.Background(), func() {}
    70  		}
    71  	}
    72  	e := &HTTPEvents{
    73  		client:        client,
    74  		ctxProducer:   ctxProducer,
    75  		Endpoint:      endpoint,
    76  		lastUpdate:    time.Now().UTC(),
    77  		saves:         make(chan events.Save, 1),
    78  		invalidations: make(chan events.Invalidation, 1),
    79  	}
    80  	glog.Infof("Loading HTTP cache from GET %s", endpoint)
    81  	e.fetchAll()
    82  
    83  	go e.refresh(time.Tick(refreshRate))
    84  	return e
    85  }
    86  
    87  type HTTPEvents struct {
    88  	client        *httpCore.Client
    89  	ctxProducer   func() (ctx context.Context, canceller func())
    90  	Endpoint      string
    91  	invalidations chan events.Invalidation
    92  	lastUpdate    time.Time
    93  	saves         chan events.Save
    94  }
    95  
    96  func (e *HTTPEvents) fetchAll() {
    97  	ctx, cancel := e.ctxProducer()
    98  	defer cancel()
    99  	resp, err := ctxhttp.Get(ctx, e.client, e.Endpoint)
   100  	if respObj, ok := e.parse(e.Endpoint, resp, err); ok &&
   101  		(len(respObj.StoredRequests) > 0 || len(respObj.StoredImps) > 0 || len(respObj.StoredResponses) > 0 || len(respObj.Accounts) > 0) {
   102  		e.saves <- events.Save{
   103  			Requests:  respObj.StoredRequests,
   104  			Imps:      respObj.StoredImps,
   105  			Responses: respObj.StoredResponses,
   106  			Accounts:  respObj.Accounts,
   107  		}
   108  	}
   109  }
   110  
   111  func (e *HTTPEvents) refresh(ticker <-chan time.Time) {
   112  	for thisTime := range ticker {
   113  		thisTimeInUTC := thisTime.UTC()
   114  
   115  		// Parse the endpoint url defined
   116  		endpointUrl, urlErr := url.Parse(e.Endpoint)
   117  
   118  		// Error with url parsing
   119  		if urlErr != nil {
   120  			glog.Errorf("Disabling refresh HTTP cache from GET '%s': %v", e.Endpoint, urlErr)
   121  			return
   122  		}
   123  
   124  		// Parse the url query string
   125  		urlQuery := endpointUrl.Query()
   126  
   127  		// See the last-modified query param
   128  		urlQuery.Set("last-modified", e.lastUpdate.Format(time.RFC3339))
   129  
   130  		// Rebuild
   131  		endpointUrl.RawQuery = urlQuery.Encode()
   132  
   133  		// Convert to string
   134  		endpoint := endpointUrl.String()
   135  
   136  		glog.Infof("Refreshing HTTP cache from GET '%s'", endpoint)
   137  
   138  		ctx, cancel := e.ctxProducer()
   139  		resp, err := ctxhttp.Get(ctx, e.client, endpoint)
   140  		if respObj, ok := e.parse(endpoint, resp, err); ok {
   141  			invalidations := events.Invalidation{
   142  				Requests:  extractInvalidations(respObj.StoredRequests),
   143  				Imps:      extractInvalidations(respObj.StoredImps),
   144  				Responses: extractInvalidations(respObj.StoredResponses),
   145  				Accounts:  extractInvalidations(respObj.Accounts),
   146  			}
   147  			if len(respObj.StoredRequests) > 0 || len(respObj.StoredImps) > 0 || len(respObj.StoredResponses) > 0 || len(respObj.Accounts) > 0 {
   148  				e.saves <- events.Save{
   149  					Requests:  respObj.StoredRequests,
   150  					Imps:      respObj.StoredImps,
   151  					Responses: respObj.StoredResponses,
   152  					Accounts:  respObj.Accounts,
   153  				}
   154  			}
   155  			if len(invalidations.Requests) > 0 || len(invalidations.Imps) > 0 || len(invalidations.Responses) > 0 || len(invalidations.Accounts) > 0 {
   156  				e.invalidations <- invalidations
   157  			}
   158  			e.lastUpdate = thisTimeInUTC
   159  		}
   160  		cancel()
   161  	}
   162  }
   163  
   164  // parse unpacks the HTTP response and sends the relevant events to the channels.
   165  // It returns true if everything was successful, and false if any errors occurred.
   166  func (e *HTTPEvents) parse(endpoint string, resp *httpCore.Response, err error) (*responseContract, bool) {
   167  	if err != nil {
   168  		glog.Errorf("Failed call: GET %s for Stored Requests: %v", endpoint, err)
   169  		return nil, false
   170  	}
   171  	defer resp.Body.Close()
   172  
   173  	respBytes, err := io.ReadAll(resp.Body)
   174  	if err != nil {
   175  		glog.Errorf("Failed to read body of GET %s for Stored Requests: %v", endpoint, err)
   176  		return nil, false
   177  	}
   178  
   179  	if resp.StatusCode != httpCore.StatusOK {
   180  		glog.Errorf("Got %d response from GET %s for Stored Requests. Response body was: %s", resp.StatusCode, endpoint, string(respBytes))
   181  		return nil, false
   182  	}
   183  
   184  	var respObj responseContract
   185  	if err := jsonutil.UnmarshalValid(respBytes, &respObj); err != nil {
   186  		glog.Errorf("Failed to unmarshal body of GET %s for Stored Requests: %v", endpoint, err)
   187  		return nil, false
   188  	}
   189  
   190  	return &respObj, true
   191  }
   192  
   193  func extractInvalidations(changes map[string]json.RawMessage) []string {
   194  	deletedIDs := make([]string, 0, len(changes))
   195  	for id, msg := range changes {
   196  		if value, _, _, err := jsonparser.Get(msg, "deleted"); err == nil && bytes.Equal(value, []byte("true")) {
   197  			delete(changes, id)
   198  			deletedIDs = append(deletedIDs, id)
   199  		}
   200  	}
   201  	return deletedIDs
   202  }
   203  
   204  func (e *HTTPEvents) Saves() <-chan events.Save {
   205  	return e.saves
   206  }
   207  
   208  func (e *HTTPEvents) Invalidations() <-chan events.Invalidation {
   209  	return e.invalidations
   210  }
   211  
   212  type responseContract struct {
   213  	StoredRequests  map[string]json.RawMessage `json:"requests"`
   214  	StoredImps      map[string]json.RawMessage `json:"imps"`
   215  	StoredResponses map[string]json.RawMessage `json:"responses"`
   216  	Accounts        map[string]json.RawMessage `json:"accounts"`
   217  }