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