RTBKit
0.9
Open-source framework to create real-time ad bidding systems.
|
00001 /* rubicon_exchange_connector.cc 00002 Jeremy Barnes, 15 March 2013 00003 00004 Implementation of the Rubicon exchange connector. 00005 */ 00006 00007 #include "rubicon_exchange_connector.h" 00008 #include "rtbkit/plugins/bid_request/openrtb_bid_request.h" 00009 #include "rtbkit/plugins/exchange/http_auction_handler.h" 00010 #include "rtbkit/core/agent_configuration/agent_config.h" 00011 #include "openrtb/openrtb_parsing.h" 00012 #include "soa/types/json_printing.h" 00013 #include <boost/any.hpp> 00014 #include <boost/lexical_cast.hpp> 00015 #include "jml/utils/file_functions.h" 00016 00017 #include "crypto++/blowfish.h" 00018 #include "crypto++/modes.h" 00019 #include "crypto++/filters.h" 00020 00021 using namespace std; 00022 using namespace Datacratic; 00023 00024 namespace Datacratic { 00025 00026 template<typename T, int I, typename S> 00027 Json::Value jsonEncode(const ML::compact_vector<T, I, S> & vec) 00028 { 00029 Json::Value result(Json::arrayValue); 00030 for (unsigned i = 0; i < vec.size(); ++i) 00031 result[i] = jsonEncode(vec[i]); 00032 return result; 00033 } 00034 00035 template<typename T, int I, typename S> 00036 ML::compact_vector<T, I, S> 00037 jsonDecode(const Json::Value & val, ML::compact_vector<T, I, S> *) 00038 { 00039 ExcAssert(val.isArray()); 00040 ML::compact_vector<T, I, S> res; 00041 res.reserve(val.size()); 00042 for (unsigned i = 0; i < val.size(); ++i) 00043 res.push_back(jsonDecode(val[i], (T*)0)); 00044 return res; 00045 } 00046 00047 00048 } // namespace Datacratic 00049 00050 namespace OpenRTB { 00051 00052 template<typename T> 00053 Json::Value jsonEncode(const OpenRTB::List<T> & vec) 00054 { 00055 using Datacratic::jsonEncode; 00056 Json::Value result(Json::arrayValue); 00057 for (unsigned i = 0; i < vec.size(); ++i) 00058 result[i] = jsonEncode(vec[i]); 00059 return result; 00060 } 00061 00062 template<typename T> 00063 OpenRTB::List<T> 00064 jsonDecode(const Json::Value & val, OpenRTB::List<T> *) 00065 { 00066 using Datacratic::jsonDecode; 00067 ExcAssert(val.isArray()); 00068 OpenRTB::List<T> res; 00069 res.reserve(val.size()); 00070 for (unsigned i = 0; i < val.size(); ++i) 00071 res.push_back(jsonDecode(val[i], (T*)0)); 00072 return res; 00073 } 00074 00075 } // namespace OpenRTB 00076 00077 namespace RTBKIT { 00078 00079 BOOST_STATIC_ASSERT(hasFromJson<Datacratic::Id>::value == true); 00080 BOOST_STATIC_ASSERT(hasFromJson<int>::value == false); 00081 00082 00083 /*****************************************************************************/ 00084 /* RUBICON EXCHANGE CONNECTOR */ 00085 /*****************************************************************************/ 00086 00087 RubiconExchangeConnector:: 00088 RubiconExchangeConnector(const std::string & name, 00089 std::shared_ptr<ServiceProxies> proxies) 00090 : HttpExchangeConnector(name, proxies) 00091 { 00092 } 00093 00094 std::shared_ptr<BidRequest> 00095 RubiconExchangeConnector:: 00096 parseBidRequest(HttpAuctionHandler & connection, 00097 const HttpHeader & header, 00098 const std::string & payload) 00099 { 00100 std::shared_ptr<BidRequest> res; 00101 00102 // Check for JSON content-type 00103 if (header.contentType != "application/json") { 00104 connection.sendErrorResponse("non-JSON request"); 00105 return res; 00106 } 00107 00108 // Check for the x-openrtb-version header 00109 auto it = header.headers.find("x-openrtb-version"); 00110 if (it == header.headers.end()) { 00111 connection.sendErrorResponse("no OpenRTB version header supplied"); 00112 return res; 00113 } 00114 00115 // Check that it's version 2.1 00116 string openRtbVersion = it->second; 00117 if (openRtbVersion != "2.1") { 00118 connection.sendErrorResponse("expected OpenRTB version 2.1; got " + openRtbVersion); 00119 return res; 00120 } 00121 00122 cerr << "got request" << endl << header << endl << payload << endl; 00123 00124 // Parse the bid request 00125 ML::Parse_Context context("Bid Request", payload.c_str(), payload.size()); 00126 res.reset(OpenRtbBidRequestParser::parseBidRequest(context, exchangeName(), exchangeName())); 00127 00128 cerr << res->toJson() << endl; 00129 00130 return res; 00131 } 00132 00133 double 00134 RubiconExchangeConnector:: 00135 getTimeAvailableMs(HttpAuctionHandler & connection, 00136 const HttpHeader & header, 00137 const std::string & payload) 00138 { 00139 // Scan the payload quickly for the tmax parameter. 00140 static const string toFind = "\"tmax\":"; 00141 string::size_type pos = payload.find(toFind); 00142 if (pos == string::npos) 00143 return 10.0; 00144 00145 int tmax = atoi(payload.c_str() + pos + toFind.length()); 00146 00147 return tmax; 00148 } 00149 00150 HttpResponse 00151 RubiconExchangeConnector:: 00152 getResponse(const HttpAuctionHandler & connection, 00153 const HttpHeader & requestHeader, 00154 const Auction & auction) const 00155 { 00156 const Auction::Data * current = auction.getCurrentData(); 00157 00158 if (current->hasError()) 00159 return getErrorResponse(connection, auction, current->error + ": " + current->details); 00160 00161 OpenRTB::BidResponse response; 00162 response.id = auction.id; 00163 00164 std::map<Id, int> seatToBid; 00165 00166 string en = exchangeName(); 00167 00168 // Create a spot for each of the bid responses 00169 for (unsigned spotNum = 0; spotNum < current->responses.size(); 00170 ++spotNum) { 00171 if (!current->hasValidResponse(spotNum)) 00172 continue; 00173 00174 // Get the winning bid 00175 auto & resp = current->winningResponse(spotNum); 00176 00177 // Find how the agent is configured. We need to copy some of the 00178 // fields into the bid. 00179 const AgentConfig * config 00180 = std::static_pointer_cast<const AgentConfig> 00181 (resp.agentConfig).get(); 00182 00183 // Get the exchange specific data for this campaign 00184 auto cpinfo = config->getProviderData<CampaignInfo>(en); 00185 00186 // Put in the fixed parts from the creative 00187 int creativeIndex = resp.agentCreativeIndex; 00188 00189 auto & creative = config->creatives.at(creativeIndex); 00190 00191 // Get the exchange specific data for this creative 00192 auto crinfo = creative.getProviderData<CreativeInfo>(en); 00193 00194 // Find the index in the seats array 00195 int seatIndex = -1; 00196 { 00197 auto it = seatToBid.find(cpinfo->seat); 00198 if (it == seatToBid.end()) { 00199 seatIndex = seatToBid.size(); 00200 seatToBid[cpinfo->seat] = seatIndex; 00201 response.seatbid.emplace_back(); 00202 response.seatbid.back().seat = cpinfo->seat; 00203 } 00204 else seatIndex = it->second; 00205 } 00206 00207 // Get the seatBid object 00208 OpenRTB::SeatBid & seatBid = response.seatbid.at(seatIndex); 00209 00210 // Add a new bid to the array 00211 seatBid.bid.emplace_back(); 00212 auto & b = seatBid.bid.back(); 00213 00214 // Put in the variable parts 00215 b.cid = Id(resp.agent); 00216 b.id = Id(auction.id, auction.request->imp[0].id); 00217 b.impid = auction.request->imp[spotNum].id; 00218 b.price.val = USD_CPM(resp.price.maxPrice); 00219 b.adm = crinfo->adm; 00220 b.adomain = crinfo->adomain; 00221 b.crid = crinfo->crid; 00222 } 00223 00224 if (seatToBid.empty()) 00225 return HttpResponse(204, "none", ""); 00226 00227 static Datacratic::DefaultDescription<OpenRTB::BidResponse> desc; 00228 std::ostringstream stream; 00229 StreamJsonPrintingContext context(stream); 00230 desc.printJsonTyped(&response, context); 00231 00232 cerr << Json::parse(stream.str()); 00233 00234 return HttpResponse(200, "application/json", stream.str()); 00235 } 00236 00237 HttpResponse 00238 RubiconExchangeConnector:: 00239 getDroppedAuctionResponse(const HttpAuctionHandler & connection, 00240 const Auction & auction, 00241 const std::string & reason) const 00242 { 00243 return HttpResponse(204, "application/json", "{}"); 00244 } 00245 00246 HttpResponse 00247 RubiconExchangeConnector:: 00248 getErrorResponse(const HttpAuctionHandler & connection, 00249 const Auction & auction, 00250 const std::string & errorMessage) const 00251 { 00252 Json::Value response; 00253 response["error"] = errorMessage; 00254 return HttpResponse(400, response); 00255 } 00256 00257 ExchangeConnector::ExchangeCompatibility 00258 RubiconExchangeConnector:: 00259 getCampaignCompatibility(const AgentConfig & config, 00260 bool includeReasons) const 00261 { 00262 ExchangeCompatibility result; 00263 result.setCompatible(); 00264 00265 auto cpinfo = std::make_shared<CampaignInfo>(); 00266 00267 const Json::Value & pconf = config.providerConfig["rubicon"]; 00268 00269 try { 00270 cpinfo->seat = Id(pconf["seat"].asString()); 00271 if (!cpinfo->seat) 00272 result.setIncompatible("providerConfig.rubicon.seat is null", 00273 includeReasons); 00274 } catch (const std::exception & exc) { 00275 result.setIncompatible 00276 (string("providerConfig.rubicon.seat parsing error: ") 00277 + exc.what(), includeReasons); 00278 return result; 00279 } 00280 00281 result.info = cpinfo; 00282 00283 return result; 00284 } 00285 00286 namespace { 00287 00288 using Datacratic::jsonDecode; 00289 00291 template<typename T> 00292 void getAttr(ExchangeConnector::ExchangeCompatibility & result, 00293 const Json::Value & config, 00294 const char * fieldName, 00295 T & field, 00296 bool includeReasons) 00297 { 00298 try { 00299 if (!config.isMember(fieldName)) { 00300 result.setIncompatible 00301 ("creative[].providerConfig.rubicon." + string(fieldName) 00302 + " must be specified", includeReasons); 00303 return; 00304 } 00305 00306 const Json::Value & val = config[fieldName]; 00307 00308 jsonDecode(val, field); 00309 } 00310 catch (const std::exception & exc) { 00311 result.setIncompatible("creative[].providerConfig.rubicon." 00312 + string(fieldName) + ": error parsing field: " 00313 + exc.what(), includeReasons); 00314 return; 00315 } 00316 } 00317 00318 } // file scope 00319 00320 ExchangeConnector::ExchangeCompatibility 00321 RubiconExchangeConnector:: 00322 getCreativeCompatibility(const Creative & creative, 00323 bool includeReasons) const 00324 { 00325 ExchangeCompatibility result; 00326 result.setCompatible(); 00327 00328 auto crinfo = std::make_shared<CreativeInfo>(); 00329 00330 const Json::Value & pconf = creative.providerConfig["rubicon"]; 00331 00332 // 1. Must have rubicon.attr containing creative attributes. These 00333 // turn into RubiconCreativeAttribute filters. 00334 getAttr(result, pconf, "attr", crinfo->attr, includeReasons); 00335 00336 // TODO: create filter from these... 00337 00338 // 2. Must have rubicon.adm that includes Rubicon's macro 00339 getAttr(result, pconf, "adm", crinfo->adm, includeReasons); 00340 if (crinfo->adm.find("${AUCTION_PRICE:BF}") == string::npos) 00341 result.setIncompatible 00342 ("creative[].providerConfig.rubicon.adm ad markup must contain " 00343 "encrypted win price macro ${AUCTION_PRICE:BF}", 00344 includeReasons); 00345 00346 // 3. Must have creative ID in rubicon.crid 00347 getAttr(result, pconf, "crid", crinfo->crid, includeReasons); 00348 if (!crinfo->crid) 00349 result.setIncompatible 00350 ("creative[].providerConfig.rubicon.crid is null", 00351 includeReasons); 00352 00353 // 4. Must have advertiser names array in rubicon.adomain 00354 getAttr(result, pconf, "adomain", crinfo->adomain, includeReasons); 00355 if (crinfo->adomain.empty()) 00356 result.setIncompatible 00357 ("creative[].providerConfig.rubicon.adomain is empty", 00358 includeReasons); 00359 00360 // Cache the information 00361 result.info = crinfo; 00362 00363 return result; 00364 } 00365 00366 float 00367 RubiconExchangeConnector:: 00368 decodeWinPrice(const std::string & sharedSecret, 00369 const std::string & winPriceStr) 00370 { 00371 ExcAssertEqual(winPriceStr.length(), 16); 00372 00373 auto tox = [] (char c) 00374 { 00375 if (c >= '0' && c <= '9') 00376 return c - '0'; 00377 else if (c >= 'A' && c <= 'F') 00378 return 10 + c - 'A'; 00379 else if (c >= 'a' && c <= 'f') 00380 return 10 + c - 'a'; 00381 throw ML::Exception("invalid hex digit"); 00382 }; 00383 00384 unsigned char input[8]; 00385 for (unsigned i = 0; i < 8; ++i) 00386 input[i] 00387 = tox(winPriceStr[i * 2]) * 16 00388 + tox(winPriceStr[i * 2 + 1]); 00389 00390 CryptoPP::ECB_Mode<CryptoPP::Blowfish>::Decryption d; 00391 d.SetKey((byte *)sharedSecret.c_str(), sharedSecret.size()); 00392 CryptoPP::StreamTransformationFilter 00393 filt(d, nullptr, 00394 CryptoPP::StreamTransformationFilter::NO_PADDING); 00395 filt.Put(input, 8); 00396 filt.MessageEnd(); 00397 char recovered[9]; 00398 size_t nrecovered = filt.Get((byte *)recovered, 8); 00399 00400 ExcAssertEqual(nrecovered, 8); 00401 recovered[nrecovered] = 0; 00402 00403 float res = boost::lexical_cast<float>(recovered); 00404 00405 return res; 00406 } 00407 00408 } // namespace RTBKIT