mirror of
https://github.com/refraction-networking/uquic.git
synced 2025-04-04 12:47:36 +03:00
add a HTTP/0.9 implementation
This commit is contained in:
parent
4af8a33c3f
commit
789ea13dde
5 changed files with 407 additions and 0 deletions
|
@ -6,6 +6,7 @@ coverage:
|
||||||
- streams_map_outgoing_bidi.go
|
- streams_map_outgoing_bidi.go
|
||||||
- streams_map_outgoing_uni.go
|
- streams_map_outgoing_uni.go
|
||||||
- http3/gzip_reader.go
|
- http3/gzip_reader.go
|
||||||
|
- interop/
|
||||||
- internal/ackhandler/packet_linkedlist.go
|
- internal/ackhandler/packet_linkedlist.go
|
||||||
- internal/utils/byteinterval_linkedlist.go
|
- internal/utils/byteinterval_linkedlist.go
|
||||||
- internal/utils/packetinterval_linkedlist.go
|
- internal/utils/packetinterval_linkedlist.go
|
||||||
|
|
145
interop/http09/client.go
Normal file
145
interop/http09/client.go
Normal 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)
|
||||||
|
}
|
13
interop/http09/http09_suite_test.go
Normal file
13
interop/http09/http09_suite_test.go
Normal 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")
|
||||||
|
}
|
90
interop/http09/http_test.go
Normal file
90
interop/http09/http_test.go
Normal 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
158
interop/http09/server.go
Normal 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()
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue