github.com/prebid/prebid-server/v2@v2.18.0/stored_requests/backends/http_fetcher/fetcher.go (about) 1 package http_fetcher 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "strings" 11 12 "github.com/prebid/prebid-server/v2/stored_requests" 13 "github.com/prebid/prebid-server/v2/util/jsonutil" 14 jsonpatch "gopkg.in/evanphx/json-patch.v4" 15 16 "github.com/golang/glog" 17 "golang.org/x/net/context/ctxhttp" 18 ) 19 20 // NewFetcher returns a Fetcher which uses the Client to pull data from the endpoint. 21 // 22 // This file expects the endpoint to satisfy the following API: 23 // 24 // Stored requests 25 // GET {endpoint}?request-ids=["req1","req2"]&imp-ids=["imp1","imp2","imp3"] 26 // 27 // Accounts 28 // GET {endpoint}?account-ids=["acc1","acc2"] 29 // 30 // The above endpoints should return a payload like: 31 // 32 // { 33 // "requests": { 34 // "req1": { ... stored data for req1 ... }, 35 // "req2": { ... stored data for req2 ... }, 36 // }, 37 // "imps": { 38 // "imp1": { ... stored data for imp1 ... }, 39 // "imp2": { ... stored data for imp2 ... }, 40 // "imp3": null // If imp3 is not found 41 // } 42 // } 43 // 44 // or 45 // 46 // { 47 // "accounts": { 48 // "acc1": { ... config data for acc1 ... }, 49 // "acc2": { ... config data for acc2 ... }, 50 // }, 51 // } 52 func NewFetcher(client *http.Client, endpoint string) *HttpFetcher { 53 // Do some work up-front to figure out if the (configurable) endpoint has a query string or not. 54 // When we build requests, we'll either want to add `?request-ids=...&imp-ids=...` _or_ 55 // `&request-ids=...&imp-ids=...`. 56 57 if _, err := url.Parse(endpoint); err != nil { 58 glog.Fatalf(`Invalid endpoint "%s": %v`, endpoint, err) 59 } 60 glog.Infof("Making http_fetcher for endpoint %v", endpoint) 61 62 urlPrefix := endpoint 63 if strings.Contains(endpoint, "?") { 64 urlPrefix = urlPrefix + "&" 65 } else { 66 urlPrefix = urlPrefix + "?" 67 } 68 69 return &HttpFetcher{ 70 client: client, 71 Endpoint: urlPrefix, 72 } 73 } 74 75 type HttpFetcher struct { 76 client *http.Client 77 Endpoint string 78 Categories map[string]map[string]stored_requests.Category 79 } 80 81 func (fetcher *HttpFetcher) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) { 82 if len(requestIDs) == 0 && len(impIDs) == 0 { 83 return nil, nil, nil 84 } 85 86 httpReq, err := buildRequest(fetcher.Endpoint, requestIDs, impIDs) 87 if err != nil { 88 return nil, nil, []error{err} 89 } 90 91 httpResp, err := ctxhttp.Do(ctx, fetcher.client, httpReq) 92 if err != nil { 93 return nil, nil, []error{err} 94 } 95 defer httpResp.Body.Close() 96 requestData, impData, errs = unpackResponse(httpResp) 97 return 98 } 99 100 func (fetcher *HttpFetcher) FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) { 101 return nil, nil 102 } 103 104 // FetchAccounts retrieves account configurations 105 // 106 // Request format is similar to the one for requests: 107 // GET {endpoint}?account-ids=["account1","account2",...] 108 // 109 // The endpoint is expected to respond with a JSON map with accountID -> json.RawMessage 110 // 111 // { 112 // "account1": { ... account json ... } 113 // } 114 // 115 // The JSON contents of account config is returned as-is (NOT validated) 116 func (fetcher *HttpFetcher) FetchAccounts(ctx context.Context, accountIDs []string) (map[string]json.RawMessage, []error) { 117 if len(accountIDs) == 0 { 118 return nil, nil 119 } 120 httpReq, err := http.NewRequestWithContext(ctx, "GET", fetcher.Endpoint+"account-ids=[\""+strings.Join(accountIDs, "\",\"")+"\"]", nil) 121 if err != nil { 122 return nil, []error{ 123 fmt.Errorf(`Error fetching accounts %v via http: build request failed with %v`, accountIDs, err), 124 } 125 } 126 httpResp, err := ctxhttp.Do(ctx, fetcher.client, httpReq) 127 if err != nil { 128 return nil, []error{ 129 fmt.Errorf(`Error fetching accounts %v via http: %v`, accountIDs, err), 130 } 131 } 132 defer httpResp.Body.Close() 133 respBytes, err := io.ReadAll(httpResp.Body) 134 if err != nil { 135 return nil, []error{ 136 fmt.Errorf(`Error fetching accounts %v via http: error reading response: %v`, accountIDs, err), 137 } 138 } 139 if httpResp.StatusCode != http.StatusOK { 140 return nil, []error{ 141 fmt.Errorf(`Error fetching accounts %v via http: unexpected response status %d`, accountIDs, httpResp.StatusCode), 142 } 143 } 144 var responseData accountsResponseContract 145 if err = jsonutil.UnmarshalValid(respBytes, &responseData); err != nil { 146 return nil, []error{ 147 fmt.Errorf(`Error fetching accounts %v via http: failed to parse response: %v`, accountIDs, err), 148 } 149 } 150 errs := convertNullsToErrs(responseData.Accounts, "Account", []error{}) 151 return responseData.Accounts, errs 152 } 153 154 // FetchAccount fetchers a single accountID and returns its corresponding json 155 func (fetcher *HttpFetcher) FetchAccount(ctx context.Context, accountDefaultsJSON json.RawMessage, accountID string) (accountJSON json.RawMessage, errs []error) { 156 accountData, errs := fetcher.FetchAccounts(ctx, []string{accountID}) 157 if len(errs) > 0 { 158 return nil, errs 159 } 160 accountJSON, ok := accountData[accountID] 161 if !ok { 162 return nil, []error{stored_requests.NotFoundError{ 163 ID: accountID, 164 DataType: "Account", 165 }} 166 } 167 completeJSON, err := jsonpatch.MergePatch(accountDefaultsJSON, accountJSON) 168 if err != nil { 169 return nil, []error{err} 170 } 171 return completeJSON, nil 172 } 173 174 func (fetcher *HttpFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { 175 if fetcher.Categories == nil { 176 fetcher.Categories = make(map[string]map[string]stored_requests.Category) 177 } 178 179 //in NewFetcher function there is a code to add "?" at the end of url 180 //in case of categories we don't expect to have any parameters, that's why we need to remove "?" 181 var dataName, url string 182 if publisherId != "" { 183 dataName = fmt.Sprintf("%s_%s", primaryAdServer, publisherId) 184 url = fmt.Sprintf("%s/%s/%s.json", strings.TrimSuffix(fetcher.Endpoint, "?"), primaryAdServer, publisherId) 185 } else { 186 dataName = primaryAdServer 187 url = fmt.Sprintf("%s/%s.json", strings.TrimSuffix(fetcher.Endpoint, "?"), primaryAdServer) 188 } 189 190 if data, ok := fetcher.Categories[dataName]; ok { 191 if val, ok := data[iabCategory]; ok { 192 return val.Id, nil 193 } else { 194 return "", fmt.Errorf("Unable to find category mapping for adserver: '%s', publisherId: '%s'", primaryAdServer, publisherId) 195 } 196 } 197 198 httpReq, err := http.NewRequest("GET", url, nil) 199 if err != nil { 200 return "", err 201 } 202 203 httpResp, err := ctxhttp.Do(ctx, fetcher.client, httpReq) 204 if err != nil { 205 return "", err 206 } 207 defer httpResp.Body.Close() 208 209 respBytes, err := io.ReadAll(httpResp.Body) 210 tmp := make(map[string]stored_requests.Category) 211 212 if err := jsonutil.UnmarshalValid(respBytes, &tmp); err != nil { 213 return "", fmt.Errorf("Unable to unmarshal categories for adserver: '%s', publisherId: '%s'", primaryAdServer, publisherId) 214 } 215 fetcher.Categories[dataName] = tmp 216 217 if val, ok := tmp[iabCategory]; ok { 218 return val.Id, nil 219 } else { 220 return "", fmt.Errorf("Unable to find category mapping for adserver: '%s', publisherId: '%s'", primaryAdServer, publisherId) 221 } 222 } 223 224 func buildRequest(endpoint string, requestIDs []string, impIDs []string) (*http.Request, error) { 225 if len(requestIDs) > 0 && len(impIDs) > 0 { 226 return http.NewRequest("GET", endpoint+"request-ids=[\""+strings.Join(requestIDs, "\",\"")+"\"]&imp-ids=[\""+strings.Join(impIDs, "\",\"")+"\"]", nil) 227 } else if len(requestIDs) > 0 { 228 return http.NewRequest("GET", endpoint+"request-ids=[\""+strings.Join(requestIDs, "\",\"")+"\"]", nil) 229 } else { 230 return http.NewRequest("GET", endpoint+"imp-ids=[\""+strings.Join(impIDs, "\",\"")+"\"]", nil) 231 } 232 } 233 234 func unpackResponse(resp *http.Response) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) { 235 respBytes, err := io.ReadAll(resp.Body) 236 if err != nil { 237 errs = append(errs, err) 238 return 239 } 240 241 if resp.StatusCode == http.StatusOK { 242 var responseObj responseContract 243 if err := jsonutil.UnmarshalValid(respBytes, &responseObj); err != nil { 244 errs = append(errs, err) 245 return 246 } 247 248 requestData = responseObj.Requests 249 impData = responseObj.Imps 250 251 errs = convertNullsToErrs(requestData, "Request", errs) 252 errs = convertNullsToErrs(impData, "Imp", errs) 253 254 return 255 } 256 257 errs = append(errs, fmt.Errorf("Error fetching Stored Requests via HTTP. Response code was %d", resp.StatusCode)) 258 return 259 } 260 261 func convertNullsToErrs(m map[string]json.RawMessage, dataType string, errs []error) []error { 262 for id, val := range m { 263 if val == nil { 264 delete(m, id) 265 errs = append(errs, stored_requests.NotFoundError{ 266 ID: id, 267 DataType: dataType, 268 }) 269 } 270 } 271 return errs 272 } 273 274 // responseContract is used to unmarshal for the endpoint 275 type responseContract struct { 276 Requests map[string]json.RawMessage `json:"requests"` 277 Imps map[string]json.RawMessage `json:"imps"` 278 } 279 280 type accountsResponseContract struct { 281 Accounts map[string]json.RawMessage `json:"accounts"` 282 }