http: return more than data from server app

Server app could return headers in front of body/data buffer.
Offers apis for building and serialization of headers section.
HTTP layer now only add Date, Server and Content-Lengths headers,
rest is up to app. Well known header names are predefined.

Type: improvement

Change-Id: If778bdfc9acf6b0d11a48f0a745a3a56c96c2436
Signed-off-by: Matus Fabian <matfabia@cisco.com>
diff --git a/extras/hs-test/http_test.go b/extras/hs-test/http_test.go
index a5694bf..e20efd6 100644
--- a/extras/hs-test/http_test.go
+++ b/extras/hs-test/http_test.go
@@ -9,7 +9,6 @@
 	"time"
 
 	. "fd.io/hs-test/infra"
-	. "github.com/onsi/ginkgo/v2"
 )
 
 func init() {
@@ -21,7 +20,7 @@
 		HttpContentLengthTest, HttpStaticBuildInUrlGetIfListTest, HttpStaticBuildInUrlGetVersionTest,
 		HttpStaticMacTimeTest, HttpStaticBuildInUrlGetVersionVerboseTest, HttpVersionNotSupportedTest,
 		HttpInvalidContentLengthTest, HttpInvalidTargetSyntaxTest, HttpStaticPathTraversalTest, HttpUriDecodeTest,
-		HttpHeadersTest)
+		HttpHeadersTest, HttpStaticFileHandler)
 	RegisterNoTopoSoloTests(HttpStaticPromTest, HttpTpsTest, HttpTpsInterruptModeTest)
 }
 
