add a HTTP/0.9 implementation

This commit is contained in:
Marten Seemann 2019-10-05 00:11:16 +07:00
parent 4af8a33c3f
commit 789ea13dde
5 changed files with 407 additions and 0 deletions

View file

@ -6,6 +6,7 @@ coverage:
- streams_map_outgoing_bidi.go
- streams_map_outgoing_uni.go
- http3/gzip_reader.go
- interop/
- internal/ackhandler/packet_linkedlist.go
- internal/utils/byteinterval_linkedlist.go
- internal/utils/packetinterval_linkedlist.go

145
interop/http09/client.go Normal file
View file

@ -0,0 +1,145 @@
package http09
import (
"context"
"crypto/tls"
"errors"
"io/ioutil"
"net"
"net/http"
"strings"
"sync"
"golang.org/x/net/idna"
"github.com/lucas-clemente/quic-go"
)
// RoundTripper performs HTTP/0.9 roundtrips over QUIC.
type RoundTripper struct {
mutex sync.Mutex
TLSClientConfig *tls.Config
QuicConfig *quic.Config
clients map[string]*client
}
var _ http.RoundTripper = &RoundTripper{}
// RoundTrip performs a HTTP/0.9 request.
// It only supports GET requests.
func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodGet {
return nil, errors.New("only GET requests supported")
}
r.mutex.Lock()
hostname := authorityAddr("https", hostnameFromRequest(req))
if r.clients == nil {
r.clients = make(map[string]*client)
}
c, ok := r.clients[hostname]
if !ok {
tlsConf := r.TLSClientConfig.Clone()
tlsConf.NextProtos = []string{h09alpn}
c = &client{
hostname: hostname,
tlsConf: tlsConf,
quicConf: r.QuicConfig,
}
r.clients[hostname] = c
}
r.mutex.Unlock()
return c.RoundTrip(req)
}
// Close closes the roundtripper.
func (r *RoundTripper) Close() error {
r.mutex.Lock()
defer r.mutex.Unlock()
for _, c := range r.clients {
if err := c.Close(); err != nil {
return err
}
}
return nil
}
type client struct {
hostname string
tlsConf *tls.Config
quicConf *quic.Config
once sync.Once
sess quic.Session
dialErr error
}
func (c *client) RoundTrip(req *http.Request) (*http.Response, error) {
c.once.Do(func() {
c.sess, c.dialErr = quic.DialAddr(c.hostname, c.tlsConf, c.quicConf)
})
if c.dialErr != nil {
return nil, c.dialErr
}
return c.doRequest(req)
}
func (c *client) doRequest(req *http.Request) (*http.Response, error) {
str, err := c.sess.OpenStreamSync(context.Background())
if err != nil {
return nil, err
}
cmd := "GET " + req.URL.Path
if _, err := str.Write([]byte(cmd)); err != nil {
return nil, err
}
if err := str.Close(); err != nil {
return nil, err
}
rsp := &http.Response{
Proto: "HTTP/0.9",
ProtoMajor: 0,
ProtoMinor: 9,
Request: req,
Body: ioutil.NopCloser(str),
}
return rsp, nil
}
func (c *client) Close() error {
if c.sess == nil {
return nil
}
return c.sess.Close()
}
func hostnameFromRequest(req *http.Request) string {
if req.URL != nil {
return req.URL.Host
}
return ""
}
// authorityAddr returns a given authority (a host/IP, or host:port / ip:port)
// and returns a host:port. The port 443 is added if needed.
func authorityAddr(scheme string, authority string) (addr string) {
host, port, err := net.SplitHostPort(authority)
if err != nil { // authority didn't have a port
port = "443"
if scheme == "http" {
port = "80"
}
host = authority
}
if a, err := idna.ToASCII(host); err == nil {
host = a
}
// IPv6 address literal, without a port:
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
return host + ":" + port
}
return net.JoinHostPort(host, port)
}

View file

@ -0,0 +1,13 @@
package http09
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestHttp09(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "HTTP/0.9 Suite")
}

View file

