oracle: check response Content-Type

If not specified everything is allowed.

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgeniy Stratonikov 2021-07-06 16:20:01 +03:00
parent 1853d0c713
commit 8e9302f40b
6 changed files with 90 additions and 13 deletions

View file

@ -6,6 +6,7 @@ import "time"
type OracleConfiguration struct { type OracleConfiguration struct {
Enabled bool `yaml:"Enabled"` Enabled bool `yaml:"Enabled"`
AllowPrivateHost bool `yaml:"AllowPrivateHost"` AllowPrivateHost bool `yaml:"AllowPrivateHost"`
AllowedContentTypes []string `yaml:"AllowedContentTypes"`
Nodes []string `yaml:"Nodes"` Nodes []string `yaml:"Nodes"`
NeoFS NeoFSConfiguration `yaml:"NeoFS"` NeoFS NeoFSConfiguration `yaml:"NeoFS"`
MaxTaskTimeout time.Duration `yaml:"MaxTaskTimeout"` MaxTaskTimeout time.Duration `yaml:"MaxTaskTimeout"`

View file

@ -35,6 +35,7 @@ func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string) oracle.Config
Network: netmode.UnitTestNet, Network: netmode.UnitTestNet,
MainCfg: config.OracleConfiguration{ MainCfg: config.OracleConfiguration{
RefreshInterval: time.Second, RefreshInterval: time.Second,
AllowedContentTypes: []string{"application/json"},
UnlockWallet: config.Wallet{ UnlockWallet: config.Wallet{
Path: path.Join(oracleModulePath, w), Path: path.Join(oracleModulePath, w),
Password: pass, Password: pass,
@ -147,6 +148,8 @@ func TestOracle(t *testing.T) {
putOracleRequest(t, cs.Hash, bc, "https://get.filter", &flt, "handle", []byte{}, 10_000_000) putOracleRequest(t, cs.Hash, bc, "https://get.filter", &flt, "handle", []byte{}, 10_000_000)
putOracleRequest(t, cs.Hash, bc, "https://get.filterinv", &flt, "handle", []byte{}, 10_000_000) putOracleRequest(t, cs.Hash, bc, "https://get.filterinv", &flt, "handle", []byte{}, 10_000_000)
putOracleRequest(t, cs.Hash, bc, "https://get.invalidcontent", nil, "handle", []byte{}, 10_000_000)
checkResp := func(t *testing.T, id uint64, resp *transaction.OracleResponse) *state.OracleRequest { checkResp := func(t *testing.T, id uint64, resp *transaction.OracleResponse) *state.OracleRequest {
req, err := oracleCtr.GetRequestInternal(bc.dao, id) req, err := oracleCtr.GetRequestInternal(bc.dao, id)
require.NoError(t, err) require.NoError(t, err)
@ -262,6 +265,12 @@ func TestOracle(t *testing.T) {
}) })
}) })
}) })
t.Run("InvalidContentType", func(t *testing.T) {
checkResp(t, 11, &transaction.OracleResponse{
ID: 11,
Code: transaction.ContentTypeNotSupported,
})
})
} }
func TestOracleFull(t *testing.T) { func TestOracleFull(t *testing.T) {
@ -322,6 +331,7 @@ type (
testResponse struct { testResponse struct {
code int code int
ct string
body []byte body []byte
} }
) )
@ -332,6 +342,9 @@ func (c *httpClient) Do(req *http.Request) (*http.Response, error) {
if ok { if ok {
return &http.Response{ return &http.Response{
StatusCode: resp.code, StatusCode: resp.code,
Header: http.Header{
"Content-Type": {resp.ct},
},
Body: newResponseBody(resp.body), Body: newResponseBody(resp.body),
}, nil }, nil
} }
@ -343,44 +356,59 @@ func newDefaultHTTPClient() oracle.HTTPClient {
responses: map[string]testResponse{ responses: map[string]testResponse{
"https://get.1234": { "https://get.1234": {
code: http.StatusOK, code: http.StatusOK,
ct: "application/json",
body: []byte{1, 2, 3, 4}, body: []byte{1, 2, 3, 4},
}, },
"https://get.4321": { "https://get.4321": {
code: http.StatusOK, code: http.StatusOK,
ct: "application/json",
body: []byte{4, 3, 2, 1}, body: []byte{4, 3, 2, 1},
}, },
"https://get.timeout": { "https://get.timeout": {
code: http.StatusRequestTimeout, code: http.StatusRequestTimeout,
ct: "application/json",
body: []byte{}, body: []byte{},
}, },
"https://get.notfound": { "https://get.notfound": {
code: http.StatusNotFound, code: http.StatusNotFound,
ct: "application/json",
body: []byte{}, body: []byte{},
}, },
"https://get.forbidden": { "https://get.forbidden": {
code: http.StatusForbidden, code: http.StatusForbidden,
ct: "application/json",
body: []byte{}, body: []byte{},
}, },
"https://private.url": { "https://private.url": {
code: http.StatusOK, code: http.StatusOK,
ct: "application/json",
body: []byte("passwords"), body: []byte("passwords"),
}, },
"https://get.big": { "https://get.big": {
code: http.StatusOK, code: http.StatusOK,
ct: "application/json",
body: make([]byte, transaction.MaxOracleResultSize+1), body: make([]byte, transaction.MaxOracleResultSize+1),
}, },
"https://get.maxallowed": { "https://get.maxallowed": {
code: http.StatusOK, code: http.StatusOK,
ct: "application/json",
body: make([]byte, transaction.MaxOracleResultSize), body: make([]byte, transaction.MaxOracleResultSize),
}, },
"https://get.filter": { "https://get.filter": {
code: http.StatusOK, code: http.StatusOK,
ct: "application/json",
body: []byte(`{"Values":["one", 2, 3],"Another":null}`), body: []byte(`{"Values":["one", 2, 3],"Another":null}`),
}, },
"https://get.filterinv": { "https://get.filterinv": {
code: http.StatusOK, code: http.StatusOK,
ct: "application/json",
body: []byte{0xFF}, body: []byte{0xFF},
}, },
"https://get.invalidcontent": {
code: http.StatusOK,
ct: "image/gif",
body: []byte{1, 2, 3},
},
}, },
} }
} }

