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 }