diff --git a/integrationtests/integrationtests_suite_test.go b/integrationtests/integrationtests_suite_test.go index a9a5442b8..5ba0355fe 100644 --- a/integrationtests/integrationtests_suite_test.go +++ b/integrationtests/integrationtests_suite_test.go @@ -42,7 +42,8 @@ var ( dataMan dataManager port string uploadDir string - clientPath string + clientPath string // path of the quic_client + serverPath string // path of the quic_server docker *gexec.Session ) @@ -78,6 +79,7 @@ var _ = BeforeEach(func() { Fail("Failed to get current path") } clientPath = filepath.Join(thisfile, fmt.Sprintf("../../../quic-clients/client-%s-debug", runtime.GOOS)) + serverPath = filepath.Join(thisfile, fmt.Sprintf("../../../quic-clients/server-%s-debug", runtime.GOOS)) }) var _ = AfterEach(func() { diff --git a/integrationtests/server_test.go b/integrationtests/server_test.go new file mode 100644 index 000000000..a338d509e --- /dev/null +++ b/integrationtests/server_test.go @@ -0,0 +1,213 @@ +package integrationtests + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "io/ioutil" + "math/big" + mrand "math/rand" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "time" + + "github.com/lucas-clemente/quic-go/h2quic" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Server tests", func() { + mrand.Seed(time.Now().UnixNano()) + + var ( + serverPort string + tmpDir string + session *Session + client *http.Client + ) + + generateCA := func() (*rsa.PrivateKey, *x509.Certificate) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + Expect(err).ToNot(HaveOccurred()) + + t := time.Now().Add(-time.Minute) + templateRoot := &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: t, + NotAfter: t.Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + } + certDER, err := x509.CreateCertificate(rand.Reader, templateRoot, templateRoot, &key.PublicKey, key) + Expect(err).ToNot(HaveOccurred()) + cert, err := x509.ParseCertificate(certDER) + Expect(err).ToNot(HaveOccurred()) + return key, cert + } + + // prepare the file such that it can be by the quic_server + // some HTTP headers neeed to be prepended, see https://www.chromium.org/quic/playing-with-quic + createDownloadFile := func(filename string, data []byte) { + dataDir := filepath.Join(tmpDir, "quic.clemente.io") + err := os.Mkdir(dataDir, 0777) + Expect(err).ToNot(HaveOccurred()) + f, err := os.Create(filepath.Join(dataDir, filename)) + Expect(err).ToNot(HaveOccurred()) + defer f.Close() + _, err = f.Write([]byte("HTTP/1.1 200 OK\n")) + Expect(err).ToNot(HaveOccurred()) + _, err = f.Write([]byte("Content-Type: text/html\n")) + Expect(err).ToNot(HaveOccurred()) + _, err = f.Write([]byte("X-Original-Url: https://quic.clemente.io:" + serverPort + "/" + filename + "\n")) + Expect(err).ToNot(HaveOccurred()) + _, err = f.Write([]byte("Content-Length: " + strconv.Itoa(len(data)) + "\n\n")) + Expect(err).ToNot(HaveOccurred()) + _, err = f.Write(data) + Expect(err).ToNot(HaveOccurred()) + } + + // download files must be create *before* the quic_server is started + // the quic_server reads its data dir on startup, and only serves those files that were already present then + startServer := func() { + go func() { + defer GinkgoRecover() + var err error + command := exec.Command( + serverPath, + "--quic_response_cache_dir="+filepath.Join(tmpDir, "quic.clemente.io"), + "--key_file="+filepath.Join(tmpDir, "key.pkcs8"), + "--certificate_file="+filepath.Join(tmpDir, "cert.pem"), + "--port="+serverPort, + ) + session, err = Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + }() + } + + stopServer := func() { + session.Kill() + } + + BeforeEach(func() { + serverPort = strconv.Itoa(20000 + int(mrand.Int31n(10000))) + + var err error + tmpDir, err = ioutil.TempDir("", "quic-server-certs") + Expect(err).ToNot(HaveOccurred()) + + // generate an RSA key pair for the server + key, err := rsa.GenerateKey(rand.Reader, 1024) + Expect(err).ToNot(HaveOccurred()) + + // save the private key in PKCS8 format to disk (required by quic_server) + pkcs8key, err := asn1.Marshal(struct { // copied from the x509 package + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte + }{ + PrivateKey: x509.MarshalPKCS1PrivateKey(key), + Algo: pkix.AlgorithmIdentifier{ + Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}, + Parameters: asn1.RawValue{Tag: 5}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + f, err := os.Create(filepath.Join(tmpDir, "key.pkcs8")) + Expect(err).ToNot(HaveOccurred()) + _, err = f.Write(pkcs8key) + Expect(err).ToNot(HaveOccurred()) + f.Close() + + // generate a Certificate Authority + // this CA is used to sign the server's key + // it is set as a valid CA in the QUIC client + rootKey, CACert := generateCA() + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + Subject: pkix.Name{CommonName: "quic.clemente.io"}, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, CACert, &key.PublicKey, rootKey) + Expect(err).ToNot(HaveOccurred()) + // save the certificate to disk + certOut, err := os.Create(filepath.Join(tmpDir, "cert.pem")) + Expect(err).ToNot(HaveOccurred()) + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + certOut.Close() + + // prepare the h2quic.client + certPool := x509.NewCertPool() + certPool.AddCert(CACert) + client = &http.Client{ + Transport: &h2quic.QuicRoundTripper{ + TLSClientConfig: &tls.Config{RootCAs: certPool}, + }, + } + }) + + AfterEach(func() { + Expect(tmpDir).ToNot(BeEmpty()) + err := os.RemoveAll(tmpDir) + Expect(err).ToNot(HaveOccurred()) + tmpDir = "" + }) + + It("downloads a hello", func(done Done) { + data := []byte("Hello world!\n") + createDownloadFile("hello", data) + + startServer() + defer stopServer() + + rsp, err := client.Get("https://quic.clemente.io:" + serverPort + "/hello") + Expect(err).ToNot(HaveOccurred()) + Expect(rsp.StatusCode).To(Equal(200)) + body, err := ioutil.ReadAll(rsp.Body) + Expect(err).ToNot(HaveOccurred()) + Expect(body).To(Equal(data)) + close(done) + }, 5) + + It("downloads a small file", func(done Done) { + dataMan.GenerateData(dataLen) + data := dataMan.GetData() + createDownloadFile("file.dat", data) + + startServer() + defer stopServer() + + rsp, err := client.Get("https://quic.clemente.io:" + serverPort + "/file.dat") + Expect(err).ToNot(HaveOccurred()) + Expect(rsp.StatusCode).To(Equal(200)) + body, err := ioutil.ReadAll(rsp.Body) + Expect(err).ToNot(HaveOccurred()) + Expect(body).To(Equal(data)) + close(done) + }, 5) + + It("downloads a large file", func(done Done) { + dataMan.GenerateData(dataLongLen) + data := dataMan.GetData() + createDownloadFile("file.dat", data) + + startServer() + defer stopServer() + + rsp, err := client.Get("https://quic.clemente.io:" + serverPort + "/file.dat") + Expect(err).ToNot(HaveOccurred()) + Expect(rsp.StatusCode).To(Equal(200)) + body, err := ioutil.ReadAll(rsp.Body) + Expect(err).ToNot(HaveOccurred()) + Expect(body).To(Equal(data)) + close(done) + }, 20) +})