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 }