@@ -89,19 +88,81 @@
 }
 
 func HttpStaticPromTest(s *NoTopoSuite) {
-	finished := make(chan error, 1)
 	query := "stats.prom"
 	vpp := s.GetContainerByName("vpp").VppInstance
 	serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString()
 	s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers"))
 	s.Log(vpp.Vppctl("prom enable"))
 	time.Sleep(time.Second * 5)
-	go func() {
-		defer GinkgoRecover()
-		s.StartWget(finished, serverAddress, "80", query, "")
-	}()
-	err := <-finished
-	s.AssertNil(err)
+	client := NewHttpClient()
+	req, err := http.NewRequest("GET", "http://"+serverAddress+":80/"+query, nil)
+	s.AssertNil(err, fmt.Sprint(err))
+	resp, err := client.Do(req)
+	s.AssertNil(err, fmt.Sprint(err))
+	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, false))
+	s.AssertEqual(200, resp.StatusCode)
+	s.AssertContains(resp.Header.Get("Content-Type"), "text")
+	s.AssertContains(resp.Header.Get("Content-Type"), "plain")
+	s.AssertNotEqual(int64(0), resp.ContentLength)
+	_, err = io.ReadAll(resp.Body)
+}
+
+func HttpStaticFileHandler(s *NoTopoSuite) {
+	content := "<http><body><p>Hello</p></body></http>"
+	content2 := "<http><body><p>Page</p></body></http>"
+	vpp := s.GetContainerByName("vpp").VppInstance
+	vpp.Container.Exec("mkdir -p " + wwwRootPath)
+	vpp.Container.CreateFile(wwwRootPath+"/index.html", content)
+	vpp.Container.CreateFile(wwwRootPath+"/page.html", content2)
+	serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString()
+	s.Log(vpp.Vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug cache-size 2m"))
+
+	client := NewHttpClient()
+	req, err := http.NewRequest("GET", "http://"+serverAddress+":80/index.html", nil)
+	s.AssertNil(err, fmt.Sprint(err))
+	resp, err := client.Do(req)
+	s.AssertNil(err, fmt.Sprint(err))
+	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
+	s.AssertEqual(200, resp.StatusCode)
+	s.AssertContains(resp.Header.Get("Content-Type"), "html")
+	s.AssertContains(resp.Header.Get("Cache-Control"), "max-age=")
+	s.AssertEqual(int64(len([]rune(content))), resp.ContentLength)
+	body, err := io.ReadAll(resp.Body)
+	s.AssertEqual(string(body), content)
+	o := vpp.Vppctl("show http static server cache verbose")
+	s.Log(o)
+	s.AssertContains(o, "index.html")
+	s.AssertNotContains(o, "page.html")
+
+	resp, err = client.Do(req)
+	s.AssertNil(err, fmt.Sprint(err))
+	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
+	s.AssertEqual(200, resp.StatusCode)
+	s.AssertContains(resp.Header.Get("Content-Type"), "html")
+	s.AssertContains(resp.Header.Get("Cache-Control"), "max-age=")
+	s.AssertEqual(int64(len([]rune(content))), resp.ContentLength)
+	body, err = io.ReadAll(resp.Body)
+	s.AssertEqual(string(body), content)
+
+	req, err = http.NewRequest("GET", "http://"+serverAddress+":80/page.html", nil)
+	s.AssertNil(err, fmt.Sprint(err))
+	resp, err = client.Do(req)
+	s.AssertNil(err, fmt.Sprint(err))
+	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
+	s.AssertEqual(200, resp.StatusCode)
+	s.AssertContains(resp.Header.Get("Content-Type"), "html")
+	s.AssertContains(resp.Header.Get("Cache-Control"), "max-age=")
+	s.AssertEqual(int64(len([]rune(content2))), resp.ContentLength)
+	body, err = io.ReadAll(resp.Body)
+	s.AssertEqual(string(body), content2)
+	o = vpp.Vppctl("show http static server cache verbose")
+	s.Log(o)
+	s.AssertContains(o, "index.html")
+	s.AssertContains(o, "page.html")
 }
 
 func HttpStaticPathTraversalTest(s *NoTopoSuite) {
@@ -118,7 +179,11 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(404, resp.StatusCode)
+	s.AssertEmpty(resp.Header.Get("Content-Type"))
+	s.AssertEmpty(resp.Header.Get("Cache-Control"))
+	s.AssertEqual(int64(0), resp.ContentLength)
 }
 
 func HttpStaticMovedTest(s *NoTopoSuite) {
@@ -134,8 +199,12 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(301, resp.StatusCode)
-	s.AssertNotEqual("", resp.Header.Get("Location"))
+	s.AssertEqual("http://"+serverAddress+"/tmp.aaa/index.html", resp.Header.Get("Location"))
+	s.AssertEmpty(resp.Header.Get("Content-Type"))
+	s.AssertEmpty(resp.Header.Get("Cache-Control"))
+	s.AssertEqual(int64(0), resp.ContentLength)
 }
 
 func HttpStaticNotFoundTest(s *NoTopoSuite) {
@@ -150,7 +219,11 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(404, resp.StatusCode)
+	s.AssertEmpty(resp.Header.Get("Content-Type"))
+	s.AssertEmpty(resp.Header.Get("Cache-Control"))
+	s.AssertEqual(int64(0), resp.ContentLength)
 }
 
 func HttpCliMethodNotAllowedTest(s *NoTopoSuite) {
@@ -164,9 +237,11 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(405, resp.StatusCode)
-	// TODO: need to be fixed in http code
-	//s.AssertNotEqual("", resp.Header.Get("Allow"))
+	s.AssertNotEqual("", resp.Header.Get("Allow"), "server MUST generate an Allow header")
+	s.AssertEmpty(resp.Header.Get("Content-Type"))
+	s.AssertEqual(int64(0), resp.ContentLength)
 }
 
 func HttpCliBadRequestTest(s *NoTopoSuite) {
@@ -180,7 +255,10 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(400, resp.StatusCode)
+	s.AssertEmpty(resp.Header.Get("Content-Type"))
+	s.AssertEqual(int64(0), resp.ContentLength)
 }
 
 func HttpStaticBuildInUrlGetVersionTest(s *NoTopoSuite) {
@@ -194,6 +272,7 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(200, resp.StatusCode)
 	data, err := io.ReadAll(resp.Body)
 	s.AssertNil(err, fmt.Sprint(err))
@@ -203,6 +282,7 @@
 	s.AssertNotContains(string(data), "build_by")
 	s.AssertNotContains(string(data), "build_host")
 	s.AssertNotContains(string(data), "build_dir")
+	s.AssertContains(resp.Header.Get("Content-Type"), "json")
 }
 
 func HttpStaticBuildInUrlGetVersionVerboseTest(s *NoTopoSuite) {
@@ -216,6 +296,7 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(200, resp.StatusCode)
 	data, err := io.ReadAll(resp.Body)
 	s.AssertNil(err, fmt.Sprint(err))
@@ -225,6 +306,7 @@
 	s.AssertContains(string(data), "build_by")
 	s.AssertContains(string(data), "build_host")
 	s.AssertContains(string(data), "build_dir")
+	s.AssertContains(resp.Header.Get("Content-Type"), "json")
 }
 
 func HttpStaticBuildInUrlGetIfListTest(s *NoTopoSuite) {
@@ -238,11 +320,13 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(200, resp.StatusCode)
 	data, err := io.ReadAll(resp.Body)
 	s.AssertNil(err, fmt.Sprint(err))
 	s.AssertContains(string(data), "interface_list")
 	s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).Peer.Name())
