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

HTTP / TCP Test Harness for JSON RPC #52

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"github.com/maticnetwork/polygon-cli/cmd/fork"
"github.com/maticnetwork/polygon-cli/cmd/parseethwallet"
"github.com/maticnetwork/polygon-cli/cmd/testharness"
"os"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -87,6 +88,7 @@ func init() {
rootCmd.AddCommand(wallet.WalletCmd)
rootCmd.AddCommand(fork.ForkCmd)
rootCmd.AddCommand(parseethwallet.ParseETHWalletCmd)
rootCmd.AddCommand(testharness.TestHarnessCmd)
}

// initConfig reads in config file and ENV variables if set.
Expand Down
63 changes: 63 additions & 0 deletions cmd/testharness/jsonrpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package testharness

// junkJSONRPC is a static list of various test strings some of which are valid json but invalid json rpc. Some are just complete trash
var junkJSONRPC = []string{
// These should be acceptable
`{"jsonrpc": "2.0", "result": null, "id": 1}`, // null result should be fine
`{"jsonrpc": "2.0", "result": 123, "id": 1}`, // number should be fine
`{"jsonrpc": "2.0", "result": "123", "id": 1}`, // string should be fine
`{"jsonrpc": "2.0", "result": [1, 2 ,3], "id": 1}`, // array should be fine
`{"jsonrpc": "2.0", "result": {}, "id": 1}`, // empty object should be okay
`{"jsonrpc": "2.0", "result": {"1": true, "2": true, "3": true}, "id": 1}`, // object should be okay
`{"jsonrpc": "2.0", "result": 123, "id": "abc123"}`, // string id should be okay
`[{"jsonrpc": "2.0", "result": 123, "id": 1},{"jsonrpc": "2.0", "result": 456, "id": 2}]`, // batch response
`{"jsonrpc": "2.0", "error": {"code": -32700, "message": "test err"}, "id": null}`, // error response should be fine

// these are dubious
`{"jsonrpc": "2.0", "id": 1}`, // completely missing result
`{"jsonrpc": "2", "result": 123, "id": 1}`, // 2 instead of 2.0
`{"jsonrpc": "2.0", "result": 123, "id": null}`, // null id
`{"jsonrpc": "2.0", "result": 123, "id": 1}` + string([]byte{0}), // trailing null byte
`{"jsonrpc": "2.0", "result": 123, "id": 1}` + "\r\n" + `{"jsonrpc": "2.0", "result": 123, "id": 1}`, // \r\n seperator
`{"jsonrpc": "2.0", "result": 123, "id": 1}` + "\r" + `{"jsonrpc": "2.0", "result": 123, "id": 1}`, // \r seperator
`{"jsonrpc": "2.0", "result": 123, "id": 1}` + "\n" + `{"jsonrpc": "2.0", "result": 123, "id": 1}`, // \n seperator
`{"jsonrpc": "2.0", "result": 123}`, // missing id all together
`{"result": 123, "id": 1}`, // missing json rpc all together.. This would be jsonrpc 1.0
`[{"jsonrpc": "2.0", "result": 123, "id": 1},{"jsonrpc": "2.0", "result": 123, "id": 1}]`, // batch response with two responses fro the same id
`{"jsonrpc": "2.0", "error": {"code": -32700, "message": "test err"}, "id": 1}`, // error id must be null
`{"jsonrpc": "2.0", "error": {"code": "foo", "message": "test err"}, "id": null}`,
`{"jsonrpc": "2.0", "error": {"code": 2.718282828, "message": "test err"}, "id": null}`, // non-integer code
`{"jsonrpc": "2.0", "error": {}, "id": null}`, // empty object for error
`{"jsonrpc": "2.0", "error": null, "id": null}`, // numm error
`{"jsonrpc": "2.0", "error": {"message": "test err"}, "id": null}`, // Missing code
`{"jsonrpc": "2.0", "error": {"code": -32700}, "id": null}`, // missing message
`{"jsonrpc": "2.0", "error": true, "id": null}`, // error is wrong type

// these should break something
`{"jsonrpc": "2.0", "result": , "id": 1}`, // missing result ... broken json
`{"jsonrpc": "2.0", "result": "` + string([]byte{0}) + `", "id": 1}`, // null byte in result
`{"jsonrpc": "2.0", "result": nil, "id": 1}`, // nil instead of null
`{"jsonrpc": "2.0", "result": 123, "result": 456, "id": 1}`, // result specified twice? might work
``, // valid json but probably will break
`null`, // valid but should break
`0`, // valid but should break rp
`0x00`, // invalid I think
`<xml />`, // wrong type
`hi`, // invalid json
`"hi"`, // valid json but should break
string([]byte{0, 0, 0}), // null bytes should break
}

