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// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows
7
8/*
9The h2i command is an interactive HTTP/2 console.
10
11Usage:
12  $ h2i [flags] <hostname>
13
14Interactive 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*/
21package main
22
23import (
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
45var (
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
52type 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
60var 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
80func 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.
86func 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.
94func 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.
102type 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
118func 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
145func (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
253func (app *h2i) logf(format string, args ...interface{}) {
254	fmt.Fprintf(app.term, format+"\r\n", args...)
255}
256
257func (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
297func 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
315var errExitApp = errors.New("internal sentinel error value to quit the console reading loop")
316
317func (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
325func (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
359func 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
375func (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
389func (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
434func (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
482func (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
489func (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
520func (app *h2i) writeHeader(name, value string) {
521	app.henc.WriteField(hpack.HeaderField{Name: name, Value: value})
522	app.logf(" %s = %s", name, value)
523}
524