Browse Source

h2i: add HEADERS command, to send requests, typed as HTTP/1.1

w/ @bmizerany
Brad Fitzpatrick 10 years ago
parent
commit
647a8d1510
1 changed files with 100 additions and 5 deletions
  1. 100 5
      h2i/h2i.go

+ 100 - 5
h2i/h2i.go

@@ -19,6 +19,8 @@ Interactive commands in the console:
 package main
 
 import (
+	"bufio"
+	"bytes"
 	"crypto/tls"
 	"errors"
 	"flag"
@@ -26,10 +28,12 @@ import (
 	"io"
 	"log"
 	"net"
+	"net/http"
 	"os"
 	"strings"
 
 	"github.com/bradfitz/http2"
+	"github.com/bradfitz/http2/hpack"
 	"golang.org/x/crypto/ssh/terminal"
 )
 
@@ -42,9 +46,10 @@ var (
 type command func(*h2i, []string) error
 
 var commands = map[string]command{
-	"ping":     (*h2i).sendPing,
-	"settings": (*h2i).sendSettings,
+	"ping":     (*h2i).cmdPing,
+	"settings": (*h2i).cmdSettings,
 	"quit":     (*h2i).cmdQuit,
+	"headers":  (*h2i).cmdHeaders,
 }
 
 func usage() {
@@ -67,6 +72,11 @@ type h2i struct {
 	tc     *tls.Conn
 	framer *http2.Framer
 	term   *terminal.Terminal
+
+	// owned by the command loop:
+	streamID uint32
+	hbuf     bytes.Buffer
+	henc     *hpack.Encoder
 }
 
 func main() {
@@ -81,6 +91,8 @@ func main() {
 	app := &h2i{
 		host: host,
 	}
+	app.henc = hpack.NewEncoder(&app.hbuf)
+
 	if err := app.Main(); err != nil {
 		if app.term != nil {
 			app.logf("%v\n", err)
@@ -138,7 +150,7 @@ func (a *h2i) Main() error {
 		io.Writer
 	}{os.Stdin, os.Stdout}
 
-	a.term = terminal.NewTerminal(screen, "> ")
+	a.term = terminal.NewTerminal(screen, "h2i> ")
 	a.term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
 		if key != '\t' {
 			return
@@ -213,7 +225,7 @@ func (a *h2i) cmdQuit(args []string) error {
 	return errExitApp
 }
 
-func (a *h2i) sendSettings(args []string) error {
+func (a *h2i) cmdSettings(args []string) error {
 	if len(args) == 1 && args[0] == "ack" {
 		return a.framer.WriteSettingsAck()
 	}
@@ -221,7 +233,7 @@ func (a *h2i) sendSettings(args []string) error {
 	return nil
 }
 
-func (a *h2i) sendPing(args []string) error {
+func (a *h2i) cmdPing(args []string) error {
 	if len(args) > 1 {
 		a.logf("invalid PING usage: only accepts 0 or 1 args")
 		return nil // nil means don't end the program
@@ -235,6 +247,51 @@ func (a *h2i) sendPing(args []string) error {
 	return a.framer.WritePing(false, data)
 }
 
+func (a *h2i) cmdHeaders(args []string) error {
+	if len(args) > 0 {
+		a.logf("Error: HEADERS doesn't yet take arguments.")
+		// TODO: flags for restricting window size, to force CONTINUATION
+		// frames.
+		return nil
+	}
+	var h1req bytes.Buffer
+	a.term.SetPrompt("(as HTTP/1.1)> ")
+	defer a.term.SetPrompt("h2i> ")
+	for {
+		line, err := a.term.ReadLine()
+		if err != nil {
+			return err
+		}
+		h1req.WriteString(line)
+		h1req.WriteString("\r\n")
+		if line == "" {
+			break
+		}
+	}
+	req, err := http.ReadRequest(bufio.NewReader(&h1req))
+	if err != nil {
+		a.logf("Invalid HTTP/1.1 request: %v", err)
+		return nil
+	}
+	if a.streamID == 0 {
+		a.streamID = 1
+	} else {
+		a.streamID += 2
+	}
+	a.logf("Opening Stream-ID %d:", a.streamID)
+	hbf := a.encodeHeaders(req)
+	if len(hbf) > 16<<10 {
+		a.logf("TODO: h2i doesn't yet write CONTINUATION frames. Copy it from transport.go")
+		return nil
+	}
+	return a.framer.WriteHeaders(http2.HeadersFrameParam{
+		StreamID:      a.streamID,
+		BlockFragment: hbf,
+		EndStream:     req.Method == "GET" || req.Method == "HEAD", // good enough for now
+		EndHeaders:    true,                                        // for now
+	})
+}
+
 func (a *h2i) readFrames() error {
 	for {
 		f, err := a.framer.ReadFrame()
@@ -254,6 +311,44 @@ func (a *h2i) readFrames() error {
 			a.logf("  Window-Increment = %v\n", f.Increment)
 		case *http2.GoAwayFrame:
 			a.logf("  Last-Stream-ID = %d; Error-Code = %v (%d)\n", f.LastStreamID, f.ErrCode, f.ErrCode)
+		case *http2.DataFrame:
+			a.logf("  %q", f.Data())
 		}
 	}
 }
+
+func (a *h2i) encodeHeaders(req *http.Request) []byte {
+	a.hbuf.Reset()
+
+	// TODO(bradfitz): figure out :authority-vs-Host stuff between http2 and Go
+	host := req.Host
+	if host == "" {
+		host = req.URL.Host
+	}
+
+	path := req.URL.Path
+	if path == "" {
+		path = "/"
+	}
+
+	a.writeHeader(":authority", host) // probably not right for all sites
+	a.writeHeader(":method", req.Method)
+	a.writeHeader(":path", path)
+	a.writeHeader(":scheme", "https")
+
+	for k, vv := range req.Header {
+		lowKey := strings.ToLower(k)
+		if lowKey == "host" {
+			continue
+		}
+		for _, v := range vv {
+			a.writeHeader(lowKey, v)
+		}
+	}
+	return a.hbuf.Bytes()
+}
+
+func (a *h2i) writeHeader(name, value string) {
+	a.henc.WriteField(hpack.HeaderField{Name: name, Value: value})
+	a.logf(" %s = %s", name, value)
+}