Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate native inputs #353

Merged
merged 16 commits into from
Mar 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 23 additions & 58 deletions endpoints/openrtb2/amp_auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/mxmCherry/openrtb"
"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/exchange"
"github.com/prebid/prebid-server/openrtb_ext"
"github.com/prebid/prebid-server/pbsmetrics"
"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
"github.com/rcrowley/go-metrics"
)

Expand All @@ -22,18 +22,22 @@ import (

// TestGoodRequests makes sure that the auction runs properly-formatted stored bids correctly.
func TestGoodAmpRequests(t *testing.T) {
goodRequests := map[string]json.RawMessage{
"10": json.RawMessage(validRequest(t, "site.json")),
}

theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList())
endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, &bidderParamValidator{}, &mockAmpStoredReqFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics)
endpoint, _ := NewAmpEndpoint(&mockAmpExchange{}, &bidderParamValidator{}, &mockAmpStoredReqFetcher{goodRequests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics)

for _, requestID := range storedValidRequests {
for requestID, _ := range goodRequests {
request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil)
recorder := httptest.NewRecorder()
endpoint(recorder, request, nil)

if recorder.Code != http.StatusOK {
t.Errorf("Expected status %d. Got %d. Request config ID was %s", http.StatusOK, recorder.Code, requestID)
t.Errorf("Response body was: %s", recorder.Body)
t.Errorf("Request was: %s", testAmpStoredRequestData[requestID])
t.Errorf("Request was: %s", string(goodRequests[requestID]))
}

var response AmpResponse
Expand All @@ -52,9 +56,19 @@ func TestGoodAmpRequests(t *testing.T) {

// TestBadRequests makes sure we return 400's on bad requests.
func TestAmpBadRequests(t *testing.T) {
badRequests := map[string]json.RawMessage{
"11": json.RawMessage(validRequest(t, "app.json")),
"12": json.RawMessage(validRequest(t, "timeout.json")),
}

files := fetchFiles(t, "sample-requests/invalid-whole")
for index, file := range files {
badRequests[strconv.Itoa(100+index)] = readFile(t, "sample-requests/invalid-whole/"+file.Name())
}

theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList())
endpoint, _ := NewEndpoint(&mockAmpExchange{}, &bidderParamValidator{}, empty_fetcher.EmptyFetcher(), &config.Configuration{MaxRequestSize: maxSize}, theMetrics)
for _, requestID := range storedInvalidRequests {
endpoint, _ := NewEndpoint(&mockAmpExchange{}, &bidderParamValidator{}, &mockAmpStoredReqFetcher{badRequests}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics)
for requestID, _ := range badRequests {
request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil)
recorder := httptest.NewRecorder()

Expand All @@ -66,61 +80,12 @@ func TestAmpBadRequests(t *testing.T) {
}
}

// StoredRequest testing

var storedValidRequests = []string{"10"}
var storedInvalidRequests = []string{"11", "12", "100", "101", "102", "103", "104", "105", "106", "107", "108", "109",
"110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
"120", "121", "122", "123", "124", "125", "126", "127", "128", "129",
"103", "131", "132", "133", "134"}

// Test stored request data
var testAmpStoredRequestData = map[string]json.RawMessage{
"10": json.RawMessage(validRequests[0]),
"11": json.RawMessage(validRequests[1]),
"12": json.RawMessage(validRequests[2]),
"100": json.RawMessage(invalidRequests[0]),
"101": json.RawMessage(invalidRequests[0]),
"102": json.RawMessage(invalidRequests[0]),
"103": json.RawMessage(invalidRequests[0]),
"104": json.RawMessage(invalidRequests[0]),
"105": json.RawMessage(invalidRequests[0]),
"106": json.RawMessage(invalidRequests[0]),
"107": json.RawMessage(invalidRequests[0]),
"108": json.RawMessage(invalidRequests[0]),
"109": json.RawMessage(invalidRequests[0]),
"110": json.RawMessage(invalidRequests[0]),
"111": json.RawMessage(invalidRequests[0]),
"112": json.RawMessage(invalidRequests[0]),
"113": json.RawMessage(invalidRequests[0]),
"114": json.RawMessage(invalidRequests[0]),
"115": json.RawMessage(invalidRequests[0]),
"116": json.RawMessage(invalidRequests[0]),
"117": json.RawMessage(invalidRequests[0]),
"118": json.RawMessage(invalidRequests[0]),
"119": json.RawMessage(invalidRequests[0]),
"120": json.RawMessage(invalidRequests[0]),
"121": json.RawMessage(invalidRequests[0]),
"122": json.RawMessage(invalidRequests[0]),
"123": json.RawMessage(invalidRequests[0]),
"124": json.RawMessage(invalidRequests[0]),
"125": json.RawMessage(invalidRequests[0]),
"126": json.RawMessage(invalidRequests[0]),
"127": json.RawMessage(invalidRequests[0]),
"128": json.RawMessage(invalidRequests[0]),
"129": json.RawMessage(invalidRequests[0]),
"130": json.RawMessage(invalidRequests[0]),
"131": json.RawMessage(invalidRequests[0]),
"132": json.RawMessage(invalidRequests[0]),
"133": json.RawMessage(invalidRequests[0]),
"134": json.RawMessage(invalidRequests[0]),
}

type mockAmpStoredReqFetcher struct {
data map[string]json.RawMessage
}

func (cf mockAmpStoredReqFetcher) FetchRequests(ctx context.Context, ids []string) (map[string]json.RawMessage, []error) {
return testAmpStoredRequestData, nil
func (cf *mockAmpStoredReqFetcher) FetchRequests(ctx context.Context, ids []string) (map[string]json.RawMessage, []error) {
return cf.data, nil
}

type mockAmpExchange struct {
Expand Down
184 changes: 180 additions & 4 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/mssola/user_agent"
"github.com/mxmCherry/openrtb"
nativeRequests "github.com/mxmCherry/openrtb/native/request"
"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/exchange"
"github.com/prebid/prebid-server/openrtb_ext"
Expand Down Expand Up @@ -259,10 +260,8 @@ func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]strin
}
}

if imp.Native != nil {
if imp.Native.Request == "" {
return fmt.Errorf("request.imp[%d].native.request must be a JSON encoded string conforming to the openrtb 1.2 Native spec", index)
}
if err := fillAndValidateNative(imp.Native, index); err != nil {
return err
}

if err := validatePmp(imp.PMP, index); err != nil {
Expand Down Expand Up @@ -304,6 +303,183 @@ func validateBanner(banner *openrtb.Banner, impIndex int) error {
return nil
}

// fillAndValidateNative validates the request, and assigns the Asset IDs as recommended by the Native v1.2 spec.
func fillAndValidateNative(n *openrtb.Native, impIndex int) error {
if n == nil {
return nil
}

var nativePayload nativeRequests.Request
if err := json.Unmarshal(json.RawMessage(n.Request), &nativePayload); err != nil {
return err
}

if err := validateNativeContext(nativePayload.Context, impIndex); err != nil {
return err
}
if err := validateNativePlacementType(nativePayload.PlcmtType, impIndex); err != nil {
return err
}
if err := fillAndValidateNativeAssets(nativePayload.Assets, impIndex); err != nil {
return err
}

// TODO #218: Validate eventtrackers once mxmcherry/openrtb has been updated to support Native v1.2

serialized, err := json.Marshal(nativePayload)
if err != nil {
return err
}
encoded, err := json.Marshal(string(serialized))
if err != nil {
return err
}
n.Request = string(encoded)
return nil
}

func validateNativeContext(c nativeRequests.ContextType, impIndex int) error {
if c < 1 || c > 3 {
return fmt.Errorf("request.imp[%d].native.request.context must be in the range [1, 3]. Got %d", impIndex, c)
}
return nil
}

func validateNativePlacementType(pt nativeRequests.PlacementType, impIndex int) error {
if pt < 1 || pt > 4 {
return fmt.Errorf("request.imp[%d].native.request.plcmttype must be in the range [1, 4]. Got %d", impIndex, pt)
}
return nil
}

func fillAndValidateNativeAssets(assets []nativeRequests.Asset, impIndex int) error {
if len(assets) < 1 {
return fmt.Errorf("request.imp[%d].native.request.assets must be an array containing at least one object.", impIndex)
}

for i := 0; i < len(assets); i++ {
// Per the OpenRTB spec docs, this is a "unique asset ID, assigned by exchange. Typically a counter for the array"
// To avoid conflict with the Request, we'll return a 400 if the Request _did_ define this ID,
// and then populate it as the spec suggests.
if err := validateNativeAsset(assets[i], impIndex, i); err != nil {
return err
}
assets[i].ID = int64(i)
Copy link
Collaborator

@hhhjort hhhjort Feb 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the spec: "Typically a counter for the array." This enforces that it will be a counter. I don't know of any use cases for ID being anything else, so its probably good. But potentially will break something that stretches the spec here.

Oh, I see below that we are changing the spec that we don't want anyone else filling asset IDs. So any breaking changes should not be a surprise, but still could potentially block some funky implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah... to be honest I'm not 100% sure how to handle this. I saw the following:

Unique asset ID, assigned by exchange.

Since Prebid is the exchange, in this context, I don't think we're breaking the spec here. We just decided that the "typical" implementation is the one we'll be using.

I decided to return 4xx on IDs sent by the Publisher just to prevent all sorts of nasty logic/confusion. As a general rule, we never overwrite anything which the Publisher gives us. So if we allowed IDs, then we'd have to make sure that the ones we added weren't duplicates, which gets... messy.

Decided to just reject those requests to keep things simple.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with that then.

}
return nil
}

func validateNativeAsset(asset nativeRequests.Asset, impIndex int, assetIndex int) error {
if asset.ID != 0 {
return fmt.Errorf(`request.imp[%d].native.request.assets[%d].id must not be defined. Prebid Server will set this automatically, using the index of the asset in the array as the ID.`, impIndex, assetIndex)
}

foundType := false

if asset.Title != nil {
foundType = true
if err := validateNativeAssetTitle(asset.Title, impIndex, assetIndex); err != nil {
return err
}
}

if asset.Img != nil {
if foundType {
return fmt.Errorf("request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", impIndex, assetIndex)
}
foundType = true
if err := validateNativeAssetImg(asset.Img, impIndex, assetIndex); err != nil {
return err
}
}

if asset.Video != nil {
if foundType {
return fmt.Errorf("request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", impIndex, assetIndex)
}
foundType = true
if err := validateNativeAssetVideo(asset.Video, impIndex, assetIndex); err != nil {
return err
}
}

if asset.Data != nil {
if foundType {
return fmt.Errorf("request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", impIndex, assetIndex)
}
foundType = true
if err := validateNativeAssetData(asset.Data, impIndex, assetIndex); err != nil {
return err
}
}

return nil
}

func validateNativeAssetTitle(title *nativeRequests.Title, impIndex int, assetIndex int) error {
if title.Len < 1 {
return fmt.Errorf("request.imp[%d].native.request.assets[%d].title.len must be a positive integer", impIndex, assetIndex)
}
return nil
}

func validateNativeAssetImg(image *nativeRequests.Image, impIndex int, assetIndex int) error {
// Note that w, wmin, h, and hmin cannot be negative because these variables use unsigned ints.
// Those fail during the standard json.Unmarshal() call.
if image.W == 0 && image.WMin == 0 {
return fmt.Errorf(`request.imp[%d].native.request.assets[%d].img must contain at least one of "w" or "wmin"`, impIndex, assetIndex)
}
if image.H == 0 && image.HMin == 0 {
return fmt.Errorf(`request.imp[%d].native.request.assets[%d].img must contain at least one of "h" or "hmin"`, impIndex, assetIndex)
}

return nil
}

func validateNativeAssetVideo(video *nativeRequests.Video, impIndex int, assetIndex int) error {
if len(video.MIMEs) < 1 {
return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.mimes must be an array with at least one MIME type.", impIndex, assetIndex)
}
if video.MinDuration < 1 {
return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer.", impIndex, assetIndex)
}
if video.MaxDuration < 1 {
return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer.", impIndex, assetIndex)
}
if err := validateNativeVideoProtocols(video.Protocols, impIndex, assetIndex); err != nil {
return err
}

return nil
}

func validateNativeAssetData(data *nativeRequests.Data, impIndex int, assetIndex int) error {
if data.Type < 1 || data.Type > 12 {
return fmt.Errorf("request.imp[%d].native.request.assets[%d].data.type must in the range [1, 12]. Got %d.", impIndex, assetIndex, data.Type)
}

return nil
}

func validateNativeVideoProtocols(protocols []nativeRequests.Protocol, impIndex int, assetIndex int) error {
if len(protocols) < 1 {
return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least one element", impIndex, assetIndex)
}
for i := 0; i < len(protocols); i++ {
if err := validateNativeVideoProtocol(protocols[i], impIndex, assetIndex, i); err != nil {
return err
}
}
return nil
}

func validateNativeVideoProtocol(protocol nativeRequests.Protocol, impIndex int, assetIndex int, protocolIndex int) error {
if protocol < 0 || protocol > 10 {
return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols[%d] must be in the range [1, 10]. Got %d", impIndex, assetIndex, protocolIndex, protocol)
}
return nil
}

func validateFormat(format *openrtb.Format, impIndex int, formatIndex int) error {
usesHW := format.W != 0 || format.H != 0
usesRatios := format.WMin != 0 || format.WRatio != 0 || format.HRatio != 0
Expand Down
Loading