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.
5 // +build !plan9,!solaris
8 The h2i command is an interactive HTTP/2 console.
11 $ h2i [flags] <hostname>
13 Interactive commands in the console: (all parts case-insensitive)
18 headers (open a new stream by typing HTTP/1.1)
38 "golang.org/x/crypto/ssh/terminal"
39 "golang.org/x/net/http2"
40 "golang.org/x/net/http2/hpack"
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)")
52 run func(*h2i, []string) error // required
54 // complete optionally specifies tokens (case-insensitive) which are
55 // valid for this subcommand.
56 complete func() []string
59 var commands = map[string]command{
60 "ping": {run: (*h2i).cmdPing},
62 run: (*h2i).cmdSettings,
63 complete: func() []string {
66 http2.SettingHeaderTableSize.String(),
67 http2.SettingEnablePush.String(),
68 http2.SettingMaxConcurrentStreams.String(),
69 http2.SettingInitialWindowSize.String(),
70 http2.SettingMaxFrameSize.String(),
71 http2.SettingMaxHeaderListSize.String(),
75 "quit": {run: (*h2i).cmdQuit},
76 "headers": {run: (*h2i).cmdHeaders},
80 fmt.Fprintf(os.Stderr, "Usage: h2i <hostname>\n\n")
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")
92 // withoutPort strips the port from addr if present.
93 func withoutPort(addr string) string {
94 if h, _, err := net.SplitHostPort(addr); err == nil {
100 // h2i is the app's state.
105 term *terminal.Terminal
107 // owned by the command loop:
112 // owned by the readFrames loop:
113 peerSetting map[http2.SettingID]uint32
120 if flag.NArg() != 1 {
129 peerSetting: make(map[http2.SettingID]uint32),
131 app.henc = hpack.NewEncoder(&app.hbuf)
133 if err := app.Main(); err != nil {
135 app.logf("%v\n", err)
137 fmt.Fprintf(os.Stderr, "%v\n", err)
141 fmt.Fprintf(os.Stdout, "\n")
144 func (app *h2i) Main() error {
146 ServerName: withoutPort(app.host),
147 NextProtos: strings.Split(*flagNextProto, ","),
148 InsecureSkipVerify: *flagInsecure,
151 hostAndPort := *flagDial
152 if hostAndPort == "" {
153 hostAndPort = withPort(app.host)
155 log.Printf("Connecting to %s ...", hostAndPort)
156 tc, err := tls.Dial("tcp", hostAndPort, cfg)
158 return fmt.Errorf("Error dialing %s: %v", hostAndPort, err)
160 log.Printf("Connected to %v", tc.RemoteAddr())
163 if err := tc.Handshake(); err != nil {
164 return fmt.Errorf("TLS handshake: %v", err)
167 if err := tc.VerifyHostname(app.host); err != nil {
168 return fmt.Errorf("VerifyHostname: %v", err)
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")
177 if _, err := io.WriteString(tc, http2.ClientPreface); err != nil {
181 app.framer = http2.NewFramer(tc, tc)
183 oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
187 defer terminal.Restore(0, oldState)
189 var screen = struct {
192 }{os.Stdin, os.Stdout}
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) {
200 if pos != len(line) {
201 // TODO: we're being lazy for now, only supporting tab completion at the end.
204 // Auto-complete for the command itself.
205 if !strings.Contains(line, " ") {
207 name, _, ok = lookupCommand(line)
211 return name, len(name), true
213 _, c, ok := lookupCommand(line[:strings.IndexByte(line, ' ')])
214 if !ok || c.complete == nil {
217 if strings.HasSuffix(line, " ") {
218 app.logf("%s", strings.Join(c.complete(), " "))
219 return line, pos, true
221 m := lastWord.FindStringSubmatch(line)
223 return line, len(line), true
227 for _, cand := range c.complete() {
228 if len(soFar) > len(cand) || !strings.EqualFold(cand[:len(soFar)], soFar) {
231 match = append(match, cand)
237 // TODO: auto-complete any common prefix
238 app.logf("%s", strings.Join(match, " "))
239 return line, pos, true
241 newLine = line[:len(line)-len(soFar)] + match[0]
242 return newLine, len(newLine), true
246 errc := make(chan error, 2)
247 go func() { errc <- app.readFrames() }()
248 go func() { errc <- app.readConsole() }()
252 func (app *h2i) logf(format string, args ...interface{}) {
253 fmt.Fprintf(app.term, format+"\r\n", args...)
256 func (app *h2i) readConsole() error {
257 if s := *flagSettings; s != "omit" {
260 args = strings.Split(s, ",")
262 _, c, ok := lookupCommand("settings")
264 panic("settings command not found")
270 line, err := app.term.ReadLine()
275 return fmt.Errorf("terminal.ReadLine: %v", err)
277 f := strings.Fields(line)
281 cmd, args := f[0], f[1:]
282 if _, c, ok := lookupCommand(cmd); ok {
283 err = c.run(app, args)
285 app.logf("Unknown command %q", line)
287 if err == errExitApp {
296 func lookupCommand(prefix string) (name string, c command, ok bool) {
297 prefix = strings.ToLower(prefix)
298 if c, ok = commands[prefix]; ok {
302 for full, candidate := range commands {
303 if strings.HasPrefix(full, prefix) {
305 return "", command{}, false // ambiguous
311 return name, c, c.run != nil
314 var errExitApp = errors.New("internal sentinel error value to quit the console reading loop")
316 func (a *h2i) cmdQuit(args []string) error {
318 a.logf("the QUIT command takes no argument")
324 func (a *h2i) cmdSettings(args []string) error {
325 if len(args) == 1 && strings.EqualFold(args[0], "ACK") {
326 return a.framer.WriteSettingsAck()
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")
334 eq := strings.Index(arg, "=")
336 a.logf("Error: invalid argument %q (expected SETTING_NAME=nnnn)", arg)
339 sid, ok := settingByName(arg[:eq])
341 a.logf("Error: unknown setting name %q", arg[:eq])
344 val, err := strconv.ParseUint(arg[eq+1:], 10, 32)
346 a.logf("Error: invalid argument %q (expected SETTING_NAME=nnnn)", arg)
349 settings = append(settings, http2.Setting{
354 a.logf("Sending: %v", settings)
355 return a.framer.WriteSettings(settings...)
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,
367 if strings.EqualFold(sid.String(), name) {
374 func (app *h2i) cmdPing(args []string) error {
376 app.logf("invalid PING usage: only accepts 0 or 1 args")
377 return nil // nil means don't end the program
381 copy(data[:], args[0])
383 copy(data[:], "h2i_ping")
385 return app.framer.WritePing(false, data)
388 func (app *h2i) cmdHeaders(args []string) error {
390 app.logf("Error: HEADERS doesn't yet take arguments.")
391 // TODO: flags for restricting window size, to force CONTINUATION
395 var h1req bytes.Buffer
396 app.term.SetPrompt("(as HTTP/1.1)> ")
397 defer app.term.SetPrompt("h2i> ")
399 line, err := app.term.ReadLine()
403 h1req.WriteString(line)
404 h1req.WriteString("\r\n")
409 req, err := http.ReadRequest(bufio.NewReader(&h1req))
411 app.logf("Invalid HTTP/1.1 request: %v", err)
414 if app.streamID == 0 {
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")
425 return app.framer.WriteHeaders(http2.HeadersFrameParam{
426 StreamID: app.streamID,
428 EndStream: req.Method == "GET" || req.Method == "HEAD", // good enough for now
429 EndHeaders: true, // for now
433 func (app *h2i) readFrames() error {
435 f, err := app.framer.ReadFrame()
437 return fmt.Errorf("ReadFrame: %v", err)
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 {
446 app.peerSetting[s.ID] = s.Val
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:
457 app.logf(" PRIORITY = %v", f.Priority)
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)
466 app.hdec.Write(f.HeaderBlockFragment())
467 case *http2.PushPromiseFrame:
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)
475 app.hdec.Write(f.HeaderBlockFragment())
480 // called from readLoop
481 func (app *h2i) onNewHeaderField(f hpack.HeaderField) {
483 app.logf(" %s = %q (SENSITIVE)", f.Name, f.Value)
485 app.logf(" %s = %q", f.Name, f.Value)
488 func (app *h2i) encodeHeaders(req *http.Request) []byte {
491 // TODO(bradfitz): figure out :authority-vs-Host stuff between http2 and Go
497 path := req.RequestURI
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")
507 for k, vv := range req.Header {
508 lowKey := strings.ToLower(k)
509 if lowKey == "host" {
512 for _, v := range vv {
513 app.writeHeader(lowKey, v)
516 return app.hbuf.Bytes()
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)