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 !plan9,!solaris
6
7/*
8The h2i command is an interactive HTTP/2 console.
9
10Usage:
11  $ h2i [flags] <hostname>
12
13Interactive 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*/
20package main
21
22import (
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	"golang.org/x/crypto/ssh/terminal"
39	"golang.org/x/net/http2"
40	"golang.org/x/net/http2/hpack"
41)
42
43// Flags
44var (
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
51type 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
59var 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
79func 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.
85func 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.
93func 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.
101type 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
117func 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
144func (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
252func (app *h2i) logf(format string, args ...interface{}) {
253	fmt.Fprintf(app.term, format+"\r\n", args...)
254}
255
256func (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
296func 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
314var errExitApp = errors.New("internal sentinel error value to quit the console reading loop")
315
316func (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
324func (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
358func 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
374func (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
388func (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
433func (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
481func (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
488func (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
519func (app *h2i) writeHeader(name, value string) {
520	app.henc.WriteField(hpack.HeaderField{Name: name, Value: value})
521	app.logf(" %s = %s", name, value)
522}
523