github.com/Andyfoo/golang/x/net@v0.0.0-20190901054642-57c1bf301704/http2/h2i/h2i.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows
     6  
     7  /*
     8  The h2i command is an interactive HTTP/2 console.
     9  
    10  Usage:
    11    $ h2i [flags] <hostname>
    12  
    13  Interactive commands in the console: (all parts case-insensitive)
    14  
    15    ping [data]
    16    settings ack
    17    settings FOO=n BAR=z
    18    headers      (open a new stream by typing HTTP/1.1)
    19  */
    20  package main
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"crypto/tls"
    26  	"errors"
    27  	"flag"
    28  	"fmt"
    29  	"io"
    30  	"log"
    31  	"net"
    32  	"net/http"
    33  	"os"
    34  	"regexp"
    35  	"strconv"
    36  	"strings"
    37  
    38  	"github.com/Andyfoo/golang/x/crypto/ssh/terminal"
    39  	"github.com/Andyfoo/golang/x/net/http2"
    40  	"github.com/Andyfoo/golang/x/net/http2/hpack"
    41  )
    42  
    43  // Flags
    44  var (
    45  	flagNextProto = flag.String("nextproto", "h2,h2-14", "Comma-separated list of NPN/ALPN protocol names to negotiate.")
    46  	flagInsecure  = flag.Bool("insecure", false, "Whether to skip TLS cert validation")
    47  	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.")
    48  	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)")
    49  )
    50  
    51  type command struct {
    52  	run func(*h2i, []string) error // required
    53  
    54  	// complete optionally specifies tokens (case-insensitive) which are
    55  	// valid for this subcommand.
    56  	complete func() []string
    57  }
    58  
    59  var commands = map[string]command{
    60  	"ping": {run: (*h2i).cmdPing},
    61  	"settings": {
    62  		run: (*h2i).cmdSettings,
    63  		complete: func() []string {
    64  			return []string{
    65  				"ACK",
    66  				http2.SettingHeaderTableSize.String(),
    67  				http2.SettingEnablePush.String(),
    68  				http2.SettingMaxConcurrentStreams.String(),
    69  				http2.SettingInitialWindowSize.String(),
    70  				http2.SettingMaxFrameSize.String(),
    71  				http2.SettingMaxHeaderListSize.String(),
    72  			}
    73  		},
    74  	},
    75  	"quit":    {run: (*h2i).cmdQuit},
    76  	"headers": {run: (*h2i).cmdHeaders},
    77  }
    78  
    79  func usage() {
    80  	fmt.Fprintf(os.Stderr, "Usage: h2i <hostname>\n\n")
    81  	flag.PrintDefaults()
    82  }
    83  
    84  // withPort adds ":443" if another port isn't already present.
    85  func withPort(host string) string {
    86  	if _, _, err := net.SplitHostPort(host); err != nil {
    87  		return net.JoinHostPort(host, "443")
    88  	}
    89  	return host
    90  }
    91  
    92  // withoutPort strips the port from addr if present.
    93  func withoutPort(addr string) string {
    94  	if h, _, err := net.SplitHostPort(addr); err == nil {
    95  		return h
    96  	}
    97  	return addr
    98  }
    99  
   100  // h2i is the app's state.
   101  type h2i struct {
   102  	host   string
   103  	tc     *tls.Conn
   104  	framer *http2.Framer
   105  	term   *terminal.Terminal
   106  
   107  	// owned by the command loop:
   108  	streamID uint32
   109  	hbuf     bytes.Buffer
   110  	henc     *hpack.Encoder
   111  
   112  	// owned by the readFrames loop:
   113  	peerSetting map[http2.SettingID]uint32
   114  	hdec        *hpack.Decoder
   115  }
   116  
   117  func main() {
   118  	flag.Usage = usage
   119  	flag.Parse()
   120  	if flag.NArg() != 1 {
   121  		usage()
   122  		os.Exit(2)
   123  	}
   124  	log.SetFlags(0)
   125  
   126  	host := flag.Arg(0)
   127  	app := &h2i{
   128  		host:        host,
   129  		peerSetting: make(map[http2.SettingID]uint32),
   130  	}
   131  	app.henc = hpack.NewEncoder(&app.hbuf)
   132  
   133  	if err := app.Main(); err != nil {
   134  		if app.term != nil {
   135  			app.logf("%v\n", err)
   136  		} else {
   137  			fmt.Fprintf(os.Stderr, "%v\n", err)
   138  		}
   139  		os.Exit(1)
   140  	}
   141  	fmt.Fprintf(os.Stdout, "\n")
   142  }
   143  
   144  func (app *h2i) Main() error {
   145  	cfg := &tls.Config{
   146  		ServerName:         withoutPort(app.host),
   147  		NextProtos:         strings.Split(*flagNextProto, ","),
   148  		InsecureSkipVerify: *flagInsecure,
   149  	}
   150  
   151  	hostAndPort := *flagDial
   152  	if hostAndPort == "" {
   153  		hostAndPort = withPort(app.host)
   154  	}
   155  	log.Printf("Connecting to %s ...", hostAndPort)
   156  	tc, err := tls.Dial("tcp", hostAndPort, cfg)
   157  	if err != nil {
   158  		return fmt.Errorf("Error dialing %s: %v", hostAndPort, err)
   159  	}
   160  	log.Printf("Connected to %v", tc.RemoteAddr())
   161  	defer tc.Close()
   162  
   163  	if err := tc.Handshake(); err != nil {
   164  		return fmt.Errorf("TLS handshake: %v", err)
   165  	}
   166  	if !*flagInsecure {
   167  		if err := tc.VerifyHostname(app.host); err != nil {
   168  			return fmt.Errorf("VerifyHostname: %v", err)
   169  		}
   170  	}
   171  	state := tc.ConnectionState()
   172  	log.Printf("Negotiated protocol %q", state.NegotiatedProtocol)
   173  	if !state.NegotiatedProtocolIsMutual || state.NegotiatedProtocol == "" {
   174  		return fmt.Errorf("Could not negotiate protocol mutually")
   175  	}
   176  
   177  	if _, err := io.WriteString(tc, http2.ClientPreface); err != nil {
   178  		return err
   179  	}
   180  
   181  	app.framer = http2.NewFramer(tc, tc)
   182  
   183  	oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
   184  	if err != nil {
   185  		return err
   186  	}
   187  	defer terminal.Restore(0, oldState)
   188  
   189  	var screen = struct {
   190  		io.Reader
   191  		io.Writer
   192  	}{os.Stdin, os.Stdout}
   193  
   194  	app.term = terminal.NewTerminal(screen, "h2i> ")
   195  	lastWord := regexp.MustCompile(`.+\W(\w+)$`)
   196  	app.term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
   197  		if key != '\t' {
   198  			return
   199  		}
   200  		if pos != len(line) {
   201  			// TODO: we're being lazy for now, only supporting tab completion at the end.
   202  			return
   203  		}
   204  		// Auto-complete for the command itself.
   205  		if !strings.Contains(line, " ") {
   206  			var name string
   207  			name, _, ok = lookupCommand(line)
   208  			if !ok {
   209  				return
   210  			}
   211  			return name, len(name), true
   212  		}
   213  		_, c, ok := lookupCommand(line[:strings.IndexByte(line, ' ')])
   214  		if !ok || c.complete == nil {
   215  			return
   216  		}
   217  		if strings.HasSuffix(line, " ") {
   218  			app.logf("%s", strings.Join(c.complete(), " "))
   219  			return line, pos, true
   220  		}
   221  		m := lastWord.FindStringSubmatch(line)
   222  		if m == nil {
   223  			return line, len(line), true
   224  		}
   225  		soFar := m[1]
   226  		var match []string
   227  		for _, cand := range c.complete() {
   228  			if len(soFar) > len(cand) || !strings.EqualFold(cand[:len(soFar)], soFar) {
   229  				continue
   230  			}
   231  			match = append(match, cand)
   232  		}
   233  		if len(match) == 0 {
   234  			return
   235  		}
   236  		if len(match) > 1 {
   237  			// TODO: auto-complete any common prefix
   238  			app.logf("%s", strings.Join(match, " "))
   239  			return line, pos, true
   240  		}
   241  		newLine = line[:len(line)-len(soFar)] + match[0]
   242  		return newLine, len(newLine), true
   243  
   244  	}
   245  
   246  	errc := make(chan error, 2)
   247  	go func() { errc <- app.readFrames() }()
   248  	go func() { errc <- app.readConsole() }()
   249  	return <-errc
   250  }
   251  
   252  func (app *h2i) logf(format string, args ...interface{}) {
   253  	fmt.Fprintf(app.term, format+"\r\n", args...)
   254  }
   255  
   256  func (app *h2i) readConsole() error {
   257  	if s := *flagSettings; s != "omit" {
   258  		var args []string
   259  		if s != "empty" {
   260  			args = strings.Split(s, ",")
   261  		}
   262  		_, c, ok := lookupCommand("settings")
   263  		if !ok {
   264  			panic("settings command not found")
   265  		}
   266  		c.run(app, args)
   267  	}
   268  
   269  	for {
   270  		line, err := app.term.ReadLine()
   271  		if err == io.EOF {
   272  			return nil
   273  		}
   274  		if err != nil {
   275  			return fmt.Errorf("terminal.ReadLine: %v", err)
   276  		}
   277  		f := strings.Fields(line)
   278  		if len(f) == 0 {
   279  			continue
   280  		}
   281  		cmd, args := f[0], f[1:]
   282  		if _, c, ok := lookupCommand(cmd); ok {
   283  			err = c.run(app, args)
   284  		} else {
   285  			app.logf("Unknown command %q", line)
   286  		}
   287  		if err == errExitApp {
   288  			return nil
   289  		}
   290  		if err != nil {
   291  			return err
   292  		}
   293  	}
   294  }
   295  
   296  func lookupCommand(prefix string) (name string, c command, ok bool) {
   297  	prefix = strings.ToLower(prefix)
   298  	if c, ok = commands[prefix]; ok {
   299  		return prefix, c, ok
   300  	}
   301  
   302  	for full, candidate := range commands {
   303  		if strings.HasPrefix(full, prefix) {
   304  			if c.run != nil {
   305  				return "", command{}, false // ambiguous
   306  			}
   307  			c = candidate
   308  			name = full
   309  		}
   310  	}
   311  	return name, c, c.run != nil
   312  }
   313  
   314  var errExitApp = errors.New("internal sentinel error value to quit the console reading loop")
   315  
   316  func (a *h2i) cmdQuit(args []string) error {
   317  	if len(args) > 0 {
   318  		a.logf("the QUIT command takes no argument")
   319  		return nil
   320  	}
   321  	return errExitApp
   322  }
   323  
   324  func (a *h2i) cmdSettings(args []string) error {
   325  	if len(args) == 1 && strings.EqualFold(args[0], "ACK") {
   326  		return a.framer.WriteSettingsAck()
   327  	}
   328  	var settings []http2.Setting
   329  	for _, arg := range args {
   330  		if strings.EqualFold(arg, "ACK") {
   331  			a.logf("Error: ACK must be only argument with the SETTINGS command")
   332  			return nil
   333  		}
   334  		eq := strings.Index(arg, "=")
   335  		if eq == -1 {
   336  			a.logf("Error: invalid argument %q (expected SETTING_NAME=nnnn)", arg)
   337  			return nil
   338  		}
   339  		sid, ok := settingByName(arg[:eq])
   340  		if !ok {
   341  			a.logf("Error: unknown setting name %q", arg[:eq])
   342  			return nil
   343  		}
   344  		val, err := strconv.ParseUint(arg[eq+1:], 10, 32)
   345  		if err != nil {
   346  			a.logf("Error: invalid argument %q (expected SETTING_NAME=nnnn)", arg)
   347  			return nil
   348  		}
   349  		settings = append(settings, http2.Setting{
   350  			ID:  sid,
   351  			Val: uint32(val),
   352  		})
   353  	}
   354  	a.logf("Sending: %v", settings)
   355  	return a.framer.WriteSettings(settings...)
   356  }
   357  
   358  func settingByName(name string) (http2.SettingID, bool) {
   359  	for _, sid := range [...]http2.SettingID{
   360  		http2.SettingHeaderTableSize,
   361  		http2.SettingEnablePush,
   362  		http2.SettingMaxConcurrentStreams,
   363  		http2.SettingInitialWindowSize,
   364  		http2.SettingMaxFrameSize,
   365  		http2.SettingMaxHeaderListSize,
   366  	} {
   367  		if strings.EqualFold(sid.String(), name) {
   368  			return sid, true
   369  		}
   370  	}
   371  	return 0, false
   372  }
   373  
   374  func (app *h2i) cmdPing(args []string) error {
   375  	if len(args) > 1 {
   376  		app.logf("invalid PING usage: only accepts 0 or 1 args")
   377  		return nil // nil means don't end the program
   378  	}
   379  	var data [8]byte
   380  	if len(args) == 1 {
   381  		copy(data[:], args[0])
   382  	} else {
   383  		copy(data[:], "h2i_ping")
   384  	}
   385  	return app.framer.WritePing(false, data)
   386  }
   387  
   388  func (app *h2i) cmdHeaders(args []string) error {
   389  	if len(args) > 0 {
   390  		app.logf("Error: HEADERS doesn't yet take arguments.")
   391  		// TODO: flags for restricting window size, to force CONTINUATION
   392  		// frames.
   393  		return nil
   394  	}
   395  	var h1req bytes.Buffer
   396  	app.term.SetPrompt("(as HTTP/1.1)> ")
   397  	defer app.term.SetPrompt("h2i> ")
   398  	for {
   399  		line, err := app.term.ReadLine()
   400  		if err != nil {
   401  			return err
   402  		}
   403  		h1req.WriteString(line)
   404  		h1req.WriteString("\r\n")
   405  		if line == "" {
   406  			break
   407  		}
   408  	}
   409  	req, err := http.ReadRequest(bufio.NewReader(&h1req))
   410  	if err != nil {
   411  		app.logf("Invalid HTTP/1.1 request: %v", err)
   412  		return nil
   413  	}
   414  	if app.streamID == 0 {
   415  		app.streamID = 1
   416  	} else {
   417  		app.streamID += 2
   418  	}
   419  	app.logf("Opening Stream-ID %d:", app.streamID)
   420  	hbf := app.encodeHeaders(req)
   421  	if len(hbf) > 16<<10 {
   422  		app.logf("TODO: h2i doesn't yet write CONTINUATION frames. Copy it from transport.go")
   423  		return nil
   424  	}
   425  	return app.framer.WriteHeaders(http2.HeadersFrameParam{
   426  		StreamID:      app.streamID,
   427  		BlockFragment: hbf,
   428  		EndStream:     req.Method == "GET" || req.Method == "HEAD", // good enough for now
   429  		EndHeaders:    true,                                        // for now
   430  	})
   431  }
   432  
   433  func (app *h2i) readFrames() error {
   434  	for {
   435  		f, err := app.framer.ReadFrame()
   436  		if err != nil {
   437  			return fmt.Errorf("ReadFrame: %v", err)
   438  		}
   439  		app.logf("%v", f)
   440  		switch f := f.(type) {
   441  		case *http2.PingFrame:
   442  			app.logf("  Data = %q", f.Data)
   443  		case *http2.SettingsFrame:
   444  			f.ForeachSetting(func(s http2.Setting) error {
   445  				app.logf("  %v", s)
   446  				app.peerSetting[s.ID] = s.Val
   447  				return nil
   448  			})
   449  		case *http2.WindowUpdateFrame:
   450  			app.logf("  Window-Increment = %v", f.Increment)
   451  		case *http2.GoAwayFrame:
   452  			app.logf("  Last-Stream-ID = %d; Error-Code = %v (%d)", f.LastStreamID, f.ErrCode, f.ErrCode)
   453  		case *http2.DataFrame:
   454  			app.logf("  %q", f.Data())
   455  		case *http2.HeadersFrame:
   456  			if f.HasPriority() {
   457  				app.logf("  PRIORITY = %v", f.Priority)
   458  			}
   459  			if app.hdec == nil {
   460  				// TODO: if the user uses h2i to send a SETTINGS frame advertising
   461  				// something larger, we'll need to respect SETTINGS_HEADER_TABLE_SIZE
   462  				// and stuff here instead of using the 4k default. But for now:
   463  				tableSize := uint32(4 << 10)
   464  				app.hdec = hpack.NewDecoder(tableSize, app.onNewHeaderField)
   465  			}
   466  			app.hdec.Write(f.HeaderBlockFragment())
   467  		case *http2.PushPromiseFrame:
   468  			if app.hdec == nil {
   469  				// TODO: if the user uses h2i to send a SETTINGS frame advertising
   470  				// something larger, we'll need to respect SETTINGS_HEADER_TABLE_SIZE
   471  				// and stuff here instead of using the 4k default. But for now:
   472  				tableSize := uint32(4 << 10)
   473  				app.hdec = hpack.NewDecoder(tableSize, app.onNewHeaderField)
   474  			}
   475  			app.hdec.Write(f.HeaderBlockFragment())
   476  		}
   477  	}
   478  }
   479  
   480  // called from readLoop
   481  func (app *h2i) onNewHeaderField(f hpack.HeaderField) {
   482  	if f.Sensitive {
   483  		app.logf("  %s = %q (SENSITIVE)", f.Name, f.Value)
   484  	}
   485  	app.logf("  %s = %q", f.Name, f.Value)
   486  }
   487  
   488  func (app *h2i) encodeHeaders(req *http.Request) []byte {
   489  	app.hbuf.Reset()
   490  
   491  	// TODO(bradfitz): figure out :authority-vs-Host stuff between http2 and Go
   492  	host := req.Host
   493  	if host == "" {
   494  		host = req.URL.Host
   495  	}
   496  
   497  	path := req.RequestURI
   498  	if path == "" {
   499  		path = "/"
   500  	}
   501  
   502  	app.writeHeader(":authority", host) // probably not right for all sites
   503  	app.writeHeader(":method", req.Method)
   504  	app.writeHeader(":path", path)
   505  	app.writeHeader(":scheme", "https")
   506  
   507  	for k, vv := range req.Header {
   508  		lowKey := strings.ToLower(k)
   509  		if lowKey == "host" {
   510  			continue
   511  		}
   512  		for _, v := range vv {
   513  			app.writeHeader(lowKey, v)
   514  		}
   515  	}
   516  	return app.hbuf.Bytes()
   517  }
   518  
   519  func (app *h2i) writeHeader(name, value string) {
   520  	app.henc.WriteField(hpack.HeaderField{Name: name, Value: value})
   521  	app.logf(" %s = %s", name, value)
   522  }