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  }