+	s.AssertContains(resp.Header.Get("Content-Type"), "json")
 }
 
 func HttpStaticBuildInUrlGetIfStatsTest(s *NoTopoSuite) {
@@ -256,12 +340,14 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(200, resp.StatusCode)
 	data, err := io.ReadAll(resp.Body)
 	s.AssertNil(err, fmt.Sprint(err))
 	s.AssertContains(string(data), "interface_stats")
 	s.AssertContains(string(data), "local0")
 	s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).Peer.Name())
+	s.AssertContains(resp.Header.Get("Content-Type"), "json")
 }
 
 func validatePostInterfaceStats(s *NoTopoSuite, data string) {
@@ -284,10 +370,12 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(200, resp.StatusCode)
 	data, err := io.ReadAll(resp.Body)
 	s.AssertNil(err, fmt.Sprint(err))
 	validatePostInterfaceStats(s, string(data))
+	s.AssertContains(resp.Header.Get("Content-Type"), "json")
 }
 
 func HttpStaticMacTimeTest(s *NoTopoSuite) {
@@ -302,12 +390,14 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(200, resp.StatusCode)
 	data, err := io.ReadAll(resp.Body)
 	s.AssertNil(err, fmt.Sprint(err))
 	s.AssertContains(string(data), "mactime")
 	s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).Ip4AddressString())
 	s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).HwAddress.String())
+	s.AssertContains(resp.Header.Get("Content-Type"), "json")
 }
 
 func HttpInvalidRequestLineTest(s *NoTopoSuite) {
@@ -444,7 +534,10 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(501, resp.StatusCode)
+	s.AssertEmpty(resp.Header.Get("Content-Type"))
+	s.AssertEqual(int64(0), resp.ContentLength)
 }
 
 func HttpVersionNotSupportedTest(s *NoTopoSuite) {
@@ -468,12 +561,13 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
 	s.AssertEqual(200, resp.StatusCode)
 	data, err := io.ReadAll(resp.Body)
 	s.AssertNil(err, fmt.Sprint(err))
-	s.Log(string(data))
 	s.AssertNotContains(string(data), "unknown input")
 	s.AssertContains(string(data), "Compiler")
+	s.AssertContains(resp.Header.Get("Content-Type"), "html")
 }
 
 func HttpHeadersTest(s *NoTopoSuite) {
@@ -539,5 +633,8 @@
 	resp, err := client.Do(req)
 	s.AssertNil(err, fmt.Sprint(err))
 	defer resp.Body.Close()
+	s.Log(DumpHttpResp(resp, true))
+	s.AssertEqual(200, resp.StatusCode)
 	s.AssertEqual("http_cli_server", resp.Header.Get("Server"))
+	s.AssertContains(resp.Header.Get("Content-Type"), "html")
 }
diff --git a/extras/hs-test/infra/utils.go b/extras/hs-test/infra/utils.go
index 9619efb..05b7b36 100644
--- a/extras/hs-test/infra/utils.go
+++ b/extras/hs-test/infra/utils.go
@@ -5,6 +5,7 @@
 	"io"
 	"net"
 	"net/http"
+	"net/http/httputil"
 	"os"
 	"strings"
 	"time"
@@ -96,6 +97,14 @@
 	return client
 }
 
+func DumpHttpResp(resp *http.Response, body bool) string {
+	dump, err := httputil.DumpResponse(resp, body)
+	if err != nil {
+		return ""
+	}
+	return string(dump)
+}
+
 func TcpSendReceive(address, data string) (string, error) {
 	conn, err := net.DialTimeout("tcp", address, time.Second*30)
 	if err != nil {