var junkContentTypeHeader = []string{
"application/javascript",
"application/octet-stream",
"application/xhtml+xml",
"application/json",
"application/xml",
"application/x-www-form-urlencoded",
"audio/x-wav",
"image/gif",
"image/png",
"multipart/mixed",
"multipart/form-data",
}
143 changes: 143 additions & 0 deletions cmd/testharness/l3-l4-conf.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/bin/bash

# https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#TRAVERSINGOFTABLES
# https://inai.de/projects/xtables-addons/
# https://tldp.org/HOWTO/Traffic-Control-HOWTO/

# apt install linux-headers-$(uname -r)
# apt install xtables-addons-dkms xtables-addons-common

# geth --dev --dev.period 2 --ws --ws.addr 0.0.0.0 --ws.port 8546 --http --http.addr 0.0.0.0 --http.port 8545 --http.api admin,debug,web3,eth,txpool,personal,miner,net --verbosity 5 --rpc.gascap 50000000 --rpc.txfeecap 0 --miner.gaslimit 10 --miner.gasprice 1 --gpo.blocks 1 --gpo.percentile 1 --gpo.maxprice 10 --gpo.ignoreprice 2 --dev.gaslimit 50000000
# polycli testharness --listen-ip 0.0.0.0


readonly http_port=8545
readonly ws_port=8546
readonly harness_port=11235

readonly interface=enp1s0


cleanup () {
tc qdisc del dev $interface root

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT

iptables -t filter -t raw -F
iptables -t raw -F
iptables -t nat -F
iptables -t mangle -F
}

main () {
# Allow port 22/ssh access
iptables -I INPUT -p tcp --dport 22 -j ACCEPT

# Setup root qdisc
tc qdisc add dev $interface root handle 1: htb

# Set default chain policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Accept on localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Allow established sessions to receive traffic
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow ICMP
iptables -A INPUT -p icmp -j ACCEPT

# Allow HTTP
iptables -I INPUT -p tcp --dport $http_port -j ACCEPT
iptables -I INPUT -p tcp --dport $ws_port -j ACCEPT
iptables -I INPUT -p tcp --dport $harness_port -j ACCEPT

# Allow 8000
iptables -t nat -A PREROUTING -p TCP --dport 8000 -j REDIRECT --to-port $http_port

# DROP 8001
iptables -t filter -A INPUT -p TCP --dport 8001 -j DROP

# REJECT 8002
iptables -t filter -A INPUT -p TCP --dport 8002 -j REJECT

# TARPIT 8003
iptables -t filter -A INPUT -p TCP --dport 8003 -j TARPIT

# DELUDE 8004
iptables -t filter -A INPUT -p TCP --dport 8004 -j DELUDE



# Allow 8101 and Drop 10% of the packets in the way in
iptables -t nat -A PREROUTING -p TCP --dport 8101 -j REDIRECT --to-port $http_port
iptables -t raw -I PREROUTING -p TCP --dport 8101 -m statistic --mode random --probability 0.1 -j DROP

# Allow 8102 and Drop 10% of the packets on the way out
iptables -t mangle -A PREROUTING -p TCP --dport 8102 -j CONNMARK --set-mark 8102
iptables -t nat -A PREROUTING -p TCP --dport 8102 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p TCP -m connmark --mark 8102 -m statistic --mode random --probability 0.1 -j DROP

# Allow 8103 and add a 1 second delay
iptables -t mangle -A PREROUTING -p TCP --dport 8103 -j CONNMARK --set-mark 8103
iptables -t nat -A PREROUTING -p TCP --dport 8103 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p tcp -m connmark --mark 8103 -j CLASSIFY --set-class 1:3
tc class add dev $interface parent 1: classid 1:3 htb rate 1000mbps
tc qdisc add dev $interface parent 1:3 handle 3: netem delay 1000ms
# tc filter add dev $interface parent 1:0 protocol ip u32 match ip sport 8083 FFFF flowid 1:2

# Allow 8104 and add a packet limit of 2
iptables -t mangle -A PREROUTING -p TCP --dport 8104 -j CONNMARK --set-mark 8104
iptables -t nat -A PREROUTING -p TCP --dport 8104 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p tcp -m connmark --mark 8104 -j CLASSIFY --set-class 1:4
tc class add dev $interface parent 1: classid 1:4 htb rate 1000mbps
tc qdisc add dev $interface parent 1:4 handle 4: netem limit 2

# Allow 8105 and lose a random 20% of packets
iptables -t mangle -A PREROUTING -p TCP --dport 8105 -j CONNMARK --set-mark 8105
iptables -t nat -A PREROUTING -p TCP --dport 8105 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p tcp -m connmark --mark 8105 -j CLASSIFY --set-class 1:5
tc class add dev $interface parent 1: classid 1:5 htb rate 1000mbps
tc qdisc add dev $interface parent 1:5 handle 5: netem loss random 20%

# Allow 8106 and corrupt a random 20% of packets
iptables -t mangle -A PREROUTING -p TCP --dport 8106 -j CONNMARK --set-mark 8106
iptables -t nat -A PREROUTING -p TCP --dport 8106 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p tcp -m connmark --mark 8106 -j CLASSIFY --set-class 1:6
tc class add dev $interface parent 1: classid 1:6 htb rate 1000mbps
tc qdisc add dev $interface parent 1:6 handle 6: netem corrupt 20%

# Allow 8107 and duplicate a random 20% of packets
iptables -t mangle -A PREROUTING -p TCP --dport 8107 -j CONNMARK --set-mark 8107
iptables -t nat -A PREROUTING -p TCP --dport 8107 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p tcp -m connmark --mark 8107 -j CLASSIFY --set-class 1:7
tc class add dev $interface parent 1: classid 1:7 htb rate 1000mbps
tc qdisc add dev $interface parent 1:7 handle 7: netem duplicate 20%

# Allow 8108 and reorder a random 50% of packets
iptables -t mangle -A PREROUTING -p TCP --dport 8108 -j CONNMARK --set-mark 8108
iptables -t nat -A PREROUTING -p TCP --dport 8108 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p tcp -m connmark --mark 8108 -j CLASSIFY --set-class 1:8
tc class add dev $interface parent 1: classid 1:8 htb rate 1000mbps
tc qdisc add dev $interface parent 1:8 handle 8: netem duplicate 50%

# Allow 8109 and use a very slow rate limit
iptables -t mangle -A PREROUTING -p TCP --dport 8109 -j CONNMARK --set-mark 8109
iptables -t nat -A PREROUTING -p TCP --dport 8109 -j REDIRECT --to-port $http_port
iptables -t mangle -A POSTROUTING -p tcp -m connmark --mark 8109 -j CLASSIFY --set-class 1:9
tc class add dev $interface parent 1: classid 1:9 htb rate 1000mbps
tc qdisc add dev $interface parent 1:9 handle 9: netem rate 56kbit


}

cleanup;
main;


