Merge pull request #2042 from nspcc-dev/oracle-not-supported

Check oracle response Content-Type header
This commit is contained in:
Roman Khimov 2021-07-13 11:31:37 +03:00 committed by GitHub
commit 83a557f0eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 107 additions and 13 deletions

View file

@ -61,6 +61,8 @@ ApplicationConfiguration:
MinPeers: 5 MinPeers: 5
Oracle: Oracle:
Enabled: false Enabled: false
AllowedContentTypes:
- application/json
P2PNotary: P2PNotary:
Enabled: false Enabled: false
UnlockWallet: UnlockWallet:

View file

@ -57,6 +57,8 @@ ApplicationConfiguration:
MinPeers: 3 MinPeers: 3
Oracle: Oracle:
Enabled: false Enabled: false
AllowedContentTypes:
- application/json
Nodes: Nodes:
- 172.200.0.1:30333 - 172.200.0.1:30333
- 172.200.0.2:30334 - 172.200.0.2:30334

View file

@ -57,6 +57,8 @@ ApplicationConfiguration:
MinPeers: 3 MinPeers: 3
Oracle: Oracle:
Enabled: false Enabled: false
AllowedContentTypes:
- application/json
Nodes: Nodes:
- 172.200.0.1:30333 - 172.200.0.1:30333
- 172.200.0.2:30334 - 172.200.0.2:30334

View file

@ -51,6 +51,8 @@ ApplicationConfiguration:
MinPeers: 0 MinPeers: 0
Oracle: Oracle:
Enabled: false Enabled: false
AllowedContentTypes:
- application/json
Nodes: Nodes:
- 172.200.0.1:30333 - 172.200.0.1:30333
RequestTimeout: 5s RequestTimeout: 5s

View file

@ -57,6 +57,8 @@ ApplicationConfiguration:
MinPeers: 3 MinPeers: 3
Oracle: Oracle:
Enabled: false Enabled: false
AllowedContentTypes:
- application/json
Nodes: Nodes:
- 172.200.0.1:30333 - 172.200.0.1:30333
- 172.200.0.2:30334 - 172.200.0.2:30334

View file

@ -57,6 +57,8 @@ ApplicationConfiguration:
MinPeers: 3 MinPeers: 3
Oracle: Oracle:
Enabled: false Enabled: false
AllowedContentTypes:
- application/json
Nodes: Nodes:
- 172.200.0.1:30333 - 172.200.0.1:30333
- 172.200.0.2:30334 - 172.200.0.2:30334

View file

@ -61,6 +61,8 @@ ApplicationConfiguration:
MinPeers: 5 MinPeers: 5
Oracle: Oracle:
Enabled: false Enabled: false
AllowedContentTypes:
- application/json
P2PNotary: P2PNotary:
Enabled: false Enabled: false
UnlockWallet: UnlockWallet:

View file

@ -19,6 +19,8 @@ Parameters:
* `AllowPrivateHost`: boolean value, enables/disables private IPs (like * `AllowPrivateHost`: boolean value, enables/disables private IPs (like
127.0.0.1 or 192.168.0.1) for https requests, it defaults to false and it's 127.0.0.1 or 192.168.0.1) for https requests, it defaults to false and it's
false on public networks, but you can enable it for private ones. false on public networks, but you can enable it for private ones.
* `AllowedContentTypes`: list of allowed MIME types. Only `application/json`
is allowed by default. Can be left empty to allow everything.
* `Nodes`: list of oracle node RPC endpoints, it's used for oracle node * `Nodes`: list of oracle node RPC endpoints, it's used for oracle node
communication. All oracle nodes should be specified there. communication. All oracle nodes should be specified there.
* `NeoFS`: a subsection of its own for NeoFS configuration with two * `NeoFS`: a subsection of its own for NeoFS configuration with two

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

@ -34,7 +34,8 @@ func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string) oracle.Config
Log: zaptest.NewLogger(t), Log: zaptest.NewLogger(t),
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,7 +342,10 @@ 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,
Body: newResponseBody(resp.body), Header: http.Header{
"Content-Type": {resp.ct},
},
Body: newResponseBody(resp.body),
}, nil }, nil
} }
return nil, errors.New("error during request") return nil, errors.New("error during request")
@ -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

@ -26,15 +26,16 @@ const MaxOracleResultSize = math.MaxUint16
// Enumeration of possible oracle response types. // Enumeration of possible oracle response types.
const ( const (
Success OracleResponseCode = 0x00 Success OracleResponseCode = 0x00
ProtocolNotSupported OracleResponseCode = 0x10 ProtocolNotSupported OracleResponseCode = 0x10
ConsensusUnreachable OracleResponseCode = 0x12 ConsensusUnreachable OracleResponseCode = 0x12
NotFound OracleResponseCode = 0x14 NotFound OracleResponseCode = 0x14
Timeout OracleResponseCode = 0x16 Timeout OracleResponseCode = 0x16
Forbidden OracleResponseCode = 0x18 Forbidden OracleResponseCode = 0x18
ResponseTooLarge OracleResponseCode = 0x1a ResponseTooLarge OracleResponseCode = 0x1a
InsufficientFunds OracleResponseCode = 0x1c InsufficientFunds OracleResponseCode = 0x1c
Error OracleResponseCode = 0xff ContentTypeNotSupported OracleResponseCode = 0x1f
Error OracleResponseCode = 0xff
) )
// Various validation errors. // Various validation errors.

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"
@ -118,6 +119,7 @@ func (o *Oracle) processRequest(priv *keys.PrivateKey, req request) error {
break break
} }
httpReq.Header.Set("User-Agent", "NeoOracleService/3.0") httpReq.Header.Set("User-Agent", "NeoOracleService/3.0")
httpReq.Header.Set("Content-Type", "application/json")
r, err := o.Client.Do(httpReq) r, err := o.Client.Do(httpReq)
if err != nil { if err != nil {
o.Log.Warn("oracle request failed", zap.String("url", req.Req.URL), zap.Error(err)) o.Log.Warn("oracle request failed", zap.String("url", req.Req.URL), zap.Error(err))
@ -126,6 +128,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 +249,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))
}