220 lines
6.1 KiB
Go
220 lines
6.1 KiB
Go
|
|
package forward
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"io"
|
|||
|
|
"net/http"
|
|||
|
|
"net/http/httptest"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func TestLogHTTP_writesThreeBlockLog(t *testing.T) {
|
|||
|
|
tmp := t.TempDir()
|
|||
|
|
setAPILogRootForTest(tmp)
|
|||
|
|
t.Cleanup(func() { setAPILogRootForTest("") })
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
reqBody = `{"hello":"world"}`
|
|||
|
|
respBody = `{"code":200,"msg":"ok"}`
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
mux := http.NewServeMux()
|
|||
|
|
mux.HandleFunc("/province/supervise/data", func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
if got := r.Header.Get("X-Test-Token"); got != "secret-value-12345" {
|
|||
|
|
t.Errorf("header=%q", got)
|
|||
|
|
}
|
|||
|
|
b, _ := io.ReadAll(r.Body)
|
|||
|
|
if string(b) != reqBody {
|
|||
|
|
t.Errorf("body=%q", b)
|
|||
|
|
}
|
|||
|
|
w.Header().Set("Content-Type", "application/json")
|
|||
|
|
w.WriteHeader(http.StatusOK)
|
|||
|
|
_, _ = w.Write([]byte(respBody))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
srv := httptest.NewServer(logHTTP(mux))
|
|||
|
|
defer srv.Close()
|
|||
|
|
|
|||
|
|
req, err := http.NewRequest(http.MethodPost, srv.URL+"/province/supervise/data", strings.NewReader(reqBody))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
req.Header.Set("X-Test-Token", "secret-value-12345")
|
|||
|
|
req.Header.Set("Content-Type", "application/json")
|
|||
|
|
|
|||
|
|
resp, err := http.DefaultClient.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
if resp.StatusCode != http.StatusOK {
|
|||
|
|
t.Fatalf("status=%d", resp.StatusCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
content := readOnlyAPILogFile(t, tmp)
|
|||
|
|
|
|||
|
|
checks := []string{
|
|||
|
|
"=== META ===",
|
|||
|
|
"Kind: supervise",
|
|||
|
|
"=== 1. 入站请求(transit → forward)===",
|
|||
|
|
"X-Test-Token: secret-value-12345",
|
|||
|
|
reqBody,
|
|||
|
|
"=== 2. 出站转发(forward → 政务云)===",
|
|||
|
|
"(未发起出站转发)",
|
|||
|
|
"=== 3. 回复(政务云 → forward → transit)===",
|
|||
|
|
respBody,
|
|||
|
|
`"code": 200`,
|
|||
|
|
"BodyJSON:",
|
|||
|
|
}
|
|||
|
|
for _, c := range checks {
|
|||
|
|
if !strings.Contains(content, c) {
|
|||
|
|
t.Errorf("log missing %q\n---\n%s", c, content)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestLogHTTP_forwardProxyThreeBlocks(t *testing.T) {
|
|||
|
|
tmp := t.TempDir()
|
|||
|
|
setAPILogRootForTest(tmp)
|
|||
|
|
t.Cleanup(func() { setAPILogRootForTest("") })
|
|||
|
|
|
|||
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
if r.URL.Path != "/mng/file/auth/upload" {
|
|||
|
|
t.Errorf("path=%s", r.URL.Path)
|
|||
|
|
}
|
|||
|
|
if r.Header.Get("X-Authorization") != "tok" {
|
|||
|
|
t.Errorf("X-Authorization=%q", r.Header.Get("X-Authorization"))
|
|||
|
|
}
|
|||
|
|
if r.Header.Get("X-Forward-Token") != "" {
|
|||
|
|
t.Error("X-Forward-Token must not reach upstream")
|
|||
|
|
}
|
|||
|
|
b, _ := io.ReadAll(r.Body)
|
|||
|
|
if string(b) != "pdf-bytes" {
|
|||
|
|
t.Errorf("body=%q", b)
|
|||
|
|
}
|
|||
|
|
w.Header().Set("Content-Type", "application/json")
|
|||
|
|
_, _ = w.Write([]byte(`{"success":true,"record":{"fileId":"abc123"}}`))
|
|||
|
|
}))
|
|||
|
|
defer upstream.Close()
|
|||
|
|
|
|||
|
|
fileTarget := strings.TrimRight(upstream.URL, "/") + "/mng/file/auth/upload"
|
|||
|
|
proxy, err := newReverseProxy(fileTarget, "file")
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mux := http.NewServeMux()
|
|||
|
|
mux.HandleFunc("/mng/file/auth/upload", func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
handleForward(w, r, "file", proxy, fileTarget, "", "", true)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
srv := httptest.NewServer(logHTTP(mux))
|
|||
|
|
defer srv.Close()
|
|||
|
|
|
|||
|
|
req, err := http.NewRequest(http.MethodPost, srv.URL+"/mng/file/auth/upload", strings.NewReader("pdf-bytes"))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary=abc")
|
|||
|
|
req.Header.Set("X-Authorization", "tok")
|
|||
|
|
req.Header.Set("X-Forward-Token", "should-strip")
|
|||
|
|
|
|||
|
|
resp, err := http.DefaultClient.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
if resp.StatusCode != http.StatusOK {
|
|||
|
|
t.Fatalf("status=%d", resp.StatusCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
content := readOnlyAPILogFile(t, tmp)
|
|||
|
|
checks := []string{
|
|||
|
|
"Kind: file",
|
|||
|
|
"=== 1. 入站请求(transit → forward)===",
|
|||
|
|
"X-Forward-Token: should-strip",
|
|||
|
|
"body_len=9",
|
|||
|
|
"=== 2. 出站转发(forward → 政务云)===",
|
|||
|
|
"TargetURL: " + fileTarget,
|
|||
|
|
"X-Authorization: tok",
|
|||
|
|
"=== 3. 回复(政务云 → forward → transit)===",
|
|||
|
|
"UpstreamStatus: 200",
|
|||
|
|
`"fileId": "abc123"`,
|
|||
|
|
"BodyJSON:",
|
|||
|
|
}
|
|||
|
|
for _, c := range checks {
|
|||
|
|
if !strings.Contains(content, c) {
|
|||
|
|
t.Errorf("log missing %q\n---\n%s", c, content)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if strings.Contains(content, "X-Forward-Token:") && strings.Contains(content, "=== 2. 出站转发") {
|
|||
|
|
// X-Forward-Token must not appear in outbound headers block
|
|||
|
|
outboundSection := content[strings.Index(content, "=== 2. 出站转发"):strings.Index(content, "=== 3. 回复")]
|
|||
|
|
if strings.Contains(outboundSection, "X-Forward-Token") {
|
|||
|
|
t.Errorf("outbound block leaked X-Forward-Token:\n%s", outboundSection)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestLogHTTP_forbiddenHasOutboundAndClientResponse(t *testing.T) {
|
|||
|
|
tmp := t.TempDir()
|
|||
|
|
setAPILogRootForTest(tmp)
|
|||
|
|
t.Cleanup(func() { setAPILogRootForTest("") })
|
|||
|
|
|
|||
|
|
mux := http.NewServeMux()
|
|||
|
|
mux.HandleFunc("/province/supervise/data", func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
handleForward(w, r, "supervise", nil, "http://example.com/up", "10.0.0.1", "secret", true)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
srv := httptest.NewServer(logHTTP(mux))
|
|||
|
|
defer srv.Close()
|
|||
|
|
|
|||
|
|
req, err := http.NewRequest(http.MethodPost, srv.URL+"/province/supervise/data", strings.NewReader(`{}`))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
req.Header.Set("Content-Type", "application/json")
|
|||
|
|
req.Header.Set("X-Forward-Token", "wrong")
|
|||
|
|
|
|||
|
|
resp, err := http.DefaultClient.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
if resp.StatusCode != http.StatusForbidden {
|
|||
|
|
t.Fatalf("status=%d", resp.StatusCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
content := readOnlyAPILogFile(t, tmp)
|
|||
|
|
checks := []string{
|
|||
|
|
"=== 2. 出站转发(forward → 政务云)===",
|
|||
|
|
"TargetURL: http://example.com/up",
|
|||
|
|
"=== 3. 回复(政务云 → forward → transit)===",
|
|||
|
|
"forbidden",
|
|||
|
|
}
|
|||
|
|
for _, c := range checks {
|
|||
|
|
if !strings.Contains(content, c) {
|
|||
|
|
t.Errorf("log missing %q\n---\n%s", c, content)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func readOnlyAPILogFile(t *testing.T, root string) string {
|
|||
|
|
t.Helper()
|
|||
|
|
hourDirs, err := os.ReadDir(root)
|
|||
|
|
if err != nil || len(hourDirs) == 0 {
|
|||
|
|
t.Fatalf("no hour dir under %s: %v", root, err)
|
|||
|
|
}
|
|||
|
|
files, err := os.ReadDir(filepath.Join(root, hourDirs[0].Name()))
|
|||
|
|
if err != nil || len(files) != 1 {
|
|||
|
|
t.Fatalf("want 1 log file, got %d err=%v", len(files), err)
|
|||
|
|
}
|
|||
|
|
raw, err := os.ReadFile(filepath.Join(root, hourDirs[0].Name(), files[0].Name()))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
return string(raw)
|
|||
|
|
}
|