RTBKit  0.9
Open-source framework to create real-time ad bidding systems.
plugins/exchange/rubicon_exchange_connector.cc
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
 All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator