diff options
author | Valery Piashchynski <[email protected]> | 2020-11-26 15:30:16 +0300 |
---|---|---|
committer | GitHub <[email protected]> | 2020-11-26 15:30:16 +0300 |
commit | ee68279e68ab854ae313fc84ea6a2a905133da87 (patch) | |
tree | f0731b1bbbc996347060e8c88d49616090877a65 /plugins/http/tests | |
parent | 0a48a027642a34c560717526c55f70b7260d678c (diff) | |
parent | 7ef7ce5859be3b30476167ee9a2d9d3b0092259a (diff) |
Merge pull request #400 from spiral/plugin/http
[RR2] New http plugin for the RR 2.0
Diffstat (limited to 'plugins/http/tests')
24 files changed, 4511 insertions, 0 deletions
diff --git a/plugins/http/tests/config_test.go b/plugins/http/tests/config_test.go new file mode 100644 index 00000000..068bd66e --- /dev/null +++ b/plugins/http/tests/config_test.go @@ -0,0 +1,330 @@ +package tests + +//import ( +// "os" +// "testing" +// "time" +// +// json "github.com/json-iterator/go" +// "github.com/stretchr/testify/assert" +//) +// +//type mockCfg struct{ cfg string } +// +//func (cfg *mockCfg) Get(name string) service.Config { return nil } +//func (cfg *mockCfg) Unmarshal(out interface{}) error { +// j := json.ConfigCompatibleWithStandardLibrary +// return j.Unmarshal([]byte(cfg.cfg), out) +//} +// +//func Test_Config_Hydrate_Error1(t *testing.T) { +// cfg := &mockCfg{`{"address": "localhost:8080"}`} +// c := &Config{} +// +// assert.NoError(t, c.Hydrate(cfg)) +//} +// +//func Test_Config_Hydrate_Error2(t *testing.T) { +// cfg := &mockCfg{`{"dir": "/dir/"`} +// c := &Config{} +// +// assert.Error(t, c.Hydrate(cfg)) +//} +// +//func Test_Config_Valid(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.NoError(t, cfg.Valid()) +//} +// +//func Test_Trusted_Subnets(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// TrustedSubnets: []string{"200.1.0.0/16"}, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.NoError(t, cfg.parseCIDRs()) +// +// assert.True(t, cfg.IsTrusted("200.1.0.10")) +// assert.False(t, cfg.IsTrusted("127.0.0.0.1")) +//} +// +//func Test_Trusted_Subnets_Err(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// TrustedSubnets: []string{"200.1.0.0"}, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.parseCIDRs()) +//} +// +//func Test_Config_Valid_SSL(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// SSL: SSLConfig{ +// Cert: "fixtures/server.crt", +// Key: "fixtures/server.key", +// }, +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.Hydrate(&testCfg{httpCfg: "{}"})) +// +// assert.NoError(t, cfg.Valid()) +// assert.True(t, cfg.EnableTLS()) +// assert.Equal(t, 443, cfg.SSL.Port) +//} +// +//func Test_Config_SSL_No_key(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// SSL: SSLConfig{ +// Cert: "fixtures/server.crt", +// }, +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} +// +//func Test_Config_SSL_No_Cert(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// SSL: SSLConfig{ +// Key: "fixtures/server.key", +// }, +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} +// +//func Test_Config_NoUploads(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} +// +//func Test_Config_NoHTTP2(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 0, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} +// +//func Test_Config_NoWorkers(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} +// +//func Test_Config_NoPool(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 0, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} +// +//func Test_Config_DeadPool(t *testing.T) { +// cfg := &Config{ +// Address: ":8080", +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} +// +//func Test_Config_InvalidAddress(t *testing.T) { +// cfg := &Config{ +// Address: "unexpected_address", +// MaxRequestSize: 1024, +// Uploads: &UploadsConfig{ +// Dir: os.TempDir(), +// Forbid: []string{".go"}, +// }, +// HTTP2: &HTTP2Config{ +// Enabled: true, +// }, +// Workers: &roadrunner.ServerConfig{ +// Command: "php tests/client.php echo pipes", +// Relay: "pipes", +// Pool: &roadrunner.Config{ +// NumWorkers: 1, +// AllocateTimeout: time.Second, +// DestroyTimeout: time.Second, +// }, +// }, +// } +// +// assert.Error(t, cfg.Valid()) +//} diff --git a/plugins/http/tests/configs/.rr-broken-pipes.yaml b/plugins/http/tests/configs/.rr-broken-pipes.yaml new file mode 100644 index 00000000..aacc303e --- /dev/null +++ b/plugins/http/tests/configs/.rr-broken-pipes.yaml @@ -0,0 +1,29 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php broken pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:12384 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + + diff --git a/plugins/http/tests/configs/.rr-echoErr.yaml b/plugins/http/tests/configs/.rr-echoErr.yaml new file mode 100644 index 00000000..696fc0ae --- /dev/null +++ b/plugins/http/tests/configs/.rr-echoErr.yaml @@ -0,0 +1,28 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echoerr pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:8080 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ "" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/http/tests/configs/.rr-env.yaml b/plugins/http/tests/configs/.rr-env.yaml new file mode 100644 index 00000000..c9fdc798 --- /dev/null +++ b/plugins/http/tests/configs/.rr-env.yaml @@ -0,0 +1,31 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php env pipes" + user: "" + group: "" + env: + "env_key": "ENV_VALUE" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:12084 + maxRequestSize: 1024 + middleware: [ "" ] + env: + "RR_HTTP": "true" + "env_key": "ENV_VALUE" + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml b/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml new file mode 100644 index 00000000..dbd19445 --- /dev/null +++ b/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php request-uri pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8082 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8890 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://127.0.0.1:6921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-fcgi.yaml b/plugins/http/tests/configs/.rr-fcgi.yaml new file mode 100644 index 00000000..0cbd6d02 --- /dev/null +++ b/plugins/http/tests/configs/.rr-fcgi.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8081 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8889 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-h2c.yaml b/plugins/http/tests/configs/.rr-h2c.yaml new file mode 100644 index 00000000..316daea9 --- /dev/null +++ b/plugins/http/tests/configs/.rr-h2c.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8083 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8891 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: true + h2c: true + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-http.yaml b/plugins/http/tests/configs/.rr-http.yaml new file mode 100644 index 00000000..c907c5e7 --- /dev/null +++ b/plugins/http/tests/configs/.rr-http.yaml @@ -0,0 +1,41 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18903 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8892 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:7921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 + + diff --git a/plugins/http/tests/configs/.rr-init.yaml b/plugins/http/tests/configs/.rr-init.yaml new file mode 100644 index 00000000..50aa91ec --- /dev/null +++ b/plugins/http/tests/configs/.rr-init.yaml @@ -0,0 +1,41 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:15395 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8892 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:7921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 + + diff --git a/plugins/http/tests/configs/.rr-resetter.yaml b/plugins/http/tests/configs/.rr-resetter.yaml new file mode 100644 index 00000000..b46b21f5 --- /dev/null +++ b/plugins/http/tests/configs/.rr-resetter.yaml @@ -0,0 +1,28 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:10084 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/http/tests/configs/.rr-ssl-push.yaml b/plugins/http/tests/configs/.rr-ssl-push.yaml new file mode 100644 index 00000000..90a99192 --- /dev/null +++ b/plugins/http/tests/configs/.rr-ssl-push.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php push pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8086 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8894 + redirect: true + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-ssl-redirect.yaml b/plugins/http/tests/configs/.rr-ssl-redirect.yaml new file mode 100644 index 00000000..1878ba53 --- /dev/null +++ b/plugins/http/tests/configs/.rr-ssl-redirect.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8087 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8895 + redirect: true + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-ssl.yaml b/plugins/http/tests/configs/.rr-ssl.yaml new file mode 100644 index 00000000..127c1678 --- /dev/null +++ b/plugins/http/tests/configs/.rr-ssl.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8085 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8893 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/fixtures/server.crt b/plugins/http/tests/fixtures/server.crt new file mode 100644 index 00000000..24d67fd7 --- /dev/null +++ b/plugins/http/tests/fixtures/server.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICTTCCAdOgAwIBAgIJAOKyUd+llTRKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYT +AlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv +MRMwEQYDVQQKDApSb2FkUnVubmVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgw +OTMwMTMzNDUzWhcNMjgwOTI3MTMzNDUzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwK +Um9hZFJ1bm5lcjESMBAGA1UEAwwJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EE +ACIDYgAEVnbShsM+l5RR3wfWWmGhzuFGwNzKCk7i9xyobDIyBUxG/UUSfj7KKlUX +puDnDEtF5xXcepl744CyIAYFLOXHb5WqI4jCOzG0o9f/00QQ4bQudJOdbqV910QF +C2vb7Fxro1MwUTAdBgNVHQ4EFgQU9xUexnbB6ORKayA7Pfjzs33otsAwHwYDVR0j +BBgwFoAU9xUexnbB6ORKayA7Pfjzs33otsAwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAgNoADBlAjEAue3HhR/MUhxoa9tSDBtOJT3FYbDQswrsdqBTz97CGKst +e7XeZ3HMEvEXy0hGGEMhAjAqcD/4k9vViVppgWFtkk6+NFbm+Kw/QeeAiH5FgFSj +8xQcb+b7nPwNLp3JOkXkVd4= +-----END CERTIFICATE----- diff --git a/plugins/http/tests/fixtures/server.key b/plugins/http/tests/fixtures/server.key new file mode 100644 index 00000000..7501dd46 --- /dev/null +++ b/plugins/http/tests/fixtures/server.key @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCQP8utxNbHR6xZOLAJgUhn88r6IrPqmN0MsgGJM/jePB+T9UhkmIU8 +PMm2HeScbcugBwYFK4EEACKhZANiAARWdtKGwz6XlFHfB9ZaYaHO4UbA3MoKTuL3 +HKhsMjIFTEb9RRJ+PsoqVRem4OcMS0XnFdx6mXvjgLIgBgUs5cdvlaojiMI7MbSj +1//TRBDhtC50k51upX3XRAULa9vsXGs= +-----END EC PRIVATE KEY----- diff --git a/plugins/http/tests/handler_test.go b/plugins/http/tests/handler_test.go new file mode 100644 index 00000000..b898c265 --- /dev/null +++ b/plugins/http/tests/handler_test.go @@ -0,0 +1,1852 @@ +package tests + +import ( + "bytes" + "context" + "io/ioutil" + "mime/multipart" + "net/url" + "os/exec" + "runtime" + "strings" + + "github.com/spiral/roadrunner/v2" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" + + "net/http" + "os" + "testing" + "time" +) + +func TestHandler_Echo(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echo", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + go func(server *http.Server) { + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }(hs) + time.Sleep(time.Millisecond * 10) + + body, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", body) +} + +func Test_HandlerErrors(t *testing.T) { + _, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, nil) + assert.Error(t, err) +} + +func TestHandler_Headers(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "header", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8078", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 100) + + req, err := http.NewRequest("GET", "http://localhost:8078?hello=world", nil) + assert.NoError(t, err) + + req.Header.Add("input", "sample") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "world", r.Header.Get("Header")) + assert.Equal(t, "SAMPLE", string(b)) +} + +func TestHandler_Empty_User_Agent(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "user-agent", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8088", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) + assert.NoError(t, err) + + req.Header.Add("user-agent", "") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "", string(b)) +} + +func TestHandler_User_Agent(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "user-agent", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8088", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) + assert.NoError(t, err) + + req.Header.Add("User-Agent", "go-agent") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "go-agent", string(b)) +} + +func TestHandler_Cookies(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "cookie", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8079", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("GET", "http://localhost:8079", nil) + assert.NoError(t, err) + + req.AddCookie(&http.Cookie{Name: "input", Value: "input-value"}) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "INPUT-VALUE", string(b)) + + for _, c := range r.Cookies() { + assert.Equal(t, "output", c.Name) + assert.Equal(t, "cookie-output", c.Value) + } +} + +func TestHandler_JsonPayload_POST(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "payload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8090", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest( + "POST", + "http://localhost"+hs.Addr, + bytes.NewBufferString(`{"key":"value"}`), + ) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/json") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, `{"value":"key"}`, string(b)) +} + +func TestHandler_JsonPayload_PUT(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "payload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8081", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/json") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, `{"value":"key"}`, string(b)) +} + +func TestHandler_JsonPayload_PATCH(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "payload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8082", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/json") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, `{"value":"key"}`, string(b)) +} + +func TestHandler_FormData_POST(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8083", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + // Sorted + assert.Equal(t, "{\"arr\":{\"c\":{\"p\":\"l\",\"z\":\"\"},\"x\":{\"y\":{\"e\":\"f\",\"z\":\"y\"}}},\"key\":\"value\",\"name\":[\"name1\",\"name2\",\"name3\"]}", string(b)) +} + +func TestHandler_FormData_POST_Overwrite(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8083", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + form := url.Values{} + + form.Add("key", "value1") + form.Add("key", "value2") + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"key":"value2","arr":{"x":{"y":null}}}`, string(b)) +} + +func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8083", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_FormData_PUT(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":17834", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_FormData_PATCH(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8085", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, "{\"arr\":{\"c\":{\"p\":\"l\",\"z\":\"\"},\"x\":{\"y\":{\"e\":\"f\",\"z\":\"y\"}}},\"key\":\"value\",\"name\":[\"name1\",\"name2\",\"name3\"]}", string(b)) +} + +func TestHandler_Multipart_POST(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8019", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name1") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name2") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name3") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][z]", "y") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][e]", "f") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]p", "l") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]z", "") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the writer: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, "{\"arr\":{\"c\":{\"p\":\"l\",\"z\":\"\"},\"x\":{\"y\":{\"e\":\"f\",\"z\":\"y\"}}},\"key\":\"value\",\"name\":[\"name1\",\"name2\",\"name3\"]}", string(b)) +} + +func TestHandler_Multipart_PUT(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8020", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name1") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name2") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name3") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][z]", "y") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][e]", "f") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]p", "l") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]z", "") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the writer: error %v", err) + } + + req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_Multipart_PATCH(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name1") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name2") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name3") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][z]", "y") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][e]", "f") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]p", "l") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]z", "") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the writer: error %v", err) + } + + req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_Error(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "error", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + _, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_Error2(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "error2", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + _, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_Error3(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "pid", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err = hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + b2 := &bytes.Buffer{} + for i := 0; i < 1024*1024; i++ { + b2.Write([]byte(" ")) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, b2) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err = r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + assert.NoError(t, err) + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_ResponseDuration(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echo", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + gotresp := make(chan interface{}) + h.AddListener(func(event interface{}) { + switch t := event.(type) { + case httpPlugin.ResponseEvent: + if t.Elapsed() > 0 { + close(gotresp) + } + default: + } + }) + + body, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + + <-gotresp + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", body) +} + +func TestHandler_ResponseDurationDelayed(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echoDelay", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + gotresp := make(chan interface{}) + h.AddListener(func(event interface{}) { + switch tp := event.(type) { + case httpPlugin.ResponseEvent: + if tp.Elapsed() > time.Second { + close(gotresp) + } + default: + } + }) + + body, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + <-gotresp + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", body) +} + +func TestHandler_ErrorDuration(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "error", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err = hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + goterr := make(chan interface{}) + h.AddListener(func(event interface{}) { + switch tp := event.(type) { + case httpPlugin.ErrorEvent: + if tp.Elapsed() > 0 { + close(goterr) + } + default: + } + }) + + _, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + + <-goterr + + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_IP(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := get("http://127.0.0.1:8177/") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "127.0.0.1", body) +} + +func TestHandler_XRealIP(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8179", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := getHeader("http://127.0.0.1:8179/", map[string]string{ + "X-Real-Ip": "200.0.0.1", + }) + + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "200.0.0.1", body) +} + +func TestHandler_XForwardedFor(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "100.0.0.0/16", + "200.0.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ + "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", + }) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "101.0.0.1", body) + _ = r.Body.Close() + + body, r, err = getHeader("http://127.0.0.1:8177/", map[string]string{ + "X-Forwarded-For": "100.0.0.1, 200.0.0.1, 101.0.0.1, invalid", + }) + + assert.NoError(t, err) + _ = r.Body.Close() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "101.0.0.1", body) +} + +func TestHandler_XForwardedFor_NotTrustedRemoteIp(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ + "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", + }) + + assert.NoError(t, err) + _ = r.Body.Close() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "127.0.0.1", body) +} + +func BenchmarkHandler_Listen_Echo(b *testing.B) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echo", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: int64(runtime.NumCPU()), + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + b.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(b, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + b.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err = hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + b.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + b.ResetTimer() + b.ReportAllocs() + bb := "WORLD" + for n := 0; n < b.N; n++ { + r, err := http.Get("http://localhost:8177/?hello=world") + if err != nil { + b.Fail() + } + // Response might be nil here + if r != nil { + br, err := ioutil.ReadAll(r.Body) + if err != nil { + b.Errorf("error reading Body: error %v", err) + } + if string(br) != bb { + b.Fail() + } + err = r.Body.Close() + if err != nil { + b.Errorf("error closing the Body: error %v", err) + } + } else { + b.Errorf("got nil response") + } + } +} diff --git a/plugins/http/tests/http_test.go b/plugins/http/tests/http_test.go new file mode 100644 index 00000000..451566ca --- /dev/null +++ b/plugins/http/tests/http_test.go @@ -0,0 +1,1114 @@ +package tests + +import ( + "bytes" + "crypto/tls" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/goridge/v2" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/mocks" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/yookoala/gofast" + + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +var sslClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + +func TestHTTPInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + wg.Wait() +} + +func TestHTTPInformerReset(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-resetter.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &informer.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 10) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("HTTPInformerTest", informerTest) + t.Run("HTTPEchoTestBefore", echoHTTP) + t.Run("HTTPResetTest", resetTest) + t.Run("HTTPEchoTestAfter", echoHTTP) + + wg.Wait() +} + +func echoHTTP(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:10084?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func resetTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + // WorkerList contains list of workers. + + var ret bool + err = client.Call("resetter.Reset", "http", &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + var services []string + err = client.Call("resetter.List", nil, &services) + assert.NoError(t, err) + if services[0] != "http" { + t.Fatal("no enough services") + } +} + +func informerTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + // WorkerList contains list of workers. + list := struct { + // Workers is list of workers. + Workers []roadrunner.ProcessState `json:"workers"` + }{} + + err = client.Call("informer.Workers", "http", &list) + assert.NoError(t, err) + assert.Len(t, list.Workers, 2) +} + +func TestSSL(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("SSLEcho", sslEcho) + t.Run("SSLNoRedirect", sslNoRedirect) + t.Run("fCGIecho", fcgiEcho) + wg.Wait() +} + +func sslNoRedirect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8085?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + + assert.Nil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func sslEcho(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:8893?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func fcgiEcho(t *testing.T) { + fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:6920") + + fcgiHandler := gofast.NewHandler( + gofast.BasicParamsMap(gofast.BasicSession), + gofast.SimpleClientFactory(fcgiConnFactory, 0), + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "http://site.local/?hello=world", nil) + fcgiHandler.ServeHTTP(w, req) + + body, err := ioutil.ReadAll(w.Result().Body) + + assert.NoError(t, err) + assert.Equal(t, 201, w.Result().StatusCode) + assert.Equal(t, "WORLD", string(body)) +} + +func TestSSLRedirect(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl-redirect.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("SSLRedirect", sslRedirect) + wg.Wait() +} + +func sslRedirect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8087?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func TestSSLPushPipes(t *testing.T) { + time.Sleep(time.Second) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl-push.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("SSLPush", sslPush) + wg.Wait() +} + +func sslPush(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:8894?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + + assert.NotNil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, "", r.Header.Get("Http2-Push")) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func TestFastCGI_RequestUri(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-fcgi-reqUri.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + t.Run("FastCGIServiceRequestUri", fcgiReqURI) + + go func() { + tt := time.NewTimer(time.Second * 10) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + wg.Wait() +} + +func fcgiReqURI(t *testing.T) { + time.Sleep(time.Second * 2) + fcgiConnFactory := gofast.SimpleConnFactory("tcp", "127.0.0.1:6921") + + fcgiHandler := gofast.NewHandler( + gofast.BasicParamsMap(gofast.BasicSession), + gofast.SimpleClientFactory(fcgiConnFactory, 0), + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "http://site.local/hello-world", nil) + fcgiHandler.ServeHTTP(w, req) + + body, err := ioutil.ReadAll(w.Result().Body) + assert.NoError(t, err) + assert.Equal(t, 200, w.Result().StatusCode) + assert.Equal(t, "http://site.local/hello-world", string(body)) +} + +func TestH2CUpgrade(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-h2c.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("H2cUpgrade", h2cUpgrade) + wg.Wait() +} + +func h2cUpgrade(t *testing.T) { + req, err := http.NewRequest("PRI", "http://localhost:8083?hello=world", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Add("Upgrade", "h2c") + req.Header.Add("Connection", "HTTP2-Settings") + req.Header.Add("HTTP2-Settings", "") + + r, err2 := http.DefaultClient.Do(req) + if err2 != nil { + t.Fatal(err) + } + + assert.Equal(t, "101 Switching Protocols", r.Status) + + err3 := r.Body.Close() + if err3 != nil { + t.Fatal(err) + } +} + +func TestH2C(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-h2c.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("H2c", h2c) + wg.Wait() +} + +func h2c(t *testing.T) { + req, err := http.NewRequest("PRI", "http://localhost:8083?hello=world", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Add("Connection", "HTTP2-Settings") + req.Header.Add("HTTP2-Settings", "") + + r, err2 := http.DefaultClient.Do(req) + if err2 != nil { + t.Fatal(err) + } + + assert.Equal(t, "201 Created", r.Status) + + err3 := r.Body.Close() + if err3 != nil { + t.Fatal(err) + } +} + +func TestHttpMiddleware(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 10) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("MiddlewareTest", middleware) + wg.Wait() +} + +func middleware(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:18903?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) + + req, err = http.NewRequest("GET", "http://localhost:18903/halt", nil) + assert.NoError(t, err) + + r, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 500, r.StatusCode) + assert.Equal(t, "halted", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpEchoErr(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-echoErr.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1") + mockLogger.EXPECT().Debug("WORLD", "pid", gomock.Any()) + mockLogger.EXPECT().Info("worker event received", "event", roadrunner.EventWorkerLog, "worker state", gomock.Any()) + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("HttpEchoError", echoError) + wg.Wait() +} + +func echoError(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8080?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpEnvVariables(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-env.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("EnvVariablesTest", envVarsTest) + wg.Wait() +} + +func envVarsTest(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:12084", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "ENV_VALUE", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpBrokenPipes(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel), endure.Visualize(endure.StdOut, "")) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-broken-pipes.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.Error(t, err) + + _, err = cont.Serve() + assert.Error(t, err) +} + + +func get(url string) (string, *http.Response, error) { + r, err := http.Get(url) + if err != nil { + return "", nil, err + } + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", nil, err + } + defer func() { + _ = r.Body.Close() + }() + return string(b), r, err +} + +// get request and return body +func getHeader(url string, h map[string]string) (string, *http.Response, error) { + req, err := http.NewRequest("GET", url, bytes.NewBuffer(nil)) + if err != nil { + return "", nil, err + } + + for k, v := range h { + req.Header.Set(k, v) + } + + r, err := http.DefaultClient.Do(req) + if err != nil { + return "", nil, err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", nil, err + } + + err = r.Body.Close() + if err != nil { + return "", nil, err + } + return string(b), r, err +} diff --git a/plugins/http/tests/parse_test.go b/plugins/http/tests/parse_test.go new file mode 100644 index 00000000..a93bc059 --- /dev/null +++ b/plugins/http/tests/parse_test.go @@ -0,0 +1,54 @@ +package tests + +import ( + "testing" + + "github.com/spiral/roadrunner/v2/plugins/http" +) + +var samples = []struct { + in string + out []string +}{ + {"key", []string{"key"}}, + {"key[subkey]", []string{"key", "subkey"}}, + {"key[subkey]value", []string{"key", "subkey", "value"}}, + {"key[subkey][value]", []string{"key", "subkey", "value"}}, + {"key[subkey][value][]", []string{"key", "subkey", "value", ""}}, + {"key[subkey] [value][]", []string{"key", "subkey", "value", ""}}, + {"key [ subkey ] [ value ] [ ]", []string{"key", "subkey", "value", ""}}, +} + +func Test_FetchIndexes(t *testing.T) { + for i := 0; i < len(samples); i++ { + r := http.FetchIndexes(samples[i].in) + if !same(r, samples[i].out) { + t.Errorf("got %q, want %q", r, samples[i].out) + } + } +} + +func BenchmarkConfig_FetchIndexes(b *testing.B) { + for _, tt := range samples { + for n := 0; n < b.N; n++ { + r := http.FetchIndexes(tt.in) + if !same(r, tt.out) { + b.Fail() + } + } + } +} + +func same(in, out []string) bool { + if len(in) != len(out) { + return false + } + + for i, v := range in { + if v != out[i] { + return false + } + } + + return true +} diff --git a/plugins/http/tests/plugin1.go b/plugins/http/tests/plugin1.go new file mode 100644 index 00000000..1cbca744 --- /dev/null +++ b/plugins/http/tests/plugin1.go @@ -0,0 +1,25 @@ +package tests + +import "github.com/spiral/roadrunner/v2/plugins/config" + +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "http_test.plugin1" +} diff --git a/plugins/http/tests/plugin_middleware.go b/plugins/http/tests/plugin_middleware.go new file mode 100644 index 00000000..de829d34 --- /dev/null +++ b/plugins/http/tests/plugin_middleware.go @@ -0,0 +1,61 @@ +package tests + +import ( + "net/http" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type PluginMiddleware struct { + config config.Configurer +} + +func (p *PluginMiddleware) Init(cfg config.Configurer) error { + p.config = cfg + return nil +} + +func (p *PluginMiddleware) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/halt" { + w.WriteHeader(500) + _, err := w.Write([]byte("halted")) + if err != nil { + panic("error writing the data to the http reply") + } + } else { + next.ServeHTTP(w, r) + } + } +} + +func (p *PluginMiddleware) Name() string { + return "pluginMiddleware" +} + +type PluginMiddleware2 struct { + config config.Configurer +} + +func (p *PluginMiddleware2) Init(cfg config.Configurer) error { + p.config = cfg + return nil +} + +func (p *PluginMiddleware2) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/boom" { + w.WriteHeader(555) + _, err := w.Write([]byte("boom")) + if err != nil { + panic("error writing the data to the http reply") + } + } else { + next.ServeHTTP(w, r) + } + } +} + +func (p *PluginMiddleware2) Name() string { + return "pluginMiddleware2" +} diff --git a/plugins/http/tests/psr-worker.php b/plugins/http/tests/psr-worker.php new file mode 100644 index 00000000..65fc6bde --- /dev/null +++ b/plugins/http/tests/psr-worker.php @@ -0,0 +1,23 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ +use Spiral\Goridge; +use Spiral\RoadRunner; + +ini_set('display_errors', 'stderr'); +require dirname(__DIR__) . "/../../vendor_php/autoload.php"; + +$worker = new RoadRunner\Worker(new Goridge\StreamRelay(STDIN, STDOUT)); +$psr7 = new RoadRunner\PSR7Client($worker); + +while ($req = $psr7->acceptRequest()) { + try { + $resp = new \Zend\Diactoros\Response(); + $resp->getBody()->write("hello world"); + + $psr7->respond($resp); + } catch (\Throwable $e) { + $psr7->getWorker()->error((string)$e); + } +}
\ No newline at end of file diff --git a/plugins/http/tests/response_test.go b/plugins/http/tests/response_test.go new file mode 100644 index 00000000..2bfe7d56 --- /dev/null +++ b/plugins/http/tests/response_test.go @@ -0,0 +1,163 @@ +package tests + +import ( + "bytes" + "errors" + "net/http" + "testing" + + "github.com/spiral/roadrunner/v2" + http2 "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" +) + +type testWriter struct { + h http.Header + buf bytes.Buffer + wroteHeader bool + code int + err error + pushErr error + pushes []string +} + +func (tw *testWriter) Header() http.Header { return tw.h } + +func (tw *testWriter) Write(p []byte) (int, error) { + if !tw.wroteHeader { + tw.WriteHeader(http.StatusOK) + } + + n, e := tw.buf.Write(p) + if e == nil { + e = tw.err + } + + return n, e +} + +func (tw *testWriter) WriteHeader(code int) { tw.wroteHeader = true; tw.code = code } + +func (tw *testWriter) Push(target string, opts *http.PushOptions) error { + tw.pushes = append(tw.pushes, target) + + return tw.pushErr +} + +func TestNewResponse_Error(t *testing.T) { + r, err := http2.NewResponse(roadrunner.Payload{Context: []byte(`invalid payload`)}) + assert.Error(t, err) + assert.Nil(t, r) +} + +func TestNewResponse_Write(t *testing.T) { + r, err := http2.NewResponse(roadrunner.Payload{ + Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), + Body: []byte(`sample body`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Equal(t, 301, w.code) + assert.Equal(t, "value", w.h.Get("key")) + assert.Equal(t, "sample body", w.buf.String()) +} + +func TestNewResponse_Stream(t *testing.T) { + r, err := http2.NewResponse(roadrunner.Payload{ + Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), + }) + + // r is pointer, so, it might be nil + if r == nil { + t.Fatal("response is nil") + } + + r.Body = &bytes.Buffer{} + r.Body.(*bytes.Buffer).WriteString("hello world") + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Equal(t, 301, w.code) + assert.Equal(t, "value", w.h.Get("key")) + assert.Equal(t, "hello world", w.buf.String()) +} + +func TestNewResponse_StreamError(t *testing.T) { + r, err := http2.NewResponse(roadrunner.Payload{ + Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), + }) + + // r is pointer, so, it might be nil + if r == nil { + t.Fatal("response is nil") + } + + r.Body = &bytes.Buffer{} + r.Body.(*bytes.Buffer).WriteString("hello world") + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string)), err: errors.New("error")} + assert.Error(t, r.Write(w)) +} + +func TestWrite_HandlesPush(t *testing.T) { + r, err := http2.NewResponse(roadrunner.Payload{ + Context: []byte(`{"headers":{"Http2-Push":["/test.js"],"content-type":["text/html"]},"status": 200}`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Nil(t, w.h["Http2-Push"]) + assert.Equal(t, []string{"/test.js"}, w.pushes) +} + +func TestWrite_HandlesTrailers(t *testing.T) { + r, err := http2.NewResponse(roadrunner.Payload{ + Context: []byte(`{"headers":{"Trailer":["foo, bar", "baz"],"foo":["test"],"bar":["demo"]},"status": 200}`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Nil(t, w.h[http2.TrailerHeaderKey]) + assert.Nil(t, w.h["foo"]) //nolint:golint,staticcheck + assert.Nil(t, w.h["baz"]) //nolint:golint,staticcheck + + assert.Equal(t, "test", w.h.Get("Trailer:foo")) + assert.Equal(t, "demo", w.h.Get("Trailer:bar")) +} + +func TestWrite_HandlesHandlesWhitespacesInTrailer(t *testing.T) { + r, err := http2.NewResponse(roadrunner.Payload{ + Context: []byte( + `{"headers":{"Trailer":["foo\t,bar , baz"],"foo":["a"],"bar":["b"],"baz":["c"]},"status": 200}`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Equal(t, "a", w.h.Get("Trailer:foo")) + assert.Equal(t, "b", w.h.Get("Trailer:bar")) + assert.Equal(t, "c", w.h.Get("Trailer:baz")) +} diff --git a/plugins/http/tests/uploads_config_test.go b/plugins/http/tests/uploads_config_test.go new file mode 100644 index 00000000..497cd54f --- /dev/null +++ b/plugins/http/tests/uploads_config_test.go @@ -0,0 +1,26 @@ +package tests + +import ( + "os" + "testing" + + "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" +) + +func TestFsConfig_Forbids(t *testing.T) { + cfg := http.UploadsConfig{Forbid: []string{".php"}} + + assert.True(t, cfg.Forbids("index.php")) + assert.True(t, cfg.Forbids("index.PHP")) + assert.True(t, cfg.Forbids("phpadmin/index.bak.php")) + assert.False(t, cfg.Forbids("index.html")) +} + +func TestFsConfig_TmpFallback(t *testing.T) { + cfg := http.UploadsConfig{Dir: "test"} + assert.Equal(t, "test", cfg.TmpDir()) + + cfg = http.UploadsConfig{Dir: ""} + assert.Equal(t, os.TempDir(), cfg.TmpDir()) +} diff --git a/plugins/http/tests/uploads_test.go b/plugins/http/tests/uploads_test.go new file mode 100644 index 00000000..ee244c06 --- /dev/null +++ b/plugins/http/tests/uploads_test.go @@ -0,0 +1,431 @@ +package tests + +import ( + "bytes" + "context" + "crypto/sha512" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "os/exec" + "testing" + "time" + + j "github.com/json-iterator/go" + "github.com/spiral/roadrunner/v2" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" +) + +var json = j.ConfigCompatibleWithStandardLibrary + +const testFile = "uploads_test.go" + +func TestHandler_Upload_File(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 0, "application/octet-stream") + + assert.Equal(t, `{"upload":`+fs+`}`, string(b)) +} + +func TestHandler_Upload_NestedFile(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload[x][y][z][]", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 0, "application/octet-stream") + + assert.Equal(t, `{"upload":{"x":{"y":{"z":[`+fs+`]}}}}`, string(b)) +} + +func TestHandler_Upload_File_NoTmpDir(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: "-------", + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 5, "application/octet-stream") + + assert.Equal(t, `{"upload":`+fs+`}`, string(b)) +} + +func TestHandler_Upload_File_Forbids(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{".go"}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 7, "application/octet-stream") + + assert.Equal(t, `{"upload":`+fs+`}`, string(b)) +} + +func Test_FileExists(t *testing.T) { + assert.True(t, exists(testFile)) + assert.False(t, exists("uploads_test.")) +} + +func mustOpen(f string) *os.File { + r, err := os.Open(f) + if err != nil { + panic(err) + } + return r +} + +type fInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mime string `json:"mime"` + Error int `json:"error"` + Sha512 string `json:"sha512,omitempty"` +} + +func fileString(f string, errNo int, mime string) string { + s, err := os.Stat(f) + if err != nil { + fmt.Println(fmt.Errorf("error stat the file, error: %v", err)) + } + + ff, err := os.Open(f) + if err != nil { + fmt.Println(fmt.Errorf("error opening the file, error: %v", err)) + } + + defer func() { + er := ff.Close() + if er != nil { + fmt.Println(fmt.Errorf("error closing the file, error: %v", er)) + } + }() + + h := sha512.New() + _, err = io.Copy(h, ff) + if err != nil { + fmt.Println(fmt.Errorf("error copying the file, error: %v", err)) + } + + v := &fInfo{ + Name: s.Name(), + Size: s.Size(), + Error: errNo, + Mime: mime, + Sha512: hex.EncodeToString(h.Sum(nil)), + } + + if errNo != 0 { + v.Sha512 = "" + v.Size = 0 + } + + r, err := json.Marshal(v) + if err != nil { + fmt.Println(fmt.Errorf("error marshalling fInfo, error: %v", err)) + } + return string(r) +} + +// exists if file exists. +func exists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} |