github.com/prebid/prebid-server/v2@v2.18.0/router/router.go (about)

     1  package router
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	analyticsBuild "github.com/prebid/prebid-server/v2/analytics/build"
    14  	"github.com/prebid/prebid-server/v2/config"
    15  	"github.com/prebid/prebid-server/v2/currency"
    16  	"github.com/prebid/prebid-server/v2/endpoints"
    17  	"github.com/prebid/prebid-server/v2/endpoints/events"
    18  	infoEndpoints "github.com/prebid/prebid-server/v2/endpoints/info"
    19  	"github.com/prebid/prebid-server/v2/endpoints/openrtb2"
    20  	"github.com/prebid/prebid-server/v2/errortypes"
    21  	"github.com/prebid/prebid-server/v2/exchange"
    22  	"github.com/prebid/prebid-server/v2/experiment/adscert"
    23  	"github.com/prebid/prebid-server/v2/floors"
    24  	"github.com/prebid/prebid-server/v2/gdpr"
    25  	"github.com/prebid/prebid-server/v2/hooks"
    26  	"github.com/prebid/prebid-server/v2/macros"
    27  	"github.com/prebid/prebid-server/v2/metrics"
    28  	metricsConf "github.com/prebid/prebid-server/v2/metrics/config"
    29  	"github.com/prebid/prebid-server/v2/modules"
    30  	"github.com/prebid/prebid-server/v2/modules/moduledeps"
    31  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    32  	"github.com/prebid/prebid-server/v2/pbs"
    33  	pbc "github.com/prebid/prebid-server/v2/prebid_cache_client"
    34  	"github.com/prebid/prebid-server/v2/router/aspects"
    35  	"github.com/prebid/prebid-server/v2/server/ssl"
    36  	storedRequestsConf "github.com/prebid/prebid-server/v2/stored_requests/config"
    37  	"github.com/prebid/prebid-server/v2/usersync"
    38  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    39  	"github.com/prebid/prebid-server/v2/util/uuidutil"
    40  	"github.com/prebid/prebid-server/v2/version"
    41  
    42  	_ "github.com/go-sql-driver/mysql"
    43  	"github.com/golang/glog"
    44  	"github.com/julienschmidt/httprouter"
    45  	_ "github.com/lib/pq"
    46  	"github.com/rs/cors"
    47  )
    48  
    49  // NewJsonDirectoryServer is used to serve .json files from a directory as a single blob. For example,
    50  // given a directory containing the files "a.json" and "b.json", this returns a Handle which serves JSON like:
    51  //
    52  //	{
    53  //	  "a": { ... content from the file a.json ... },
    54  //	  "b": { ... content from the file b.json ... }
    55  //	}
    56  //
    57  // This function stores the file contents in memory, and should not be used on large directories.
    58  // If the root directory, or any of the files in it, cannot be read, then the program will exit.
    59  func NewJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.BidderParamValidator, aliases map[string]string) httprouter.Handle {
    60  	return newJsonDirectoryServer(schemaDirectory, validator, aliases, openrtb_ext.GetAliasBidderToParent())
    61  }
    62  
    63  func newJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.BidderParamValidator, aliases map[string]string, yamlAliases map[openrtb_ext.BidderName]openrtb_ext.BidderName) httprouter.Handle {
    64  	// Slurp the files into memory first, since they're small and it minimizes request latency.
    65  	files, err := os.ReadDir(schemaDirectory)
    66  	if err != nil {
    67  		glog.Fatalf("Failed to read directory %s: %v", schemaDirectory, err)
    68  	}
    69  
    70  	bidderMap := openrtb_ext.BuildBidderMap()
    71  
    72  	data := make(map[string]json.RawMessage, len(files))
    73  	for _, file := range files {
    74  		bidder := strings.TrimSuffix(file.Name(), ".json")
    75  		bidderName, isValid := bidderMap[bidder]
    76  		if !isValid {
    77  			glog.Fatalf("Schema exists for an unknown bidder: %s", bidder)
    78  		}
    79  		data[bidder] = json.RawMessage(validator.Schema(bidderName))
    80  	}
    81  
    82  	// Add in any aliases
    83  	for aliasName, parentBidder := range yamlAliases {
    84  		data[string(aliasName)] = json.RawMessage(validator.Schema(parentBidder))
    85  	}
    86  
    87  	// Add in any default aliases
    88  	for aliasName, bidderName := range aliases {
    89  		bidderData, ok := data[bidderName]
    90  		if !ok {
    91  			glog.Fatalf("Default alias (%s) exists referencing unknown bidder: %s", aliasName, bidderName)
    92  		}
    93  		data[aliasName] = bidderData
    94  	}
    95  
    96  	response, err := jsonutil.Marshal(data)
    97  	if err != nil {
    98  		glog.Fatalf("Failed to marshal bidder param JSON-schema: %v", err)
    99  	}
   100  
   101  	return func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
   102  		w.Header().Add("Content-Type", "application/json")
   103  		w.Write(response)
   104  	}
   105  }
   106  
   107  func serveIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
   108  	http.ServeFile(w, r, "static/index.html")
   109  }
   110  
   111  type NoCache struct {
   112  	Handler http.Handler
   113  }
   114  
   115  func (m NoCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   116  	w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
   117  	w.Header().Add("Pragma", "no-cache")
   118  	w.Header().Add("Expires", "0")
   119  	m.Handler.ServeHTTP(w, r)
   120  }
   121  
   122  type Router struct {
   123  	*httprouter.Router
   124  	MetricsEngine   *metricsConf.DetailedMetricsEngine
   125  	ParamsValidator openrtb_ext.BidderParamValidator
   126  	Shutdown        func()
   127  }
   128  
   129  func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *Router, err error) {
   130  	const schemaDirectory = "./static/bidder-params"
   131  
   132  	r = &Router{
   133  		Router: httprouter.New(),
   134  	}
   135  
   136  	// For bid processing, we need both the hardcoded certificates and the certificates found in container's
   137  	// local file system
   138  	certPool := ssl.GetRootCAPool()
   139  	var readCertErr error
   140  	certPool, readCertErr = ssl.AppendPEMFileToRootCAPool(certPool, cfg.PemCertsFile)
   141  	if readCertErr != nil {
   142  		glog.Infof("Could not read certificates file: %s \n", readCertErr.Error())
   143  	}
   144  
   145  	generalHttpClient := &http.Client{
   146  		Transport: &http.Transport{
   147  			Proxy:               http.ProxyFromEnvironment,
   148  			MaxConnsPerHost:     cfg.Client.MaxConnsPerHost,
   149  			MaxIdleConns:        cfg.Client.MaxIdleConns,
   150  			MaxIdleConnsPerHost: cfg.Client.MaxIdleConnsPerHost,
   151  			IdleConnTimeout:     time.Duration(cfg.Client.IdleConnTimeout) * time.Second,
   152  			TLSClientConfig:     &tls.Config{RootCAs: certPool},
   153  		},
   154  	}
   155  
   156  	cacheHttpClient := &http.Client{
   157  		Transport: &http.Transport{
   158  			Proxy:               http.ProxyFromEnvironment,
   159  			MaxConnsPerHost:     cfg.CacheClient.MaxConnsPerHost,
   160  			MaxIdleConns:        cfg.CacheClient.MaxIdleConns,
   161  			MaxIdleConnsPerHost: cfg.CacheClient.MaxIdleConnsPerHost,
   162  			IdleConnTimeout:     time.Duration(cfg.CacheClient.IdleConnTimeout) * time.Second,
   163  		},
   164  	}
   165  
   166  	floorFechterHttpClient := &http.Client{
   167  		Transport: &http.Transport{
   168  			Proxy:               http.ProxyFromEnvironment,
   169  			MaxConnsPerHost:     cfg.PriceFloors.Fetcher.HttpClient.MaxConnsPerHost,
   170  			MaxIdleConns:        cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConns,
   171  			MaxIdleConnsPerHost: cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConnsPerHost,
   172  			IdleConnTimeout:     time.Duration(cfg.PriceFloors.Fetcher.HttpClient.IdleConnTimeout) * time.Second,
   173  		},
   174  	}
   175  
   176  	if err := checkSupportedUserSyncEndpoints(cfg.BidderInfos); err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	syncersByBidder, errs := usersync.BuildSyncers(cfg, cfg.BidderInfos)
   181  	if len(errs) > 0 {
   182  		return nil, errortypes.NewAggregateError("user sync", errs)
   183  	}
   184  
   185  	syncerKeys := make([]string, 0, len(syncersByBidder))
   186  	syncerKeysHashSet := map[string]struct{}{}
   187  	for _, syncer := range syncersByBidder {
   188  		syncerKeysHashSet[syncer.Key()] = struct{}{}
   189  	}
   190  	for k := range syncerKeysHashSet {
   191  		syncerKeys = append(syncerKeys, k)
   192  	}
   193  
   194  	moduleDeps := moduledeps.ModuleDeps{HTTPClient: generalHttpClient, RateConvertor: rateConvertor}
   195  	repo, moduleStageNames, err := modules.NewBuilder().Build(cfg.Hooks.Modules, moduleDeps)
   196  	if err != nil {
   197  		glog.Fatalf("Failed to init hook modules: %v", err)
   198  	}
   199  
   200  	// Metrics engine
   201  	r.MetricsEngine = metricsConf.NewMetricsEngine(cfg, openrtb_ext.CoreBidderNames(), syncerKeys, moduleStageNames)
   202  	shutdown, fetcher, ampFetcher, accounts, categoriesFetcher, videoFetcher, storedRespFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, generalHttpClient, r.Router)
   203  	// todo(zachbadgett): better shutdown
   204  	r.Shutdown = shutdown
   205  
   206  	analyticsRunner := analyticsBuild.New(&cfg.Analytics)
   207  
   208  	paramsValidator, err := openrtb_ext.NewBidderParamsValidator(schemaDirectory)
   209  	if err != nil {
   210  		glog.Fatalf("Failed to create the bidder params validator. %v", err)
   211  	}
   212  
   213  	activeBidders := exchange.GetActiveBidders(cfg.BidderInfos)
   214  	disabledBidders := exchange.GetDisabledBidderWarningMessages(cfg.BidderInfos)
   215  
   216  	defaultAliases, defReqJSON := readDefaultRequest(cfg.DefReqConfig)
   217  	if err := validateDefaultAliases(defaultAliases); err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	gvlVendorIDs := cfg.BidderInfos.ToGVLVendorIDMap()
   222  	vendorListFetcher := gdpr.NewVendorListFetcher(context.Background(), cfg.GDPR, generalHttpClient, gdpr.VendorListURLMaker)
   223  	gdprPermsBuilder := gdpr.NewPermissionsBuilder(cfg.GDPR, gvlVendorIDs, vendorListFetcher)
   224  	tcf2CfgBuilder := gdpr.NewTCF2Config
   225  
   226  	cacheClient := pbc.NewClient(cacheHttpClient, &cfg.CacheURL, &cfg.ExtCacheURL, r.MetricsEngine)
   227  
   228  	adapters, adaptersErrs := exchange.BuildAdapters(generalHttpClient, cfg, cfg.BidderInfos, r.MetricsEngine)
   229  	if len(adaptersErrs) > 0 {
   230  		errs := errortypes.NewAggregateError("Failed to initialize adapters", adaptersErrs)
   231  		return nil, errs
   232  	}
   233  	adsCertSigner, err := adscert.NewAdCertsSigner(cfg.Experiment.AdCerts)
   234  	if err != nil {
   235  		glog.Fatalf("Failed to create ads cert signer: %v", err)
   236  	}
   237  
   238  	priceFloorFetcher := floors.NewPriceFloorFetcher(cfg.PriceFloors, floorFechterHttpClient, r.MetricsEngine)
   239  
   240  	tmaxAdjustments := exchange.ProcessTMaxAdjustments(cfg.TmaxAdjustments)
   241  	planBuilder := hooks.NewExecutionPlanBuilder(cfg.Hooks, repo)
   242  	macroReplacer := macros.NewStringIndexBasedReplacer()
   243  	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner, macroReplacer, priceFloorFetcher)
   244  	var uuidGenerator uuidutil.UUIDRandomGenerator
   245  	openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
   246  	if err != nil {
   247  		glog.Fatalf("Failed to create the openrtb2 endpoint handler. %v", err)
   248  	}
   249  
   250  	ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
   251  	if err != nil {
   252  		glog.Fatalf("Failed to create the amp endpoint handler. %v", err)
   253  	}
   254  
   255  	videoEndpoint, err := openrtb2.NewVideoEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, cacheClient, tmaxAdjustments)
   256  	if err != nil {
   257  		glog.Fatalf("Failed to create the video endpoint handler. %v", err)
   258  	}
   259  
   260  	requestTimeoutHeaders := config.RequestTimeoutHeaders{}
   261  	if cfg.RequestTimeoutHeaders != requestTimeoutHeaders {
   262  		videoEndpoint = aspects.QueuedRequestTimeout(videoEndpoint, cfg.RequestTimeoutHeaders, r.MetricsEngine, metrics.ReqTypeVideo)
   263  	}
   264  
   265  	r.POST("/openrtb2/auction", openrtbEndpoint)
   266  	r.POST("/openrtb2/video", videoEndpoint)
   267  	r.GET("/openrtb2/amp", ampEndpoint)
   268  	r.GET("/info/bidders", infoEndpoints.NewBiddersEndpoint(cfg.BidderInfos, defaultAliases))
   269  	r.GET("/info/bidders/:bidderName", infoEndpoints.NewBiddersDetailEndpoint(cfg.BidderInfos, defaultAliases))
   270  	r.GET("/bidders/params", NewJsonDirectoryServer(schemaDirectory, paramsValidator, defaultAliases))
   271  	r.POST("/cookie_sync", endpoints.NewCookieSyncEndpoint(syncersByBidder, cfg, gdprPermsBuilder, tcf2CfgBuilder, r.MetricsEngine, analyticsRunner, accounts, activeBidders).Handle)
   272  	r.GET("/status", endpoints.NewStatusEndpoint(cfg.StatusResponse))
   273  	r.GET("/", serveIndex)
   274  	r.Handler("GET", "/version", endpoints.NewVersionEndpoint(version.Ver, version.Rev))
   275  	r.ServeFiles("/static/*filepath", http.Dir("static"))
   276  
   277  	// vtrack endpoint
   278  	if cfg.VTrack.Enabled {
   279  		vtrackEndpoint := events.NewVTrackEndpoint(cfg, accounts, cacheClient, cfg.BidderInfos, r.MetricsEngine)
   280  		r.POST("/vtrack", vtrackEndpoint)
   281  	}
   282  
   283  	// event endpoint
   284  	eventEndpoint := events.NewEventEndpoint(cfg, accounts, analyticsRunner, r.MetricsEngine)
   285  	r.GET("/event", eventEndpoint)
   286  
   287  	userSyncDeps := &pbs.UserSyncDeps{
   288  		HostCookieConfig: &(cfg.HostCookie),
   289  		ExternalUrl:      cfg.ExternalURL,
   290  		RecaptchaSecret:  cfg.RecaptchaSecret,
   291  		PriorityGroups:   cfg.UserSync.PriorityGroups,
   292  	}
   293  
   294  	r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg, syncersByBidder, gdprPermsBuilder, tcf2CfgBuilder, analyticsRunner, accounts, r.MetricsEngine))
   295  	r.GET("/getuids", endpoints.NewGetUIDsEndpoint(cfg.HostCookie))
   296  	r.POST("/optout", userSyncDeps.OptOut)
   297  	r.GET("/optout", userSyncDeps.OptOut)
   298  
   299  	return r, nil
   300  }
   301  
   302  func checkSupportedUserSyncEndpoints(bidderInfos config.BidderInfos) error {
   303  	for name, info := range bidderInfos {
   304  		if info.Syncer == nil {
   305  			continue
   306  		}
   307  
   308  		for _, endpoint := range info.Syncer.Supports {
   309  			endpointLower := strings.ToLower(endpoint)
   310  			switch endpointLower {
   311  			case "iframe":
   312  				if info.Syncer.IFrame == nil {
   313  					glog.Warningf("bidder %s supports iframe user sync, but doesn't have a default and must be configured by the host", name)
   314  				}
   315  			case "redirect":
   316  				if info.Syncer.Redirect == nil {
   317  					glog.Warningf("bidder %s supports redirect user sync, but doesn't have a default and must be configured by the host", name)
   318  				}
   319  			default:
   320  				return fmt.Errorf("failed to load bidder info for %s, user sync supported endpoint '%s' is unrecognized", name, endpoint)
   321  			}
   322  		}
   323  	}
   324  	return nil
   325  }
   326  
   327  // Fixes #648
   328  //
   329  // These CORS options pose a security risk... but it's a calculated one.
   330  // People _must_ call us with "withCredentials" set to "true" because that's how we use the cookie sync info.
   331  // We also must allow all origins because every site on the internet _could_ call us.
   332  //
   333  // This is an inherent security risk. However, PBS doesn't use cookies for authorization--just identification.
   334  // We only store the User's ID for each Bidder, and each Bidder has already exposed a public cookie sync endpoint
   335  // which returns that data anyway.
   336  //
   337  // For more info, see:
   338  //
   339  // - https://github.com/rs/cors/issues/55
   340  // - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials
   341  // - https://portswigger.net/blog/exploiting-cors-misconfigurations-for-bitcoins-and-bounties
   342  func SupportCORS(handler http.Handler) http.Handler {
   343  	c := cors.New(cors.Options{
   344  		AllowCredentials: true,
   345  		AllowOriginFunc: func(string) bool {
   346  			return true
   347  		},
   348  		AllowedHeaders: []string{"Origin", "X-Requested-With", "Content-Type", "Accept"}})
   349  	return c.Handler(handler)
   350  }
   351  
   352  type defReq struct {
   353  	Ext defExt `json:"ext"`
   354  }
   355  type defExt struct {
   356  	Prebid defaultAliases `json:"prebid"`
   357  }
   358  type defaultAliases struct {
   359  	Aliases map[string]string `json:"aliases"`
   360  }
   361  
   362  func readDefaultRequest(defReqConfig config.DefReqConfig) (map[string]string, []byte) {
   363  	defReq := &defReq{}
   364  	aliases := make(map[string]string)
   365  	if defReqConfig.Type == "file" {
   366  		if len(defReqConfig.FileSystem.FileName) == 0 {
   367  			return aliases, []byte{}
   368  		}
   369  		defReqJSON, err := os.ReadFile(defReqConfig.FileSystem.FileName)
   370  		if err != nil {
   371  			glog.Fatalf("error reading aliases from file %s: %v", defReqConfig.FileSystem.FileName, err)
   372  			return aliases, []byte{}
   373  		}
   374  
   375  		if err := jsonutil.UnmarshalValid(defReqJSON, defReq); err != nil {
   376  			// we might not have aliases defined, but will atleast show that the JSON file is parsable.
   377  			glog.Fatalf("error parsing alias json in file %s: %v", defReqConfig.FileSystem.FileName, err)
   378  			return aliases, []byte{}
   379  		}
   380  
   381  		// Read in the alias map if we want to populate the info endpoints with aliases.
   382  		if defReqConfig.AliasInfo {
   383  			aliases = defReq.Ext.Prebid.Aliases
   384  		}
   385  		return aliases, defReqJSON
   386  	}
   387  	return aliases, []byte{}
   388  }
   389  
   390  func validateDefaultAliases(aliases map[string]string) error {
   391  	var errs []error
   392  
   393  	for alias := range aliases {
   394  		if openrtb_ext.IsBidderNameReserved(alias) {
   395  			errs = append(errs, fmt.Errorf("alias %s is a reserved bidder name and cannot be used", alias))
   396  		}
   397  	}
   398  
   399  	if len(errs) > 0 {
   400  		return errortypes.NewAggregateError("default request alias errors", errs)
   401  	}
   402  
   403  	return nil
   404  }