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 }