View file

@ -34,6 +34,7 @@ const (
Forbidden OracleResponseCode = 0x18 Forbidden OracleResponseCode = 0x18
ResponseTooLarge OracleResponseCode = 0x1a ResponseTooLarge OracleResponseCode = 0x1a
InsufficientFunds OracleResponseCode = 0x1c InsufficientFunds OracleResponseCode = 0x1c
ContentTypeNotSupported OracleResponseCode = 0x1f
Error OracleResponseCode = 0xff Error OracleResponseCode = 0xff
) )

View file

@ -16,6 +16,7 @@ func _() {
_ = x[Forbidden-24] _ = x[Forbidden-24]
_ = x[ResponseTooLarge-26] _ = x[ResponseTooLarge-26]
_ = x[InsufficientFunds-28] _ = x[InsufficientFunds-28]
_ = x[ContentTypeNotSupported-31]
_ = x[Error-255] _ = x[Error-255]
} }
@ -28,7 +29,8 @@ const (
_OracleResponseCode_name_5 = "Forbidden" _OracleResponseCode_name_5 = "Forbidden"
_OracleResponseCode_name_6 = "ResponseTooLarge" _OracleResponseCode_name_6 = "ResponseTooLarge"
_OracleResponseCode_name_7 = "InsufficientFunds" _OracleResponseCode_name_7 = "InsufficientFunds"
_OracleResponseCode_name_8 = "Error" _OracleResponseCode_name_8 = "ContentTypeNotSupported"
_OracleResponseCode_name_9 = "Error"
) )
func (i OracleResponseCode) String() string { func (i OracleResponseCode) String() string {
@ -49,8 +51,10 @@ func (i OracleResponseCode) String() string {
return _OracleResponseCode_name_6 return _OracleResponseCode_name_6
case i == 28: case i == 28:
return _OracleResponseCode_name_7 return _OracleResponseCode_name_7
case i == 255: case i == 31:
return _OracleResponseCode_name_8 return _OracleResponseCode_name_8
case i == 255:
return _OracleResponseCode_name_9
default: default:
return "OracleResponseCode(" + strconv.FormatInt(int64(i), 10) + ")" return "OracleResponseCode(" + strconv.FormatInt(int64(i), 10) + ")"
} }

View file

@ -3,6 +3,7 @@ package oracle
import ( import (
"context" "context"
"errors" "errors"
"mime"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
@ -126,6 +127,11 @@ func (o *Oracle) processRequest(priv *keys.PrivateKey, req request) error {
} }
switch r.StatusCode { switch r.StatusCode {
case http.StatusOK: case http.StatusOK:
if !checkMediaType(r.Header.Get("Content-Type"), o.MainCfg.AllowedContentTypes) {
resp.Code = transaction.ContentTypeNotSupported
break
}
result, err := readResponse(r.Body, transaction.MaxOracleResultSize) result, err := readResponse(r.Body, transaction.MaxOracleResultSize)
if err != nil { if err != nil {
if errors.Is(err, ErrResponseTooLarge) { if errors.Is(err, ErrResponseTooLarge) {
@ -242,3 +248,21 @@ func (o *Oracle) processFailedRequest(priv *keys.PrivateKey, req request) {
o.getOnTransaction()(readyTx) o.getOnTransaction()(readyTx)
} }
} }
func checkMediaType(hdr string, allowed []string) bool {
if len(allowed) == 0 {
return true
}
typ, _, err := mime.ParseMediaType(hdr)
if err != nil {
return false
}
for _, ct := range allowed {
if ct == typ {
return true
}
}
return false
}

View file

@ -0,0 +1,19 @@
package oracle
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCheckContentType(t *testing.T) {
allowedTypes := []string{"application/json", "text/plain"}
require.True(t, checkMediaType("application/json", allowedTypes))
require.True(t, checkMediaType("application/json; param=value", allowedTypes))
require.True(t, checkMediaType("text/plain; filename=file.txt", allowedTypes))
require.False(t, checkMediaType("image/gif", allowedTypes))
require.True(t, checkMediaType("image/gif", nil))
require.False(t, checkMediaType("invalid format", allowedTypes))
}