UBI LINK 广告平台DSP接入文档
UBI LINK 广告平台DSP接入文档
修订历史
版本 | 修订内容 | 修订日期 | 修订人 |
---|---|---|---|
1.0 | 发布 | 2025-06-17 | 房振春 |
1、背景及目的
UBI LINK 平台,依托自有SDK流量,旨在为各广告需求方(DSP)提供稳定而优质的交易服务。
为了协助各DSP顺利对接UBI LINK,本文档定义统一对接规范,作为各DSP平台与UBI LINK进行交互的标准规范。
2、DSP系统对接流程
1、DSP使用本文档进行对接,UBI LINK运营同学提供测试广告位以及价钱加密key
2、接口开发完成后,DSP提供联调地址,与UBI LINK进行联调
3、线上小量对数
3、竞价协议
1、UBI LINK 与DSP 之间的通信协议为HTTP协议,竞价请求使用HTTP POST方式发送,DSP服务需要开启长连接减少连接处理时间
2、竞价请求与响应采用Protobuf(Protocol Buffers)数据格式,HTTP标头需要设置:Content-Type:application/octet-stream,proto文件地址:https://gameley.coding.net/s/7110ded5-1f3f-4607-b5d0-7d9fbbb7b530
3、支持标准的gzip压缩机制,以减少传输数据的大小,若需要压缩,需要UBI LINK运营同学进行配置。广告请求数据gzip压缩会设置Http Header:”Content-Encoding: gzip”,响应数据需要gzip压缩并设置Http Header:”Content-Encoding: gzip
3、广告请求发出后,等待响应时间默认500ms
4、若DSP不参与竞价,需要设置HTTP状态码为204
4、竞价请求
BidRequest
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
id | string | 是 | 请求唯一 id | UUID.randomUUID().toString().replace("_", "") |
imp | object | 是 | 此次曝光的信息,目前仅支持一个 | |
app | object | 是 | 媒体的 app 信息 | |
device | object | 是 | 用户设备信息 |
Imp
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
native | object | 否 | banner、native 广告位用 | |
video | object | 否 | 视频广告位用,与 banner、native 互斥 | |
bidFloor | object | 否 | 底价,单位分 | |
ext | object | 否 | 扩展字段 |
imp.ext
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
platform | int | 是 | 系统平台,1=IOS,2=Android,3=PC_Web,4=H5,5=PC_Client,6=OTT | |
stagid | string | 是 | UBI LINK 平台的 DSP 广告位 id | |
tag | string | 否 | 用户标签码值,逗号分割 | 参见用户标签码值 |
pddTag | int | 否 | 是否安装拼多多,1=已安装拼多多,2=未安装,3=未知 |
imp.native
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
w | int | 否 | 广告位素材宽度 | |
h | int | 否 | 广告位素材高度 |
imp.video
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
minduration | int | 否 | 视频的最小时长,单位为秒 | 0 |
maxduration | int | 否 | 视频的最大时长,单位为秒 | 最大值不超过60 |
app
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
id | int | 是 | UBI LINK平台的预算源 appId | |
name | string | 否 | app名称 | |
ver | string | 否 | 应用版本号 | |
bundle | string | 否 | App包名 | |
storeurl | string | 否 | App Store 下载链接 | |
publisher | string | 否 | 媒体信息 | |
cat | string | 否 | APP行业分类 | |
appstoreversion | string | 否 | 手机应用商店版本号 | 大陆厂商安卓设备AS(应用市场) |
hmsversion | string | 否 | 华为HMS版本 | 华为机型上的HMS core版本号 |
Publisher
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
name | string | 否 | 媒体名称 |
Device
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
ua | string | 否 | User-Agent | |
geo | object | 否 | 设备经纬度信息 | |
ip | string | 否 | 设备 ipv4 地址 | |
devicetype | int | 否 | 设备类型4=phone,5=pad, 2=pc,3=OTT | |
os | string | 否 | 设备操作系统,例如:android、ios、windows | |
osv | string | 否 | 设备操作系统版本,例如10.2 | |
dpi | string | 否 | 屏幕像素密度 | |
ppi | string | 否 | 每英寸所拥有的像素数量 | |
density | string | 否 | 屏幕密度 | |
orientation | string | 否 | 设备⽅向 0未知, 1纵向, 2横向 | 0 |
h | int | 否 | 设备分辨率,高,单位像素 | |
w | int | 否 | 设备分辨率,款,单位像素 | |
carrier | string | 否 | 运营商 | 参见:运营商字典表 |
connectiontype | int | 否 | 联网类型 | 参见:联网类型字典表 |
ifa | string | 否 | idfa原值 | |
ifaMd5 | string | 否 | idfa md5加密 | |
idfv | string | 否 | idfv原值 | |
idfvMd5 | string | 否 | idfv md5加密 | |
did | string | 否 | imei原值 | |
didmd5 | string | 否 | imei md5加密 | |
oaidMd5 | string | 否 | oaid md5加密 | |
dpid | string | 否 | android id | |
dpidmd5 | string | 否 | android id,md5加密 | |
mac | string | 否 | mac地址,去冒号 | |
macmd5 | string | 否 | mac地址,md5加密 | |
make | string | 否 | 设备生产商,如 Apple | |
model | string | 否 | 设备型号 如 iphone | |
ext | object | 是 | 扩展字段 | |
bootmark | string | 否 | 系统启动标识,取原值回传。 iOS:1623815045.970028;Android:ec7f4f33-411a-47bc-8067-744a4e7e0723 | |
updatemark | string | 否 | 系统更新标识,取原值回传。 iOS:1581141691.570419583;Android:1004697.70999999 | |
boottime | string | 否 | 设备最近一次开机时间,iOS必传,秒级时间戳,小数点后保留6位,如:1595214620.383940 | |
osupdatetime | string | 否 | 手机系统的更新时间(时间戳) | |
deviceName | string | 否 | 设备名称 | |
deviceNameMd5 | string | 否 | 设备名称Md5 | |
memory | string | 否 | 物理内存大小;KB | |
hardDisk | string | 否 | 物理硬盘大小:KB | |
timezone | string | 否 | 系统当前时区 | |
caid | string | 否 | 广协生成的caid | |
caidver | string | 否 | 广协生成的caid版本号 | |
aaid | string | 否 | 阿里生成的aaid | |
startuptime | string | 否 | 开机时长 | |
birthtime | string | 否 | 设备初始化时间 | |
paid | string | 否 | 拼多多广告识别id |
Geo
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
lat | double | 否 | 地理位置纬度,例:31.232949 | |
lon | double | 否 | 地理位置经度,例:121.418011 |
device.ext
字段名 | 类型 | 是否必传 | 说明 | 默认值 |
---|---|---|---|---|
imei | string | 否 | imei 原值 | |
android | string | 否 | androidid 原值 | |
mac | string | 否 | 无线 mac,去冒号原值 | |
oaid | string | 否 | oaid 原值 | |
brand | string | 否 | 设备品牌 | |
mac3 | string | 否 | 有线 mac 地址,去冒号原值,ott 专用 | |
mac4 | string | 否 | 蓝牙 mac 地址,去冒号原值,ott 专用 | |
ipv6 | string | 否 | ipv6 地址 | |
ver | string | 否 | app应用版本号 | |
gid | string | 否 | app分发渠道号 |
请求示例:
{
"id":"d3bb147293534f639b8044ad9b37c9cc",
"imp":{
"ext":{
"tag":"1,2,3",
"platform":2,
"stagid":"10000079"
}
},
"app":{
"bundle":"",
"cat":"0",
"id":500039,
"name":"API 托管模式_Android",
"publisher":{
"name":"API 托管模式"
},
"storeurl":""
},
"device":{
"carrier":"46003",
"connectiontype":2,
"devicetype":4,
"h":3,
"w":4,
"didmd5":"d41d8cd98f00b204e9800998ecf8427e",
"dpidmd5":"d087848d7034fac159c6390a2fec0c91",
"ext":{
"androidid":"191e3789a779a501",
"brand":"HONOR",
"gid":"dev",
"imei":"",
"ipv6":"",
"mac":"342EB6F1B2D8",
"mac3":"",
"mac4":"",
"oaid":"",
"ver":"1.0.0"
},
"geo":{
"lat":0,
"lon":0
},
"ifa":"",
"ip":"127.0.0.1",
"macmd5":"5bfc558756f0bb3664cba82b31101980",
"make":"HUAWEI",
"model":"HRY-AL00a",
"os":"Android",
"osv":"10",
"ua":""
}
}
5、竞价响应
注:ios 广告响应必须为https协议,android 建议使用https
BidResponse
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
flag | boolean | 是 | 请求是否成功,true 为成功。 |
code | int | 是 | code 0 为编码 |
message | string | 否 | 错误信息描述 |
data | array object | 否 | 广告数据 |
Data
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
crequestid | string | 是 | 媒体生成的本次请求唯一 id |
sposid | string | 是 | UBI LINK平台的DSP广告位id |
restype | string | 否 | 填充广告类型 5=网盟,其他为"" |
material | object | 否 | 素材信息 |
sdkinfo | object array | 否 | sdk信息 |
monitor | object | 否 | 素材监测信息 |
extented | object | 否 | 扩展字段 |
orderid | string | 否 | 订单号 |
backupads | object array | 否 | 备播广告数组 |
adsequence | int | 否 | 贴片广告贴次 |
price | string | 否 | 出价,单位分 RTB必填 |
spostype | int | 否 | 广告类型: 1=开屏,2=视频贴片,3=信息流, 4=banner,5=插屏,6=激励视频 |
Material
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
crid | string | 否 | 素材id |
ldptype | int | 否 | 落地页类型 0=h5;1=下载APP, 2=deeplink,3=广点通下载类 默认值为0 |
ldp | string | 否 | 落地页url |
deeplink | string | 否 | deeplink url |
universallink | string | 否 | iOS的universallink url |
tempid | string | 否 | 广告样式模板id |
videourl | string | 否 | 视频地址(视频信息流、贴片、激励视频、开屏) |
duration | int | 否 | 视频播放时长 开屏广告不填默认为5 |
imgurl | string array | 否 | 信息流图片地址或视频信息流封面图片地址 |
title | string | 否 | 广告标题 |
desc | string | 否 | 广告描述 |
adm | string | 否 | 广告素材 url,开机、banner、插屏的图片 url |
w | string | 否 | 主素材宽 |
h | string | 否 | 主素材高 |
iconurl | string | 否 | 图标url |
apkname | string | 否 | 下载的app应用名称 |
packagename | string | 否 | 下载的安装包名称(android)或唯— id 号(ios) |
privacyUrl | string | 否 | 隐私地址 |
permissionUrl | string | 否 | 用户权限url |
appPublisher | string | 否 | 开发者名称 |
versionName | string | 否 | 版本号名称 |
apkSize | string | 否 | 应用宝大小 |
appIntro | string | 否 | 应用介绍 |
Monitor
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
pa | array object | 否 | 曝光监测 |
ca | array object | 否 | 点击监测 |
nurl | array object | 否 | 竞胜通知监测 |
ldpca | array string | 否 | 落地页加载完成监测 |
advml | object | 否 | 广告主监测链接 |
hcurl | array object | 否 | 汇川-点击反作弊监测链接 |
lurl | array object | 否 | 竞败通知监测 |
Advml
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
ledp | array object | 否 | Deeplink字段不为空此字段必传,Deeplink唤醒成功监测信息 |
ledpfailtrs | array object | 否 | Deeplink字段不为空此字段必传,Deeplink唤醒失败监测信息 |
ledpreadytrs | array object | 否 | Deeplink字段不为空此字段必传,Deeplink唤醒开始监测信息 |
ledownstarttrs | array object | 否 | 下载开始监测地址 |
ledowncomptrs | array object | 否 | 下载完成监测地址 |
leinstallstarttrs | array object | 否 | 安装开始监测信息 |
leinstallcomptrs | array object | 否 | 安装完成监测地址 |
leinstallerrortrs | array object | 否 | 安装失败监测地址 |
leinstallactiontrs | array object | 否 | 安装激活监测地址 |
leready | array object | 否 | 视频素材加载成功监测信息 |
levideoloaderror | array object | 否 | 视频素材加载错误监测信息 |
levideostart | array object | 否 | 视频开始播放监测信息 |
lefirstquartile | array object | 否 | 视频播放1/4监测信息 |
lemidpoint | array object | 否 | 视频播放1/2监测信息 |
lethirdquartile | array object | 否 | 视频播放3/4监测信息 |
levideoend | array object | 否 | 视频播放完成监测信息 |
lemute | array object | 否 | 静音播放监测信息 |
leunmute | array object | 否 | 关闭静音监测信息 |
leskip | array object | 否 | 跳过监测信息 |
lereplay | array object | 否 | 重播监测信息 |
lepause | array object | 否 | 视频暂停监测信息 |
Ledp
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
url | string | 否 | Deeplink字段不为空此字段必传,Deeplink唤醒成功监测信息 |
ledpfailtrs
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
url | string | 否 | Deeplink字段不为空此字段必传,Deeplink唤醒失败监测信息 |
ledpreadytrs
字段名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
url | string | 否 | Deeplink字段不为空此字段必传,Deeplink唤醒开始监测信息 |
ledownstarttrs
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 下载开始监测地址 |
ledowncomptrs
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 下载完成监测链接 |
leinstallstarttrs
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 安装开始监测链接 |
leinstallcomptrs
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 安装结束监测地址 |
leinstallerrortrs
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 安装失败监测链接 |
leready
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频素材加载成功监测地址 |
levideoloaderror
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频素材加载失败监测地址 |
levideostart
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频开始播放监测地址 |
lefirstquartile
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频播放1/4监测地址 |
lemidpoint
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频播放1/2监测地址 |
lethirdquartile
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频播放3/4监测地址 |
levideoend
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频播放完成监测地址 |
lemute
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 静音播放监测地址 |
leunmute
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 取消静音播放监测地址 |
leskip
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 跳过监测地址 |
lereplay
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 重新播放监测地址 |
lepause
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 视频暂停监测地址 |
Pa
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
sdkid | string | 否 | sdkid 传""即可 |
url | string | 否 | 曝光监测url |
time | int | 否 | 曝光监测上报时间 |
ca
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
sdkid | string | 否 | sdkid 传""即可 |
url | string | 否 | 点击监测url |
nurl
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 竞胜通知url |
lurl
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
url | string | 否 | 竞败通知url |
响应示例:
{
"code": 2000,
"flag": true,
"message": "请求成功",
"data": [{
"requestid": "d3bb147293534f639b8044ad9b37c9cc",
"restype": "4",
"sdkinfo": [],
"serialflag": 0,
"sposid": "10000079",
"spostype": 1,
"adsequence": 1,
"crequestid": "d3bb147293534f639b8044ad9b37c9cc",
"extended": {},
"loadtype": 0,
"material": {
"adm": "http://xxx.com/imgs/materials/2021/2/19/20210219180216_750m420.jpg",
"apkname": "apkname",
"deeplink": "http://www.deeplink.com",
"desc": "878257",
"duration": 5,
"imgurl": ["imagurl"],
"ldp": "http://www.baidu.com",
"ldptype": 0,
"packagename": "package",
"tempid": "8",
"videourl": "http://videourl0"
},
"monitor": {
"advml": {
"ledp": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"ledpfailtrs": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"ledownstarttrs": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"ledowncomptrs": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"leinstallstarttrs": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"leinstallcomptrs": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"leinstallerrortrs": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"leready": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"levideoloaderror": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.comi/event"
}],
"levideostart": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"lefirstquartile": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"lemidpoint": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"lethirdquartile": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"levideoend": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"lemute": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"leunmute": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"leskip": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}],
"lereplay": [{
"url": "http://xxx.com/stat/ikanstat/monitor"
}, {
"url": "https://xxx.com/event"
}]
},
"ca": [{
"url": "http://xxx.com/dj"
}, {
"url": "http://xxx.com/stat/ikanstat/click"
}],
"ldpca": ["http://xxx.com/stat/ikanstat/click?"],
"pa": [{
"time": 0,
"url": "http://xxx.com/bg"
}, {
"time": 1,
"url": "https://xxx.com/x/"
}, {
"time": 1,
"url": "http://xxx.com"
}, {
"time": 1,
"url": "http://xxx.com/stat/ikanstat/stat"
}]
}
}]
}
6、宏替换处理
DSP平台下发广告响应时,需要将广告主相关宏替换替换为UBI LINK平台宏替换规范。
宏定义 | 说明 |
---|---|
__LEMON__TIMESTAMP__ | 当前时间戳, 单位: 毫秒 |
__LEMON__PROGRESS__ | 当前播放进度, 单位: 秒 |
__LEMON__C_DOWN_X__ | 用户点击时手指按下时相对于屏幕的横坐标 |
__LEMON__C_DOWN_Y__ | 用户点击时手指按下时相对于屏幕的纵坐标 |
__LEMON__C_UP_X__ | 用户点击时手指离开手机屏幕时的横坐标 |
__LEMON__C_UP_Y__ | 用户点击时手指离开手机屏幕时的纵坐标 |
__LEMON__C_UP_TIME__ | 用户点击时手指离开手机屏幕时的时间戳, 单位: 毫秒 |
__LEMON__C_DOWN_OFFSET_X__ | 用户点击时手指按下时相对于广告展示区域的横坐标 |
__LEMON__C_DOWN_OFFSET_Y__ | 用户点击时手指按下时相对于广告展示区域的纵坐标 |
__LEMON__C_UP_OFFSET_X__ | 用户点击时手指离开时相对于广告展示区域的横坐标 |
__LEMON__C_UP_OFFSET_Y__ | 用户点击时手指离开时相对于广告展示区域的纵坐标 |
__LEMON__WIDTH__ | 实际广告展示区域(广告位)宽 |
__LEMON__HEIGHT__ | 实际广告展示区域(广告位)高 |
__LEMON__CLICK_ID__ | 点击ID,用于二次访问广告业务时要做替换,例如:进入落地页,在做下载操作时需要替换Clickid,用于后续的转化上报 |
__LEMON__CLICKAREA__ | 点击区域: 0-广告素材, 1-按钮 |
__LEMON__EVENT_TIME_START__ | 开始事件触发的时间戳,单位毫秒 |
__LEMON__EVENT_TIME_SECOND__ | 开始事件触发的时间戳,单位秒 |
__LEMON__EVENT_TIME_END__ | 结束事件触发的时间戳,单位毫秒 |
__LEMON__VIDEO_DURATION__ | 仅在视频广告应答出现, 点击视频时的播放时长,单位秒 |
__LEMON__DOWN_X_ABS__ | 鼠标或手指触碰或点击屏幕时,触点相对于屏幕左上角的绝对 X 坐标 |
__LEMON__DOWN_Y_ABS__ | 鼠标或手指触碰或点击屏幕时,触点相对于屏幕左上角的绝对 Y 坐标 |
__LEMON__UP_X_ABS__ | 完成点击动作后,鼠标或手指离开手机屏幕时,触点相对于屏幕左上角的绝对 X 坐标 |
__LEMON__UP_Y_ABS__ | 完成点击动作后,鼠标或手指离开手机屏幕时,触点相对于屏幕左上角的绝对 Y 坐标 |
__LEMON__GPS_LON__ | 经度。 |
__LEMON__GPS_LAT__ | 维度 |
__LEMON__CLICK_ID__ | 类似广点通下载类广告必须替换的宏,参见需要二次访问的广告处理业务说明 |
__LEMON__TSS__ | 发起或者执行上报事件动作时,手机客户端的当前时间戳,精确到秒(s) |
__LEMON__VD__ | 当视频上报事件发生时,此时视频已经播放完成的时间长度 |
__LEMON__VIDEO_TIME__ | 视频总时长,单位:秒 |
__LEMON__IP__ | 客户端IP |
__LEMON__IDFA__ | 设备idfa |
__LEMON__IMEI__ | 设备imei |
__LEMON__IMEIMD5__ | 设备imei的MD5值 |
__LEMON__IDFAMD5__ | 设备idfa的MD5值 |
__LEMON__UA__ | 数据上报终端设备User Agent |
__LEMON__OAID__ | 设备的oaid |
__LEMON__MAC__ | 用户终端的网卡接口的物理MAC地址(无冒号) |
__LEMON__ANDROIDID__ | 安卓客户端AndroidID |
__LEMON__ANDROIDIDMD5__ | 安卓客户端AndroidID md5 |
__LEMON__ALL_AAID__ | 阿里阿里巴巴匿名设备标识,需集成阿里 SDK |
__LEMON__CAID__ | 中广协CAID |
__LEMON__PRICE__ | 竞得价,价格宏(需要进行加解密) |
__LEMON__BUTTON_LUX__ | 广告按钮区域坐标(坐标定义:以屏幕左上角坐标为原点)左上角横坐标 |
__LEMON__BUTTON_LUY__ | 广告按钮区域坐标(坐标定义:以屏幕左上角坐标为原点)左上角纵坐标 |
__LEMON__BUTTON_RDX__ | 广告按钮区域坐标(坐标定义:以屏幕左上角坐标为原点)右下角横坐标 |
__LEMON__BUTTON_RDY__ | 广告按钮区域坐标(坐标定义:以屏幕左上角坐标为原点)右下角纵坐标 |
__LEMON__DISPLAY_LUX__ | 广告展示区域坐标(坐标定义:以屏幕左上角坐标为原点)左上角横坐标 |
__LEMON__DISPLAY_LUY__ | 广告展示区域坐标(坐标定义:以屏幕左上角坐标为原点)左上角纵坐标 |
__LEMON__DISPLAY_RDX__ | 广告展示区域坐标(坐标定义:以屏幕左上角坐标为原点)右下角横坐标 |
__LEMON__DISPLAY_RDY__ | 广告展示区域坐标(坐标定义:以屏幕左上角坐标为原点)右下角纵坐标 |
__LEMON__DP_WIDTH__ | 广告位的宽度,Android 端单位为逻辑像素(dp) |
__LEMON__DP_HEIGHT__ | 广告位的高度,Android 端单位为逻辑像素(dp) |
__LEMON__DP_DOWN_X__ | 手指按下手机屏幕时的x 坐标(相对于素材左上顶点), Android 端单位为逻辑像素(dp) |
__LEMON__DP_DOWN_Y__ | 手指按下手机屏幕时的y 坐标(相对于素材左上顶点), Android 端单位为逻辑像素(dp) |
__LEMON__DP_UP_X__ | 手指离开手机屏幕时的x 坐标(相对于素材左上顶点),Android 端单位为逻辑像素(dp) |
__LEMON__DP_UP_Y__ | 手指离开手机屏幕时的y 坐标(相对于素材左上顶点),Android 端单位为逻辑像素(dp) |
__LEMON__SLD__ | 广告交互方式,0:常规触屏点击,1 :滑动点击,2:摇一摇,3:自定义手势,5:扭一扭,6:擦除。其中常规触屏点击、滑动点击、自定义手势、擦除需上报点击坐标;摇一摇、扭一扭可以不上报点击坐标,需将点击坐标宏替换为-999。 |
__LEMON__LOSE_REASON__ | 竞败原因,枚举值。0:unknown(其他原因)1:非法参数(缺少参数或参数异常)2:禁止投放(黑名单)3:创意审核中4:价格低于其他参竞方 |
需要二次访问的广告处理业务说明
当ldptype =3时,说明本广告响应的落地页 (ldp)是一个广点通app下载,需要对ldp进行二次请求,以获取实际应用下载地址和clickid进行宏替换。
请求响应结构如下:
{
"ret": 0, // 返回码:0 成功; 1 失败
"data": { // APP下载和转化上报信息
"clickid": "xxxxxxxxx", // clickid 需要缓存下来,用于后续转化上报
"dstlink": "http://xxx/xxx.apk" // 当前广告对应的下载链接
}
}
参数说明:
clickid:用于后续的转化上报
dstlink:用于跳转到下载地址
需要上报开始下载到安装完成的监测。在上报前,
先要对上报 URL进行宏替换,需要替换的宏如下(如果存在的话):
参数 | 类型 | 说明 |
---|---|---|
__LEMON__CLICK_ID__ | string | click_id |
7、字典表
状态码
错误码 | 信息 | 说明 |
---|---|---|
50005 | 广告位 ID 为空 | |
50010 | 无效的广告位 | 参数有误(包含如下两种场景) 1、当广告位、应用、媒体任一开关为关闭; 2、广告位 id 不存在; |
50040 | 请求 ID 为空 | |
50045 | app 和 site 参数有误 | 媒体应用类型为 ADX&SSP 时,api 方式对接时,会校验 app 和 site 有且 仅有一个非空 |
50050 | 平台参数错误 | 系统平台:sdk 传参,与 SSP 查询结果做校验,若不匹配 (1=iOS、2=Android、3=PC_Client、4=H5,5=PC_Web、6=OTT) 返回错误码:平台参数有误 |
运营商
carrier值 | 运营商 |
---|---|
46000 | 中国移动 |
46001 | 中国联通 |
46003 | 中国电信 |
46020 | 中国铁通 |
0 | 其他或未知 |
connectiontype | 联网类型 |
---|---|
0 | 未知 |
1 | 有线网络 |
2 | wifi |
3 | 蜂窝网-未知 |
4 | 蜂窝网-2G |
5 | 蜂窝网-3G |
6 | 蜂窝网-4G |
7 | 蜂窝网-5G |
用户码值
码值 | 名称 | 包名 |
---|---|---|
1 | 淘宝-太好逛了吧 | com.taobao.taobao |
2 | 支付宝-便捷生活一点就好 | com.eg.android.AlipayGphone |
3 | 拼多多 | com.xunmeng.pinduoduo |
4 | 美团-美好生活小帮手 | com.sankuai.meituan |
5 | 抖音 | com.ss.android.ugc.aweme |
6 | 快手 | com.smile.gifmaker |
7 | 百度 | com.baidu.searchbox |
8 | 抖音极速版 | com.ss.android.ugc.aweme.lite |
9 | 快手极速版 | com.kuaishou.nebula |
10 | 京东-京东1111真便宜 | com.jingdong.app.mall |
11 | 得物 | com.shizhuang.duapp |
12 | 番茄免费小说 | com.dragon.read |
13 | 虎牙直播-热门游戏赛事直播 | com.duowan.kiwi |
14 | 腾讯视频-故乡别来无恙全网独播 | com.tencent.qqlive |
15 | 哔哩哔哩 | tv.danmaku.bili |
16 | 爱奇艺-无所畏惧全网独播 | com.qiyi.video |
17 | YY-和附近的人聊天看直播 | com.duowan.mobile |
18 | 百度极速版 | com.baidu.searchbox.lite |
19 | 搜狐视频-2023舞蹈翻跳盛典 | com.sohu.sohuvideo |
20 | 百度大字版-字大不伤眼 | com.baidu.searchbox.tomas |
21 | 懂车帝 | com.ss.android.auto |
22 | 西瓜视频-疯狂元素城首播 | com.ss.android.article.video |
23 | 番茄畅听 | com.xs.fm |
24 | 噗叽 | com.kwai.thanos |
25 | 头条搜索极速版-原今日头条极速版 | com.ss.android.article.lite |
26 | 每日走路-计步赚钱多多 | com.yt.mrzl |
27 | UC浏览器-好搜好看好好用 | com.UCMobile |
28 | 抖音火山版 | com.ss.android.ugc.live |
29 | 小红书-你的生活指南 | com.xingin.xhs |
30 | 有柿 | com.ss.android.article.search |
31 | 贝乐虎儿歌 | com.ubestkid.beilehu.android |
32 | 七猫免费小说 | com.kmxs.reader |
33 | 追书神器免费版 | com.ushaqi.zhuishushenqi.adfree |
34 | 58同城-招聘找工作租房家政买车 | com.wuba |
35 | 快看漫画-偷偷藏不住全网独家 | com.kuaikan.comic |
36 | 优酷视频-新闻女王 全网独播 | com.youku.phone |
37 | 知乎-有问题就会有答案 | com.zhihu.android |
38 | 饿了么-新人专享20元红包 | me.ele |
39 | 喜马拉雅-123狂欢节 | com.ximalaya.ting.android |
40 | 安居客-二手房新房租房 | com.anjuke.android.app |
41 | Hello语音-游戏开黑语音交友 | com.yy.huanju |
42 | 儿歌点点-宝宝儿歌故事动画大全 | com.mampod.ergedd |
43 | 花椒直播-美女视频聊天交友 | com.huajiao |
44 | TT语音-王者荣耀官方合作伙伴 | com.yiyou.ga |
45 | 贝壳找房-二手房新房租房装修 | com.lianjia.beike |
46 | 爱奇艺极速版-宁安如梦热播 | com.qiyi.video.lite |
47 | 搜狐新闻-24小时新闻热点实时更新 | com.sohu.newsclient |
48 | 幸福里-二手房新房大平台 | com.f100.android |
49 | 墨迹天气-实时预报 | com.moji.mjweather |
50 | 万能WiFi精灵-免费链接上网 | com.jiujing.wnwfjl |
51 | 满格WiFi | com.zs.wf.full.lattice |
52 | 活宝工具箱 | android.yhctstl.hbgjx |
53 | 每刻充电-快速充电赚钱 | com.hzjl.mkcd |
54 | 全免小说-免费阅读 | com.martian.qmbook |
55 | WiFi钥匙速联-免费上网连接大师 | com.hzsq.wifiyssl |
56 | 雷电清理大师-手机垃圾内存清理 | com.ntyy.clear.thunder |
57 | 今日头条 | com.ss.android.article.news |
58 | 神速清理-一键清理专家 | com.speedandroid.server.ctsion |
59 | 腾讯新闻 | com.tencent.news |
60 | 当准天气 | com.bee.rain |
61 | WiFi钥匙天天连-免费上网 | com.youying.wfysttl |
62 | 手机天猫 | com.tmall.wireless |
63 | 闲鱼-闲置交易平台 | com.taobao.idlefish |
64 | 智能5G钥匙 | com.terminillo.hasty.clamp |
65 | 趣连WiFi-手机万能钥匙测速 | com.wx.wifi.qulian |
66 | 每日清理大师-清理助手 | com.ntyy.clear.everyday |
67 | 加速WiFi-手机上网安全管家 | com.ntyy.wifi.accelerate |
68 | 同程旅行 | com.tongcheng.android |
69 | 新氧医美-正品轻医美十亿补贴 | com.youxiang.soyoungapp |
70 | 脉脉 | com.taou.maimai |
71 | 追书神器 | com.ushaqi.zhuishushenqi |
72 | 大众点评-发现好去处 | com.dianping.v1 |
73 | 芒果TV-以爱为营 | com.hunantv.imgo.activity |
74 | 最右-搞笑脑洞兴趣社区 | cn.xiaochuankeji.tieba |
75 | 伊对 | me.yidui |
76 | 新浪新闻 | com.sina.news |
77 | 唯品会-新人享豪礼 | com.achievo.vipshop |
78 | 微博-上微博看明星热点新闻资讯 | com.sina.weibo |
79 | 腾讯NOW直播-直播交友短视频平台 | com.tencent.now |
80 | 淘特 | com.taobao.litetao |
81 | 全民K歌-音乐视频平台 | com.tencent.karaoke |
82 | 链家-专业房产买卖租赁装修平台 | com.homelink.android |
83 | 酷狗音乐 | com.kugou.android |
84 | 高德地图-高德打车 | com.autonavi.minimap |
85 | 斗鱼-高清游戏娱乐直播 | air.tv.douyu.android |
86 | 阿里巴巴-1688批发采购进货市场 | com.alibaba.wireless |
87 | 多多走路赚-天天计步领红包 | com.shangyun.jbxns |
88 | 每日充电-边充边赚 | com.xy.mrcd |
89 | 电池修复大师-省电管家 | com.brs.battery.repair |
90 | 充电领宝-天天充电赚钱 | com.shk.cdlb |
91 | 美团外卖 | com.sankuai.meituan.takeoutnew |
92 | (iOS)淘宝 | com.taobao.taobao4iphone |
93 | (iOS)京东 | com.360buy.jdmobile |
94 | (iOS)拼多多 | com.xunmeng.pinduoduo |
95 | (iOS)美团 | com.meituan.imeituan |
96 | (iOS)美团外卖 | com.meituan.itakeaway |
97 | (iOS)支付宝 | com.alipay.iphoneclient |
98 | (iOS)抖音 | com.ss.iphone.ugc.Aweme |
99 | 汽车之家 | com.cubic.autohome |
8、价钱加解密
竞价成功上报时需要进⾏价格宏替换,__LEMON__PRICE__即扣费价格宏替换,需要采⽤Google OpenRTB价格加解密⽅式经过base64encode 后,作为加密后成交价格串。加密所需的 e_key 和 i_key 通过 UBI LINK 商务获取。
import javax.crypto.spec.SecretKeySpec;
public class AdxEnDecrypter {
//加密
private static String enPrice(double price, String encryptionKey, String integrityKey) throws InvalidKeyException {
DoubleClickCrypto.Keys keys = new DoubleClickCrypto.Keys(
new SecretKeySpec(encryptionKey.getBytes(), "HmacSHA1"),
new SecretKeySpec(integrityKey.getBytes(), "HmacSHA1"));
DoubleClickCrypto.Price crypto = new DoubleClickCrypto.Price(keys);
return crypto.encodePriceValue(price, null);
}
//解密
private static double dePrice(String encryptedPrice, String encryptionKey, String integrityKey) throws InvalidKeyException, SignatureException {
DoubleClickCrypto.Keys keys = new DoubleClickCrypto.Keys(
new SecretKeySpec(encryptionKey.getBytes(), "HmacSHA1"),
new SecretKeySpec(integrityKey.getBytes(), "HmacSHA1"));
DoubleClickCrypto.Price crypto = new DoubleClickCrypto.Price(keys);
return crypto.decodePriceValue(encryptedPrice);
}
}
//工具类
import com.google.common.base.MoreObjects;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Ints;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.inject.Inject;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Math.min;
/**
* Encryption and decryption support for the DoubleClick Ad Exchange RTB protocol.
* <p>
* Encrypted payloads are wrapped by "packages" in the general format:
* <code>
* initVector:16 || E(payload:?) || I(signature:4)
* </code>
* <br>where:
* <ol>
* <li>{@code initVector = timestamp:8 || serverId:8} (AdX convention)</li>
* <li>{@code E(payload) = payload ^ hmac(encryptionKey, initVector)} per max-20-byte block</li>
* <li>{@code I(signature) = hmac(integrityKey, payloadKeys || initVector)[0..3]}</li>
* </ol>
* <p>
* This class, and all nested classes / subclasses, are threadsafe.
*/
public class DoubleClickCrypto {
private static final Logger logger = LoggerFactory.getLogger(DoubleClickCrypto.class);
public static final String KEY_ALGORITHM = "HmacSHA1";
/** Initialization vector offset in the crypto package. */
public static final int INITV_BASE = 0;
/** Initialization vector size. */
public static final int INITV_SIZE = 16;
/** Timestamp subfield offset in the initialization vector. */
public static final int INITV_TIMESTAMP_OFFSET = 0;
/** ServerId subfield offset in the initialization vector. */
public static final int INITV_SERVERID_OFFSET = 8;
/** Payload offset in the crypto package. */
public static final int PAYLOAD_BASE = INITV_BASE + INITV_SIZE;
/** Integrity signature size. */
public static final int SIGNATURE_SIZE = 4;
/** Overhead (non-Payload data) total size. */
public static final int OVERHEAD_SIZE = INITV_SIZE + SIGNATURE_SIZE;
private static final int COUNTER_PAGESIZE = 20;
private static final int COUNTER_SECTIONS = 3*256 + 1;
// private static final int MICROS_PER_CURRENCY_UNIT = 1_000_000;
private static final int MICROS_PER_CURRENCY_UNIT = 1;
private final Keys keys;
private final ThreadLocalRandom fastRandom = ThreadLocalRandom.current();
/**
* Initializes with the encryption keys.
*
* @param keys Keys for the buyer's Ad Exchange account
*/
public DoubleClickCrypto(Keys keys) {
this.keys = keys;
}
/**
* Decodes data, from string to binary form.
* The default implementation performs websafe-base64 decoding (RFC 3548).
*/
protected byte[] decode(String data) {
return data == null ? null : BaseEncoding.base64Url().decode(data);
}
/**
* Encodes data, from binary form to string.
* The default implementation performs websafe-base64 encoding (RFC 3548).
*/
protected String encode(byte[] data) {
return data == null ? null : BaseEncoding.base64Url().encode(data);
}
/**
* Decrypts data.
*
* @param cipherData {@code initVector || E(payload) || I(signature)}
* @return {@code initVector || payload || I'(signature)}
* Where I'(signature) == I(signature) for success, different for failure
*/
public byte[] decrypt(byte[] cipherData) throws SignatureException {
checkArgument(cipherData.length >= OVERHEAD_SIZE,
"Invalid cipherData, %s bytes", cipherData.length);
// workBytes := initVector || E(payload) || I(signature)
byte[] workBytes = cipherData.clone();
ByteBuffer workBuffer = ByteBuffer.wrap(workBytes);
boolean success = false;
try {
// workBytes := initVector || payload || I(signature)
xorPayloadToHmacPad(workBytes);
// workBytes := initVector || payload || I'(signature)
int confirmationSignature = hmacSignature(workBytes);
int integritySignature = workBuffer.getInt(workBytes.length - SIGNATURE_SIZE);
workBuffer.putInt(workBytes.length - SIGNATURE_SIZE, confirmationSignature);
if (confirmationSignature != integritySignature) {
throw new SignatureException("Signature mismatch: "
+ Integer.toHexString(confirmationSignature)
+ " vs " + Integer.toHexString(integritySignature));
}
if (logger.isDebugEnabled()) {
logger.debug(dump("Decrypted", cipherData, workBytes));
}
success = true;
return workBytes;
} finally {
if (!success && logger.isDebugEnabled()) {
logger.debug(dump("Decrypted (failed)", cipherData, workBytes));
}
}
}
/**
* Encrypts data.
*
* @param plainData {@code initVector || payload || zeros:4}
* @return {@code initVector || E(payload) || I(signature)}
*/
public byte[] encrypt(byte[] plainData) {
checkArgument(plainData.length >= OVERHEAD_SIZE,
"Invalid plainData, %s bytes", plainData.length);
// workBytes := initVector || payload || zeros:4
byte[] workBytes = plainData.clone();
ByteBuffer workBuffer = ByteBuffer.wrap(workBytes);
boolean success = false;
try {
// workBytes := initVector || payload || I(signature)
int signature = hmacSignature(workBytes);
workBuffer.putInt(workBytes.length - SIGNATURE_SIZE, signature);
// workBytes := initVector || E(payload) || I(signature)
xorPayloadToHmacPad(workBytes);
if (logger.isDebugEnabled()) {
logger.debug(dump("Encrypted", plainData, workBytes));
}
success = true;
return workBytes;
} finally {
if (!success && logger.isDebugEnabled()) {
logger.debug(dump("Encrypted (failed)", plainData, workBytes));
}
}
}
/**
* Creates the initialization vector from component {@code (timestamp, serverId)} fields.
* This is the format used by DoubleClick, and it's a good format generally,
* even though the initialization vector can be any random data (a cryptographic nonce).
* <p>
* NOTE: Follow the advice from
* https://developers.google.com/ad-exchange/rtb/response-guide/decrypt-price#detecting_stale
* by using a high-resolution timestamp; also if the {@code serverId} is not necessary, providing
* a random value there helps further to prevent replay attacks. In all methods that have
* a {@code initVector} parameter, passing null will cause {@code (current time, random)}
* to be used (so if you really want all-zeros {@code initVector}, e.g. in unit tests to make
* results reproducible, pass a zero-filled array).
*/
public byte[] createInitVector(Date timestamp, long serverId) {
byte[] initVector = new byte[INITV_SIZE];
ByteBuffer byteBuffer = ByteBuffer.wrap(initVector);
if (timestamp != null) {
byteBuffer.putLong(INITV_TIMESTAMP_OFFSET, timestamp.getTime());
}
byteBuffer.putLong(INITV_SERVERID_OFFSET, serverId);
return initVector;
}
/**
* Returns the {@code timestamp} field from encrypted or decrypted data. Assumes that its
* initialization vector has the structure {@code (timestamp, serverId)}.
*
* @param data Encrypted or decrypted data (the initialization vector is never encrypted)
* @return Timestamp subfield of the initialization vector.
*/
public Date getTimestamp(byte[] data) {
return new Date(ByteBuffer.wrap(data).getLong(INITV_BASE + INITV_TIMESTAMP_OFFSET));
}
/**
* Returns the {@code serverId} field from encrypted or decrypted data. Assumes that its
* initialization vector has the structure {@code (timestamp, serverId)}.
*
* @param data Encrypted or decrypted data (the initialization vector is never encrypted)
* @return Timestamp subfield of the initialization vector.
*/
public long getServerId(byte[] data) {
return ByteBuffer.wrap(data).getLong(INITV_BASE + INITV_SERVERID_OFFSET);
}
/**
* Packages plaintext payload for encryption; returns {@code initVector || payload || zeros:4}.
*/
protected byte[] initPlainData(int payloadSize, byte[] initVector) {
byte[] plainData = new byte[OVERHEAD_SIZE + payloadSize];
if (initVector == null) {
ByteBuffer byteBuffer = ByteBuffer.wrap(plainData);
byteBuffer.putLong(INITV_TIMESTAMP_OFFSET, System.nanoTime());
byteBuffer.putLong(INITV_SERVERID_OFFSET, fastRandom.nextLong());
} else {
System.arraycopy(initVector, 0, plainData, INITV_BASE, min(INITV_SIZE, initVector.length));
}
return plainData;
}
/**
* {@code payload = payload ^ hmac(encryptionKey, initVector || counterBytes)}
* per max-20-byte blocks.
*/
private void xorPayloadToHmacPad(byte[] workBytes) {
int payloadSize = workBytes.length - OVERHEAD_SIZE;
int sections = (payloadSize + COUNTER_PAGESIZE - 1) / COUNTER_PAGESIZE;
checkArgument(sections <= COUNTER_SECTIONS, "Payload is %s bytes, exceeds limit of %s",
payloadSize, COUNTER_PAGESIZE * COUNTER_SECTIONS);
Mac encryptionHmac = createMac();
byte[] pad = new byte[COUNTER_PAGESIZE + 3];
int counterSize = 0;
for (int section = 0; section < sections; ++section) {
int sectionBase = section * COUNTER_PAGESIZE;
int sectionSize = min(payloadSize - sectionBase, COUNTER_PAGESIZE);
try {
encryptionHmac.reset();
encryptionHmac.init(keys.getEncryptionKey());
encryptionHmac.update(workBytes, INITV_BASE, INITV_SIZE);
if (counterSize != 0) {
encryptionHmac.update(pad, COUNTER_PAGESIZE, counterSize);
}
encryptionHmac.doFinal(pad, 0);
} catch (ShortBufferException | InvalidKeyException e) {
throw new IllegalStateException(e);
}
for (int i = 0; i < sectionSize; ++i) {
workBytes[PAYLOAD_BASE + sectionBase + i] ^= pad[i];
}
Arrays.fill(pad, 0, COUNTER_PAGESIZE, (byte) 0);
if (counterSize == 0 || ++pad[COUNTER_PAGESIZE + counterSize - 1] == 0) {
++counterSize;
}
}
}
private static Mac createMac() {
try {
return Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
/**
* {@code signature = hmac(integrityKey, payload || initVector)}
*/
private int hmacSignature(byte[] workBytes) {
try {
Mac integrityHmac = createMac();
integrityHmac.init(keys.getIntegrityKey());
integrityHmac.update(workBytes, PAYLOAD_BASE, workBytes.length - OVERHEAD_SIZE);
integrityHmac.update(workBytes, INITV_BASE, INITV_SIZE);
return Ints.fromByteArray(integrityHmac.doFinal());
} catch (InvalidKeyException e) {
throw new IllegalStateException(e);
}
}
private static String dump(String header, byte[] inData, byte[] workBytes) {
ByteBuffer initvBuffer = ByteBuffer.wrap(workBytes, INITV_BASE, INITV_SIZE);
Date timestamp = new Date(initvBuffer.getLong(INITV_BASE + INITV_TIMESTAMP_OFFSET));
long serverId = initvBuffer.getLong(INITV_BASE + INITV_SERVERID_OFFSET);
return new StringBuilder()
.append(header)
.append(": initVector={timestamp ")
.append(DateFormat.getDateTimeInstance().format(timestamp))
.append(", serverId ").append(serverId)
.append("}, input =").append(BaseEncoding.base16().encode(inData))
.append(", output =").append(BaseEncoding.base16().encode(workBytes))
.toString();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).omitNullValues()
.add("keys", keys)
.toString();
}
/**
* Holds the keys used to configure DoubleClick cryptography.
*/
public static class Keys {
private final SecretKey encryptionKey;
private final SecretKey integrityKey;
public Keys(SecretKey encryptionKey, SecretKey integrityKey) throws InvalidKeyException {
this.encryptionKey = encryptionKey;
this.integrityKey = integrityKey;
// Forces early failure if any of the keys are not good.
// This allows us to spare callers from InvalidKeyException in several methods.
Mac hmac = DoubleClickCrypto.createMac();
hmac.init(encryptionKey);
hmac.reset();
hmac.init(integrityKey);
hmac.reset();
}
public SecretKey getEncryptionKey() {
return encryptionKey;
}
public SecretKey getIntegrityKey() {
return integrityKey;
}
@Override
public int hashCode() {
return encryptionKey.hashCode() ^ integrityKey.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof Keys)) {
return false;
}
Keys other = (Keys) obj;
return encryptionKey.equals(other.encryptionKey) && integrityKey.equals(other.integrityKey);
}
@Override public String toString() {
return MoreObjects.toStringHelper(this).omitNullValues()
.add("encryptionKey", encryptionKey.getAlgorithm() + '/' + encryptionKey.getFormat())
.add("integrityKey", integrityKey.getAlgorithm() + '/' + integrityKey.getFormat())
.toString();
}
}
/**
* Encryption for winning price.
* <p>
* See <a href="https://developers.google.com/ad-exchange/rtb/response-guide/decrypt-price">
* Decrypting Price Confirmations</a>.
*/
public static class Price extends DoubleClickCrypto {
private static final int PAYLOAD_SIZE = 8;
@Inject
public Price(Keys keys) {
super(keys);
}
/**
* Encrypts the winning price.
*
* @param priceValue the price in micros (1/1.000.000th of the currency unit)
* @param initVector up to 16 bytes of nonce data
* @return encrypted price
* @see #createInitVector(Date, long)
*/
public byte[] encryptPriceMicros(long priceValue, byte[] initVector) {
byte[] plainData = initPlainData(PAYLOAD_SIZE, initVector);
ByteBuffer.wrap(plainData).putLong(PAYLOAD_BASE, priceValue);
return encrypt(plainData);
}
/**
* Decrypts the winning price.
*
* @param priceCipher encrypted price
* @return the price value in micros (1/1.000.000th of the currency unit)
*/
public long decryptPriceMicros(byte[] priceCipher) throws SignatureException {
checkArgument(priceCipher.length == (OVERHEAD_SIZE + PAYLOAD_SIZE),
"Price is %s bytes, should be %s", priceCipher.length, (OVERHEAD_SIZE + PAYLOAD_SIZE));
byte[] plainData = decrypt(priceCipher);
return ByteBuffer.wrap(plainData).getLong(PAYLOAD_BASE);
}
/**
* Encrypts and encodes the winning price.
*
* @param priceMicros the price in micros (1/1.000.000th of the currency unit)
* @param initVector up to 16 bytes of nonce data, or {@code null} for default
* generated data (see {@link #createInitVector(Date, long)}
* @return encrypted price, encoded as websafe-base64
*/
public String encodePriceMicros(long priceMicros, byte[] initVector) {
return encode(encryptPriceMicros(priceMicros, initVector));
}
/**
* Encrypts and encodes the winning price.
*
* @param priceValue the price
* @param initVector up to 16 bytes of nonce data, or {@code null} for default
* generated data (see {@link #createInitVector(Date, long)}
* @return encrypted price, encoded as websafe-base64
*/
public String encodePriceValue(double priceValue, byte[] initVector) {
return encodePriceMicros((long) (priceValue * MICROS_PER_CURRENCY_UNIT), initVector);
}
/**
* Decodes and decrypts the winning price.
*
* @param priceCipher encrypted price, encoded as websafe-base64
* @return the price value in micros (1/1.000.000th of the currency unit)
*/
public long decodePriceMicros(String priceCipher) throws SignatureException {
return decryptPriceMicros(decode(checkNotNull(priceCipher)));
}
/**
* Decodes and decrypts the winning price.
*
* @param priceCipher encrypted price, encoded as websafe-base64
* @return the price value
*/
public double decodePriceValue(String priceCipher) throws SignatureException {
return decodePriceMicros(priceCipher) / ((double) MICROS_PER_CURRENCY_UNIT);
}
}
/**
* Encryption for Advertising ID.
* <p> See
* <a href="https://developers.google.com/ad-exchange/rtb/response-guide/decrypt-advertising-id">
* Decrypting Advertising ID</a>.
*/
public static class AdId extends DoubleClickCrypto {
private static final int PAYLOAD_SIZE = 16;
@Inject
public AdId(Keys keys) {
super(keys);
}
/**
* Encrypts the Advertising Id.
*
* @param adidPlain the AdId
* @param initVector up to 16 bytes of nonce data, or {@code null} for default
* generated data (see {@link #createInitVector(Date, long)}
* @return encrypted AdId
*/
public byte[] encryptAdId(byte[] adidPlain, byte[] initVector) {
checkArgument(adidPlain.length == PAYLOAD_SIZE,
"AdId is %s bytes, should be %s", adidPlain.length, PAYLOAD_SIZE);
byte[] plainData = initPlainData(PAYLOAD_SIZE, initVector);
System.arraycopy(adidPlain, 0, plainData, PAYLOAD_BASE, PAYLOAD_SIZE);
return encrypt(plainData);
}
/**
* Decrypts the AdId.
*
* @param adidCipher encrypted AdId
* @return the AdId
*/
public byte[] decryptAdId(byte[] adidCipher) throws SignatureException {
checkArgument(adidCipher.length == (OVERHEAD_SIZE + PAYLOAD_SIZE),
"AdId is %s bytes, should be %s", adidCipher.length, (OVERHEAD_SIZE + PAYLOAD_SIZE));
byte[] plainData = decrypt(adidCipher);
return Arrays.copyOfRange(plainData, PAYLOAD_BASE, plainData.length - SIGNATURE_SIZE);
}
}
/**
* Encryption for IDFA.
* <p> See
* <a href="https://support.google.com/adxbuyer/answer/3221407">
* Targeting mobile app inventory with IDFA</a>.
*/
public static class Idfa extends DoubleClickCrypto {
@Inject
public Idfa(Keys keys) {
super(keys);
}
/**
* Encrypts the IDFA.
*
* @param idfaPlain the IDFA
* @param initVector up to 16 bytes of nonce data, or {@code null} for default
* generated data (see {@link #createInitVector(Date, long)}
* @return encrypted IDFA
*/
public byte[] encryptIdfa(byte[] idfaPlain, byte[] initVector) {
byte[] plainData = initPlainData(idfaPlain.length, initVector);
System.arraycopy(idfaPlain, 0, plainData, PAYLOAD_BASE, idfaPlain.length);
return encrypt(plainData);
}
/**
* Decrypts the IDFA.
*
* @param idfaCipher encrypted IDFA
* @return the IDFA
*/
public byte[] decryptIdfa(byte[] idfaCipher) throws SignatureException {
byte[] plainData = decrypt(idfaCipher);
return Arrays.copyOfRange(plainData, PAYLOAD_BASE, plainData.length - SIGNATURE_SIZE);
}
/**
* Encrypts and encodes the IDFA.
*
* @param idfaPlain the IDFA
* @param initVector up to 16 bytes of nonce data, or {@code null} for default
* generated data (see {@link #createInitVector(Date, long)}
* @return encrypted IDFA, websafe-base64 encoded
*/
public String encodeIdfa(byte[] idfaPlain, byte[] initVector) {
return encode(encryptIdfa(idfaPlain, initVector));
}
/**
* Decodes and decrypts the IDFA.
*
* @param idfaCipher encrypted IDFA, websafe-base64 encoded
* @return the IDFA
*/
public byte[] decodeIdfa(String idfaCipher) throws SignatureException {
return decryptIdfa(decode(idfaCipher));
}
}
/**
* Encryption for {@code HyperlocalSet} geofence information.
* <p> See
* <a href="https://developers.google.com/ad-exchange/rtb/response-guide/decrypt-hyperlocal">
* Decrypting Hyperlocal Targeting Signals</a>.
*/
public static class Hyperlocal extends DoubleClickCrypto {
@Inject
public Hyperlocal(Keys keys) {
super(keys);
}
/**
* Encrypts the serialized {@code HyperlocalSet}.
*
* @param hyperlocalPlain the {@code HyperlocalSet}
* @param initVector up to 16 bytes of nonce data, or {@code null} for default
* generated data (see {@link #createInitVector(Date, long)}
* @return encrypted {@code HyperlocalSet}
*/
public byte[] encryptHyperlocal(byte[] hyperlocalPlain, byte[] initVector) {
byte[] plainData = initPlainData(hyperlocalPlain.length, initVector);
System.arraycopy(hyperlocalPlain, 0, plainData, PAYLOAD_BASE, hyperlocalPlain.length);
return encrypt(plainData);
}
/**
* Decrypts the serialized {@code HyperlocalSet}.
*
* @param hyperlocalCipher encrypted {@code HyperlocalSet}
* @return the {@code HyperLocalSet}
*/
public byte[] decryptHyperlocal(byte[] hyperlocalCipher) throws SignatureException {
byte[] plainData = decrypt(hyperlocalCipher);
return Arrays.copyOfRange(plainData, PAYLOAD_BASE, plainData.length - SIGNATURE_SIZE);
}
}
}