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 }