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