123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- // Copyright 2015 The Go Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
- /*
- The h2i command is an interactive HTTP/2 console.
- Usage:
- $ h2i [flags] <hostname>
- Interactive commands in the console: (all parts case-insensitive)
- ping [data]
- settings ack
- settings FOO=n BAR=z
- headers (open a new stream by typing HTTP/1.1)
- */
- package main
- import (
- "bufio"
- "bytes"
- "crypto/tls"
- "errors"
- "flag"
- "fmt"
- "io"
- "log"
- "net"
- "net/http"
- "os"
- "regexp"
- "strconv"
- "strings"
- "golang.org/x/net/http2"
- "golang.org/x/net/http2/hpack"
- "golang.org/x/term"
- )
- // Flags
- var (
- flagNextProto = flag.String("nextproto", "h2,h2-14", "Comma-separated list of NPN/ALPN protocol names to negotiate.")
- flagInsecure = flag.Bool("insecure", false, "Whether to skip TLS cert validation")
- flagSettings = flag.String("settings", "empty", "comma-separated list of KEY=value settings for the initial SETTINGS frame. The magic value 'empty' sends an empty initial settings frame, and the magic value 'omit' causes no initial settings frame to be sent.")
- flagDial = flag.String("dial", "", "optional ip:port to dial, to connect to a host:port but use a different SNI name (including a SNI name without DNS)")
- )
- type command struct {
- run func(*h2i, []string) error // required
- // complete optionally specifies tokens (case-insensitive) which are
- // valid for this subcommand.
- complete func() []string
- }
- var commands = map[string]command{
- "ping": {run: (*h2i).cmdPing},
- "settings": {
- run: (*h2i).cmdSettings,
- complete: func() []string {
- return []string{
- "ACK",
- http2.SettingHeaderTableSize.String(),
- http2.SettingEnablePush.String(),
- http2.SettingMaxConcurrentStreams.String(),
- http2.SettingInitialWindowSize.String(),
- http2.SettingMaxFrameSize.String(),
- http2.SettingMaxHeaderListSize.String(),
- }
- },
- },
- "quit": {run: (*h2i).cmdQuit},
- "headers": {run: (*h2i).cmdHeaders},
- }
- func usage() {
- fmt.Fprintf(os.Stderr, "Usage: h2i <hostname>\n\n")
- flag.PrintDefaults()
- }
- // withPort adds ":443" if another port isn't already present.
- func withPort(host string) string {
- if _, _, err := net.SplitHostPort(host); err != nil {
- return net.JoinHostPort(host, "443")
- }
- return host
- }
- // withoutPort strips the port from addr if present.
- func withoutPort(addr string) string {
- if h, _, err := net.SplitHostPort(addr); err == nil {
- return h
- }
- return addr
- }
- // h2i is the app's state.
- type h2i struct {
- host string
- tc *tls.Conn
- framer *http2.Framer
- term *term.Terminal
- // owned by the command loop:
- streamID uint32
- hbuf bytes.Buffer
- henc *hpack.Encoder
- // owned by the readFrames loop:
- peerSetting map[http2.SettingID]uint32
- hdec *hpack.Decoder
- }
- func main() {
- flag.Usage = usage
- flag.Parse()
- if flag.NArg() != 1 {
- usage()
- os.Exit(2)
- }
- log.SetFlags(0)
- host := flag.Arg(0)
- app := &h2i{
- host: host,
- peerSetting: make(map[http2.SettingID]uint32),
- }
- app.henc = hpack.NewEncoder(&app.hbuf)
- if err := app.Main(); err != nil {
- if app.term != nil {
- app.logf("%v\n", err)
- } else {
- fmt.Fprintf(os.Stderr, "%v\n", err)
- }
- os.Exit(1)
- }
- fmt.Fprintf(os.Stdout, "\n")
- }
- func (app *h2i) Main() error {
- cfg := &tls.Config{
- ServerName: withoutPort(app.host),
- NextProtos: strings.Split(*flagNextProto, ","),
- InsecureSkipVerify: *flagInsecure,
- }
- hostAndPort := *flagDial
- if hostAndPort == "" {
- hostAndPort = withPort(app.host)
- }
- log.Printf("Connecting to %s ...", hostAndPort)
- tc, err := tls.Dial("tcp", hostAndPort, cfg)
- if err != nil {
- return fmt.Errorf("Error dialing %s: %v", hostAndPort, err)
- }
- log.Printf("Connected to %v", tc.RemoteAddr())
- defer tc.Close()
- if err := tc.Handshake(); err != nil {
- return fmt.Errorf("TLS handshake: %v", err)
- }
- if !*flagInsecure {
- if err := tc.VerifyHostname(app.host); err != nil {
- return fmt.Errorf("VerifyHostname: %v", err)
- }
- }
- state := tc.ConnectionState()
- log.Printf("Negotiated protocol %q", state.NegotiatedProtocol)
- if !state.NegotiatedProtocolIsMutual || state.NegotiatedProtocol == "" {
- return fmt.Errorf("Could not negotiate protocol mutually")
- }
- if _, err := io.WriteString(tc, http2.ClientPreface); err != nil {
- return err
- }
- app.framer = http2.NewFramer(tc, tc)
- oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
- if err != nil {
- return err
- }
- defer term.Restore(0, oldState)
- var screen = struct {
- io.Reader
- io.Writer
- }{os.Stdin, os.Stdout}
- app.term = term.NewTerminal(screen, "h2i> ")
- lastWord := regexp.MustCompile(`.+\W(\w+)$`)
- app.term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
- if key != '\t' {
- return
- }
- if pos != len(line) {
- // TODO: we're being lazy for now, only supporting tab completion at the end.
- return
- }
- // Auto-complete for the command itself.
- if !strings.Contains(line, " ") {
- var name string
- name, _, ok = lookupCommand(line)
- if !ok {
- return
- }
- return name, len(name), true
- }
- _, c, ok := lookupCommand(line[:strings.IndexByte(line, ' ')])
- if !ok || c.complete == nil {
- return
- }
- if strings.HasSuffix(line, " ") {
- app.logf("%s", strings.Join(c.complete(), " "))
- return line, pos, true
- }
- m := lastWord.FindStringSubmatch(line)
- if m == nil {
- return line, len(line), true
- }
- soFar := m[1]
- var match []string
- for _, cand := range c.complete() {
- if len(soFar) > len(cand) || !strings.EqualFold(cand[:len(soFar)], soFar) {
- continue
- }
- match = append(match, cand)
- }
- if len(match) == 0 {
- return
- }
- if len(match) > 1 {
- // TODO: auto-complete any common prefix
- app.logf("%s", strings.Join(match, " "))
- return line, pos, true
- }
- newLine = line[:len(line)-len(soFar)] + match[0]
- return newLine, len(newLine), true
- }
- errc := make(chan error, 2)
- go func() { errc <- app.readFrames() }()
- go func() { errc <- app.readConsole() }()
- return <-errc
- }
- func (app *h2i) logf(format string, args ...interface{}) {
- fmt.Fprintf(app.term, format+"\r\n", args...)
- }
- func (app *h2i) readConsole() error {
- if s := *flagSettings; s != "omit" {
- var args []string
- if s != "empty" {
- args = strings.Split(s, ",")
- }
- _, c, ok := lookupCommand("settings")
- if !ok {
- panic("settings command not found")
- }
- c.run(app, args)
- }
- for {
- line, err := app.term.ReadLine()
- if err == io.EOF {
- return nil
- }
- if err != nil {
- return fmt.Errorf("term.ReadLine: %v", err)
- }
- f := strings.Fields(line)
- if len(f) == 0 {
- continue
- }
- cmd, args := f[0], f[1:]
- if _, c, ok := lookupCommand(cmd); ok {
- err = c.run(app, args)
- } else {
- app.logf("Unknown command %q", line)
- }
- if err == errExitApp {
- return nil
- }
- if err != nil {
- return err
- }
- }
- }
- func lookupCommand(prefix string) (name string, c command, ok bool) {
- prefix = strings.ToLower(prefix)
- if c, ok = commands[prefix]; ok {
- return prefix, c, ok
- }
- for full, candidate := range commands {
- if strings.HasPrefix(full, prefix) {
- if c.run != nil {
- return "", command{}, false // ambiguous
- }
- c = candidate
- name = full
- }
- }
- return name, c, c.run != nil
- }
- var errExitApp = errors.New("internal sentinel error value to quit the console reading loop")
- func (a *h2i) cmdQuit(args []string) error {
- if len(args) > 0 {
- a.logf("the QUIT command takes no argument")
- return nil
- }
- return errExitApp
- }
- func (a *h2i) cmdSettings(args []string) error {
- if len(args) == 1 && strings.EqualFold(args[0], "ACK") {
- return a.framer.WriteSettingsAck()
- }
- var settings []http2.Setting
- for _, arg := range args {
- if strings.EqualFold(arg, "ACK") {
- a.logf("Error: ACK must be only argument with the SETTINGS command")
- return nil
- }
- eq := strings.Index(arg, "=")
- if eq == -1 {
- a.logf("Error: invalid argument %q (expected SETTING_NAME=nnnn)", arg)
- return nil
- }
- sid, ok := settingByName(arg[:eq])
- if !ok {
- a.logf("Error: unknown setting name %q", arg[:eq])
- return nil
- }
- val, err := strconv.ParseUint(arg[eq+1:], 10, 32)
- if err != nil {
- a.logf("Error: invalid argument %q (expected SETTING_NAME=nnnn)", arg)
- return nil
- }
- settings = append(settings, http2.Setting{
- ID: sid,
- Val: uint32(val),
- })
- }
- a.logf("Sending: %v", settings)
- return a.framer.WriteSettings(settings...)
- }
- func settingByName(name string) (http2.SettingID, bool) {
- for _, sid := range [...]http2.SettingID{
- http2.SettingHeaderTableSize,
- http2.SettingEnablePush,
- http2.SettingMaxConcurrentStreams,
- http2.SettingInitialWindowSize,
- http2.SettingMaxFrameSize,
- http2.SettingMaxHeaderListSize,
- } {
- if strings.EqualFold(sid.String(), name) {
- return sid, true
- }
- }
- return 0, false
- }
- func (app *h2i) cmdPing(args []string) error {
- if len(args) > 1 {
- app.logf("invalid PING usage: only accepts 0 or 1 args")
- return nil // nil means don't end the program
- }
- var data [8]byte
- if len(args) == 1 {
- copy(data[:], args[0])
- } else {
- copy(data[:], "h2i_ping")
- }
- return app.framer.WritePing(false, data)
- }
- func (app *h2i) cmdHeaders(args []string) error {
- if len(args) > 0 {
- app.logf("Error: HEADERS doesn't yet take arguments.")
- // TODO: flags for restricting window size, to force CONTINUATION
- // frames.
- return nil
- }
- var h1req bytes.Buffer
- app.term.SetPrompt("(as HTTP/1.1)> ")
- defer app.term.SetPrompt("h2i> ")
- for {
- line, err := app.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 {
- app.logf("Invalid HTTP/1.1 request: %v", err)
- return nil
- }
- if app.streamID == 0 {
- app.streamID = 1
- } else {
- app.streamID += 2
- }
- app.logf("Opening Stream-ID %d:", app.streamID)
- hbf := app.encodeHeaders(req)
- if len(hbf) > 16<<10 {
- app.logf("TODO: h2i doesn't yet write CONTINUATION frames. Copy it from transport.go")
- return nil
- }
- return app.framer.WriteHeaders(http2.HeadersFrameParam{
- StreamID: app.streamID,
- BlockFragment: hbf,
- EndStream: req.Method == "GET" || req.Method == "HEAD", // good enough for now
- EndHeaders: true, // for now
- })
- }
- func (app *h2i) readFrames() error {
- for {
- f, err := app.framer.ReadFrame()
- if err != nil {
- return fmt.Errorf("ReadFrame: %v", err)
- }
- app.logf("%v", f)
- switch f := f.(type) {
- case *http2.PingFrame:
- app.logf(" Data = %q", f.Data)
- case *http2.SettingsFrame:
- f.ForeachSetting(func(s http2.Setting) error {
- app.logf(" %v", s)
- app.peerSetting[s.ID] = s.Val
- return nil
- })
- case *http2.WindowUpdateFrame:
- app.logf(" Window-Increment = %v", f.Increment)
- case *http2.GoAwayFrame:
- app.logf(" Last-Stream-ID = %d; Error-Code = %v (%d)", f.LastStreamID, f.ErrCode, f.ErrCode)
- case *http2.DataFrame:
- app.logf(" %q", f.Data())
- case *http2.HeadersFrame:
- if f.HasPriority() {
- app.logf(" PRIORITY = %v", f.Priority)
- }
- if app.hdec == nil {
- // TODO: if the user uses h2i to send a SETTINGS frame advertising
- // something larger, we'll need to respect SETTINGS_HEADER_TABLE_SIZE
- // and stuff here instead of using the 4k default. But for now:
- tableSize := uint32(4 << 10)
- app.hdec = hpack.NewDecoder(tableSize, app.onNewHeaderField)
- }
- app.hdec.Write(f.HeaderBlockFragment())
- case *http2.PushPromiseFrame:
- if app.hdec == nil {
- // TODO: if the user uses h2i to send a SETTINGS frame advertising
- // something larger, we'll need to respect SETTINGS_HEADER_TABLE_SIZE
- // and stuff here instead of using the 4k default. But for now:
- tableSize := uint32(4 << 10)
- app.hdec = hpack.NewDecoder(tableSize, app.onNewHeaderField)
- }
- app.hdec.Write(f.HeaderBlockFragment())
- }
- }
- }
- // called from readLoop
- func (app *h2i) onNewHeaderField(f hpack.HeaderField) {
- if f.Sensitive {
- app.logf(" %s = %q (SENSITIVE)", f.Name, f.Value)
- }
- app.logf(" %s = %q", f.Name, f.Value)
- }
- func (app *h2i) encodeHeaders(req *http.Request) []byte {
- app.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.RequestURI
- if path == "" {
- path = "/"
- }
- app.writeHeader(":authority", host) // probably not right for all sites
- app.writeHeader(":method", req.Method)
- app.writeHeader(":path", path)
- app.writeHeader(":scheme", "https")
- for k, vv := range req.Header {
- lowKey := strings.ToLower(k)
- if lowKey == "host" {
- continue
- }
- for _, v := range vv {
- app.writeHeader(lowKey, v)
- }
- }
- return app.hbuf.Bytes()
- }
- func (app *h2i) writeHeader(name, value string) {
- app.henc.WriteField(hpack.HeaderField{Name: name, Value: value})
- app.logf(" %s = %s", name, value)
- }
|