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 }