117 changes: 117 additions & 0 deletions cmd/testharness/testharness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package testharness

import (
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"math/rand"
"net"
"net/http"
"os"
"strconv"
"strings"
)

var (
harnessPort *uint16
listenAddr *string
)

const (
packetSize = 2<<15 - 1
HarnessIdentifier = "Test Harness"
)

type (
Handler500 struct{}
Handler400 struct{}
HandlerHuge struct{}
HandlerJunk struct{}
)

var TestHarnessCmd = &cobra.Command{
Use: "testharness --mode [mode] --port [portnumber]",
Short: "Run a simple test harness on the given port",
Long: ``,
RunE: func(cmd *cobra.Command, args []string) error {
log.Info().Uint16("port", *harnessPort).Str("ip", *listenAddr).Msg("Starting server")
return startHarness()
},
PreRunE: func(cmd *cobra.Command, args []string) error {
parsedIp := net.ParseIP(*listenAddr)
if parsedIp == nil {
return fmt.Errorf("the ip %s could not be parsed", *listenAddr)
}
return nil
},
}

func startHarness() error {
http.Handle("/500", new(Handler500))
http.Handle("/400", new(Handler400))
http.Handle("/huge", new(HandlerHuge))
http.Handle("/junk", new(HandlerJunk))

return http.ListenAndServe(fmt.Sprintf("%s:%d", *listenAddr, *harnessPort), nil)
}

func init() {
zerolog.SetGlobalLevel(zerolog.TraceLevel)
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
harnessPort = TestHarnessCmd.PersistentFlags().Uint16("port", 11235, "If the mode is tcp level or higher this will set the port that the server listens on ")
listenAddr = TestHarnessCmd.PersistentFlags().String("listen-ip", "127.0.0.1", "The IP that we'll use to listen")
}

func (m Handler500) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug().Msg("handling request")
w.WriteHeader(500)
_, _ = w.Write([]byte(HarnessIdentifier))
}

func (m Handler400) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug().Msg("handling request")
w.WriteHeader(400)
_, _ = w.Write([]byte(HarnessIdentifier))
}

func (m HandlerJunk) ServeHTTP(w http.ResponseWriter, r *http.Request) {
idxS := r.URL.Query().Get("idx")
idx, err := strconv.Atoi(idxS)
if err != nil {
idx = rand.Intn(len(junkJSONRPC))
}

w.Header().Set("Content-Type", junkContentTypeHeader[rand.Intn(len(junkContentTypeHeader))])

log.Debug().Int("idx", idx).Msg("handling request")

junkResponse := junkJSONRPC[idx%len(junkJSONRPC)]
w.WriteHeader(200)
_, _ = w.Write([]byte(junkResponse))
}

func (m HandlerHuge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
b := r.URL.Query().Get("bytes")
bc, err := strconv.Atoi(b)
if err != nil {
bc = 1024 * 1024
}
hugeResponse := strings.Repeat("J", bc)
hugeResponseStr := `{"jsonrpc": "2.0", "result": "` + hugeResponse + `", "id": 1}`
log.Debug().Msg("handling request")
w.WriteHeader(200)
_, _ = w.Write([]byte(hugeResponseStr))
}

//A server that's listening on UDP rather tha TCP
//A server that responds with valid data but with a delay (e.g. a 2-second delay)
//A server that returns data with invalid or malformed JSON syntax
//A server that returns data in a different character encoding than expected (e.g. ISO-8859-1 instead of UTF-8)
//A server that responds with a different HTTP status code than expected (e.g. 301 instead of 200)
//A server that sends back a response that exceeds the content-length specified in the response header
//A server that sends back a response with missing headers
//A server that sends back a response with extra headers
//A server that requires an authentication header, but fails if it is not provided or if it is incorrect
//A server that requires a specific content type header, and fails if it is not provided or if it is incorrect
//A server that has a firewall that blocks certain IP addresses, causing the request to fail.