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 }