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