@ -0,0 +1,90 @@
package http09
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/internal/testdata"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("HTTP 0.9 integration tests", func() {
var (
server *Server
saddr net.Addr
done chan struct{}
)
http.HandleFunc("/helloworld", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Hello World!"))
})
BeforeEach(func() {
server = &Server{
Server: &http.Server{TLSConfig: testdata.GetTLSConfig()},
}
done = make(chan struct{})
go func() {
defer GinkgoRecover()
defer close(done)
_ = server.ListenAndServe()
}()
var ln quic.Listener
Eventually(func() quic.Listener {
server.mutex.Lock()
defer server.mutex.Unlock()
ln = server.listener
return server.listener
}).ShouldNot(BeNil())
saddr = ln.Addr()
saddr.(*net.UDPAddr).IP = net.IP{127, 0, 0, 1}
})
AfterEach(func() {
Expect(server.Close()).To(Succeed())
Eventually(done).Should(BeClosed())
})
It("performs request", func() {
rt := &RoundTripper{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
defer rt.Close()
req := httptest.NewRequest(
http.MethodGet,
fmt.Sprintf("https://%s/helloworld", saddr),
nil,
)
rsp, err := rt.RoundTrip(req)
Expect(err).ToNot(HaveOccurred())
data, err := ioutil.ReadAll(rsp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(data).To(Equal([]byte("Hello World!")))
})
It("allows setting of headers", func() {
http.HandleFunc("/headers", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("foo", "bar")
w.WriteHeader(1337)
_, _ = w.Write([]byte("done"))
})
rt := &RoundTripper{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
defer rt.Close()
req := httptest.NewRequest(
http.MethodGet,
fmt.Sprintf("https://%s/headers", saddr),
nil,
)
rsp, err := rt.RoundTrip(req)
Expect(err).ToNot(HaveOccurred())
data, err := ioutil.ReadAll(rsp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(data).To(Equal([]byte("done")))
})
})

158
interop/http09/server.go Normal file
View file

@ -0,0 +1,158 @@
package http09
import (
"context"
"errors"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"runtime"
"strings"
"sync"
"github.com/lucas-clemente/quic-go"
)
const h09alpn = "hq-23"
type responseWriter struct {
io.Writer
headers http.Header
}
var _ http.ResponseWriter = &responseWriter{}
func (w *responseWriter) Header() http.Header {
if w.headers == nil {
w.headers = make(http.Header)
}
return w.headers
}
func (w *responseWriter) WriteHeader(int) {}
// Server is a HTTP/0.9 server listening for QUIC connections.
type Server struct {
*http.Server
QuicConfig *quic.Config
mutex sync.Mutex
listener quic.Listener
}
// Close closes the server.
func (s *Server) Close() error {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.listener.Close()
}
// ListenAndServe listens and serves HTTP/0.9 over QUIC.
func (s *Server) ListenAndServe() error {
if s.Server == nil {
return errors.New("use of http3.Server without http.Server")
}
udpAddr, err := net.ResolveUDPAddr("udp", s.Addr)
if err != nil {
return err
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return err
}
tlsConf := s.TLSConfig.Clone()
tlsConf.NextProtos = []string{h09alpn}
ln, err := quic.Listen(conn, tlsConf, s.QuicConfig)
if err != nil {
return err
}
s.mutex.Lock()
s.listener = ln
s.mutex.Unlock()
for {
sess, err := ln.Accept(context.Background())
if err != nil {
return err
}
go s.handleConn(sess)
}
}
func (s *Server) handleConn(sess quic.Session) {
for {
str, err := sess.AcceptStream(context.Background())
if err != nil {
log.Printf("Error accepting stream: %s\n", err.Error())
return
}
go func() {
if err := s.handleStream(str); err != nil {
log.Printf("Handling stream failed: %s", err.Error())
}
}()
}
}
func (s *Server) handleStream(str quic.Stream) error {
reqBytes, err := ioutil.ReadAll(str)
if err != nil {
return err
}
request := string(reqBytes)
request = strings.TrimRight(request, "\r\n")
request = strings.TrimRight(request, " ")
if request[:5] != "GET /" {
str.CancelWrite(42)
return nil
}
u, err := url.Parse(request[4:])
if err != nil {
return err
}
u.Scheme = "https"
req := &http.Request{
Method: http.MethodGet,
Proto: "HTTP/0.9",
ProtoMajor: 0,
ProtoMinor: 9,
Body: str,
URL: u,
}
handler := s.Handler
if handler == nil {
handler = http.DefaultServeMux
}
var panicked bool
func() {
defer func() {
if p := recover(); p != nil {
// Copied from net/http/server.go
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("http: panic serving: %v\n%s", p, buf)
panicked = true
}
}()
handler.ServeHTTP(&responseWriter{Writer: str}, req)
}()
if panicked {
if _, err := str.Write([]byte("500")); err != nil {
return err
}
}
return str.Close()
}