github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/bindings/containers/attach.go (about) 1 package containers 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/binary" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "net/url" 12 "os" 13 "reflect" 14 "strconv" 15 "time" 16 17 "github.com/hanks177/podman/v4/libpod/define" 18 "github.com/hanks177/podman/v4/pkg/bindings" 19 "github.com/hanks177/podman/v4/utils" 20 "github.com/moby/term" 21 "github.com/pkg/errors" 22 "github.com/sirupsen/logrus" 23 terminal "golang.org/x/term" 24 ) 25 26 // The CloseWriter interface is used to determine whether we can do a one-sided 27 // close of a hijacked connection. 28 type CloseWriter interface { 29 CloseWrite() error 30 } 31 32 // Attach attaches to a running container 33 func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Writer, stderr io.Writer, attachReady chan bool, options *AttachOptions) error { 34 if options == nil { 35 options = new(AttachOptions) 36 } 37 isSet := struct { 38 stdin bool 39 stdout bool 40 stderr bool 41 }{ 42 stdin: !(stdin == nil || reflect.ValueOf(stdin).IsNil()), 43 stdout: !(stdout == nil || reflect.ValueOf(stdout).IsNil()), 44 stderr: !(stderr == nil || reflect.ValueOf(stderr).IsNil()), 45 } 46 // Ensure golang can determine that interfaces are "really" nil 47 if !isSet.stdin { 48 stdin = (io.Reader)(nil) 49 } 50 if !isSet.stdout { 51 stdout = (io.Writer)(nil) 52 } 53 if !isSet.stderr { 54 stderr = (io.Writer)(nil) 55 } 56 57 logrus.Infof("Going to attach to container %q", nameOrID) 58 59 conn, err := bindings.GetClient(ctx) 60 if err != nil { 61 return err 62 } 63 64 // Do we need to wire in stdin? 65 ctnr, err := Inspect(ctx, nameOrID, new(InspectOptions).WithSize(false)) 66 if err != nil { 67 return err 68 } 69 70 params, err := options.ToParams() 71 if err != nil { 72 return err 73 } 74 detachKeysInBytes := []byte{} 75 if options.Changed("DetachKeys") { 76 params.Add("detachKeys", options.GetDetachKeys()) 77 78 detachKeysInBytes, err = term.ToBytes(options.GetDetachKeys()) 79 if err != nil { 80 return errors.Wrapf(err, "invalid detach keys") 81 } 82 } 83 if isSet.stdin { 84 params.Add("stdin", "true") 85 } 86 if isSet.stdout { 87 params.Add("stdout", "true") 88 } 89 if isSet.stderr { 90 params.Add("stderr", "true") 91 } 92 93 // Unless all requirements are met, don't use "stdin" is a terminal 94 file, ok := stdin.(*os.File) 95 outFile, outOk := stdout.(*os.File) 96 needTTY := ok && outOk && terminal.IsTerminal(int(file.Fd())) && ctnr.Config.Tty 97 if needTTY { 98 state, err := setRawTerminal(file) 99 if err != nil { 100 return err 101 } 102 defer func() { 103 if err := terminal.Restore(int(file.Fd()), state); err != nil { 104 logrus.Errorf("Unable to restore terminal: %q", err) 105 } 106 logrus.SetFormatter(&logrus.TextFormatter{}) 107 }() 108 } 109 110 headers := make(http.Header) 111 headers.Add("Connection", "Upgrade") 112 headers.Add("Upgrade", "tcp") 113 114 var socket net.Conn 115 socketSet := false 116 dialContext := conn.Client.Transport.(*http.Transport).DialContext 117 t := &http.Transport{ 118 DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { 119 c, err := dialContext(ctx, network, address) 120 if err != nil { 121 return nil, err 122 } 123 if !socketSet { 124 socket = c 125 socketSet = true 126 } 127 return c, err 128 }, 129 IdleConnTimeout: time.Duration(0), 130 } 131 conn.Client.Transport = t 132 response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/containers/%s/attach", params, headers, nameOrID) 133 if err != nil { 134 return err 135 } 136 137 if !(response.IsSuccess() || response.IsInformational()) { 138 defer response.Body.Close() 139 return response.Process(nil) 140 } 141 142 if needTTY { 143 winChange := make(chan os.Signal, 1) 144 winCtx, winCancel := context.WithCancel(ctx) 145 defer winCancel() 146 notifyWinChange(winCtx, winChange, file, outFile) 147 attachHandleResize(ctx, winCtx, winChange, false, nameOrID, file, outFile) 148 } 149 150 // If we are attaching around a start, we need to "signal" 151 // back that we are in fact attached so that started does 152 // not execute before we can attach. 153 if attachReady != nil { 154 attachReady <- true 155 } 156 157 stdoutChan := make(chan error) 158 stdinChan := make(chan error, 1) // stdin channel should not block 159 160 if isSet.stdin { 161 go func() { 162 logrus.Debugf("Copying STDIN to socket") 163 164 _, err := utils.CopyDetachable(socket, stdin, detachKeysInBytes) 165 if err != nil && err != define.ErrDetach { 166 logrus.Errorf("Failed to write input to service: %v", err) 167 } 168 if err == nil { 169 if closeWrite, ok := socket.(CloseWriter); ok { 170 if err := closeWrite.CloseWrite(); err != nil { 171 logrus.Warnf("Failed to close STDIN for writing: %v", err) 172 } 173 } 174 } 175 stdinChan <- err 176 }() 177 } 178 179 buffer := make([]byte, 1024) 180 if ctnr.Config.Tty { 181 go func() { 182 logrus.Debugf("Copying STDOUT of container in terminal mode") 183 184 if !isSet.stdout { 185 stdoutChan <- fmt.Errorf("container %q requires stdout to be set", ctnr.ID) 186 } 187 // If not multiplex'ed, read from server and write to stdout 188 _, err := io.Copy(stdout, socket) 189 190 stdoutChan <- err 191 }() 192 193 for { 194 select { 195 case err := <-stdoutChan: 196 if err != nil { 197 return err 198 } 199 200 return nil 201 case err := <-stdinChan: 202 if err != nil { 203 return err 204 } 205 206 return nil 207 } 208 } 209 } else { 210 logrus.Debugf("Copying standard streams of container %q in non-terminal mode", ctnr.ID) 211 for { 212 // Read multiplexed channels and write to appropriate stream 213 fd, l, err := DemuxHeader(socket, buffer) 214 if err != nil { 215 if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { 216 return nil 217 } 218 return err 219 } 220 frame, err := DemuxFrame(socket, buffer, l) 221 if err != nil { 222 return err 223 } 224 225 switch { 226 case fd == 0: 227 if isSet.stdout { 228 if _, err := stdout.Write(frame[0:l]); err != nil { 229 return err 230 } 231 } 232 case fd == 1: 233 if isSet.stdout { 234 if _, err := stdout.Write(frame[0:l]); err != nil { 235 return err 236 } 237 } 238 case fd == 2: 239 if isSet.stderr { 240 if _, err := stderr.Write(frame[0:l]); err != nil { 241 return err 242 } 243 } 244 case fd == 3: 245 return fmt.Errorf("from service from stream: %s", frame) 246 default: 247 return fmt.Errorf("unrecognized channel '%d' in header, 0-3 supported", fd) 248 } 249 } 250 } 251 } 252 253 // DemuxHeader reads header for stream from server multiplexed stdin/stdout/stderr/2nd error channel 254 func DemuxHeader(r io.Reader, buffer []byte) (fd, sz int, err error) { 255 n, err := io.ReadFull(r, buffer[0:8]) 256 if err != nil { 257 return 258 } 259 if n < 8 { 260 err = io.ErrUnexpectedEOF 261 return 262 } 263 264 fd = int(buffer[0]) 265 if fd < 0 || fd > 3 { 266 err = errors.Wrapf(ErrLostSync, fmt.Sprintf(`channel "%d" found, 0-3 supported`, fd)) 267 return 268 } 269 270 sz = int(binary.BigEndian.Uint32(buffer[4:8])) 271 return 272 } 273 274 // DemuxFrame reads contents for frame from server multiplexed stdin/stdout/stderr/2nd error channel 275 func DemuxFrame(r io.Reader, buffer []byte, length int) (frame []byte, err error) { 276 if len(buffer) < length { 277 buffer = append(buffer, make([]byte, length-len(buffer)+1)...) 278 } 279 280 n, err := io.ReadFull(r, buffer[0:length]) 281 if err != nil { 282 return nil, err 283 } 284 if n < length { 285 err = io.ErrUnexpectedEOF 286 return 287 } 288 289 return buffer[0:length], nil 290 } 291 292 // ResizeContainerTTY sets container's TTY height and width in characters 293 func ResizeContainerTTY(ctx context.Context, nameOrID string, options *ResizeTTYOptions) error { 294 if options == nil { 295 options = new(ResizeTTYOptions) 296 } 297 return resizeTTY(ctx, bindings.JoinURL("containers", nameOrID, "resize"), options.Height, options.Width) 298 } 299 300 // ResizeExecTTY sets session's TTY height and width in characters 301 func ResizeExecTTY(ctx context.Context, nameOrID string, options *ResizeExecTTYOptions) error { 302 if options == nil { 303 options = new(ResizeExecTTYOptions) 304 } 305 return resizeTTY(ctx, bindings.JoinURL("exec", nameOrID, "resize"), options.Height, options.Width) 306 } 307 308 // resizeTTY set size of TTY of container 309 func resizeTTY(ctx context.Context, endpoint string, height *int, width *int) error { 310 conn, err := bindings.GetClient(ctx) 311 if err != nil { 312 return err 313 } 314 315 params := url.Values{} 316 if height != nil { 317 params.Set("h", strconv.Itoa(*height)) 318 } 319 if width != nil { 320 params.Set("w", strconv.Itoa(*width)) 321 } 322 params.Set("running", "true") 323 rsp, err := conn.DoRequest(ctx, nil, http.MethodPost, endpoint, params, nil) 324 if err != nil { 325 return err 326 } 327 defer rsp.Body.Close() 328 329 return rsp.Process(nil) 330 } 331 332 type rawFormatter struct { 333 logrus.TextFormatter 334 } 335 336 func (f *rawFormatter) Format(entry *logrus.Entry) ([]byte, error) { 337 buffer, err := f.TextFormatter.Format(entry) 338 if err != nil { 339 return buffer, err 340 } 341 return append(buffer, '\r'), nil 342 } 343 344 // This is intended to not be run as a goroutine, handling resizing for a container 345 // or exec session. It will call resize once and then starts a goroutine which calls resize on winChange 346 func attachHandleResize(ctx, winCtx context.Context, winChange chan os.Signal, isExec bool, id string, file *os.File, outFile *os.File) { 347 resize := func() { 348 w, h, err := getTermSize(file, outFile) 349 if err != nil { 350 logrus.Warnf("Failed to obtain TTY size: %v", err) 351 } 352 353 var resizeErr error 354 if isExec { 355 resizeErr = ResizeExecTTY(ctx, id, new(ResizeExecTTYOptions).WithHeight(h).WithWidth(w)) 356 } else { 357 resizeErr = ResizeContainerTTY(ctx, id, new(ResizeTTYOptions).WithHeight(h).WithWidth(w)) 358 } 359 if resizeErr != nil { 360 logrus.Infof("Failed to resize TTY: %v", resizeErr) 361 } 362 } 363 364 resize() 365 366 go func() { 367 for { 368 select { 369 case <-winCtx.Done(): 370 return 371 case <-winChange: 372 resize() 373 } 374 } 375 }() 376 } 377 378 // Configure the given terminal for raw mode 379 func setRawTerminal(file *os.File) (*terminal.State, error) { 380 state, err := makeRawTerm(file) 381 if err != nil { 382 return nil, err 383 } 384 385 logrus.SetFormatter(&rawFormatter{}) 386 387 return state, err 388 } 389 390 // ExecStartAndAttach starts and attaches to a given exec session. 391 func ExecStartAndAttach(ctx context.Context, sessionID string, options *ExecStartAndAttachOptions) error { 392 if options == nil { 393 options = new(ExecStartAndAttachOptions) 394 } 395 conn, err := bindings.GetClient(ctx) 396 if err != nil { 397 return err 398 } 399 400 // TODO: Make this configurable (can't use streams' InputStream as it's 401 // buffered) 402 terminalFile := os.Stdin 403 terminalOutFile := os.Stdout 404 405 logrus.Debugf("Starting & Attaching to exec session ID %q", sessionID) 406 407 // We need to inspect the exec session first to determine whether to use 408 // -t. 409 resp, err := conn.DoRequest(ctx, nil, http.MethodGet, "/exec/%s/json", nil, nil, sessionID) 410 if err != nil { 411 return err 412 } 413 defer resp.Body.Close() 414 415 respStruct := new(define.InspectExecSession) 416 if err := resp.Process(respStruct); err != nil { 417 return err 418 } 419 isTerm := true 420 if respStruct.ProcessConfig != nil { 421 isTerm = respStruct.ProcessConfig.Tty 422 } 423 424 // If we are in TTY mode, we need to set raw mode for the terminal. 425 // TODO: Share all of this with Attach() for containers. 426 needTTY := terminalFile != nil && terminal.IsTerminal(int(terminalFile.Fd())) && isTerm 427 428 body := struct { 429 Detach bool `json:"Detach"` 430 TTY bool `json:"Tty"` 431 Height uint16 `json:"h"` 432 Width uint16 `json:"w"` 433 }{ 434 Detach: false, 435 TTY: needTTY, 436 } 437 438 if needTTY { 439 state, err := setRawTerminal(terminalFile) 440 if err != nil { 441 return err 442 } 443 defer func() { 444 if err := terminal.Restore(int(terminalFile.Fd()), state); err != nil { 445 logrus.Errorf("Unable to restore terminal: %q", err) 446 } 447 logrus.SetFormatter(&logrus.TextFormatter{}) 448 }() 449 w, h, err := getTermSize(terminalFile, terminalOutFile) 450 if err != nil { 451 logrus.Warnf("Failed to obtain TTY size: %v", err) 452 } 453 body.Width = uint16(w) 454 body.Height = uint16(h) 455 } 456 457 bodyJSON, err := json.Marshal(body) 458 if err != nil { 459 return err 460 } 461 462 var socket net.Conn 463 socketSet := false 464 dialContext := conn.Client.Transport.(*http.Transport).DialContext 465 t := &http.Transport{ 466 DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { 467 c, err := dialContext(ctx, network, address) 468 if err != nil { 469 return nil, err 470 } 471 if !socketSet { 472 socket = c 473 socketSet = true 474 } 475 return c, err 476 }, 477 IdleConnTimeout: time.Duration(0), 478 } 479 conn.Client.Transport = t 480 response, err := conn.DoRequest(ctx, bytes.NewReader(bodyJSON), http.MethodPost, "/exec/%s/start", nil, nil, sessionID) 481 if err != nil { 482 return err 483 } 484 defer response.Body.Close() 485 486 if !(response.IsSuccess() || response.IsInformational()) { 487 return response.Process(nil) 488 } 489 490 if needTTY { 491 winChange := make(chan os.Signal, 1) 492 winCtx, winCancel := context.WithCancel(ctx) 493 defer winCancel() 494 495 notifyWinChange(winCtx, winChange, terminalFile, terminalOutFile) 496 attachHandleResize(ctx, winCtx, winChange, true, sessionID, terminalFile, terminalOutFile) 497 } 498 499 if options.GetAttachInput() { 500 go func() { 501 logrus.Debugf("Copying STDIN to socket") 502 _, err := utils.CopyDetachable(socket, options.InputStream, []byte{}) 503 if err != nil { 504 logrus.Errorf("Failed to write input to service: %v", err) 505 } 506 507 if closeWrite, ok := socket.(CloseWriter); ok { 508 logrus.Debugf("Closing STDIN") 509 if err := closeWrite.CloseWrite(); err != nil { 510 logrus.Warnf("Failed to close STDIN for writing: %v", err) 511 } 512 } 513 }() 514 } 515 516 buffer := make([]byte, 1024) 517 if isTerm { 518 logrus.Debugf("Handling terminal attach to exec") 519 if !options.GetAttachOutput() { 520 return fmt.Errorf("exec session %s has a terminal and must have STDOUT enabled", sessionID) 521 } 522 // If not multiplex'ed, read from server and write to stdout 523 _, err := utils.CopyDetachable(options.GetOutputStream(), socket, []byte{}) 524 if err != nil { 525 return err 526 } 527 } else { 528 logrus.Debugf("Handling non-terminal attach to exec") 529 for { 530 // Read multiplexed channels and write to appropriate stream 531 fd, l, err := DemuxHeader(socket, buffer) 532 if err != nil { 533 if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { 534 return nil 535 } 536 return err 537 } 538 frame, err := DemuxFrame(socket, buffer, l) 539 if err != nil { 540 return err 541 } 542 543 switch { 544 case fd == 0: 545 if options.GetAttachInput() { 546 // Write STDIN to STDOUT (echoing characters 547 // typed by another attach session) 548 if _, err := options.GetOutputStream().Write(frame[0:l]); err != nil { 549 return err 550 } 551 } 552 case fd == 1: 553 if options.GetAttachOutput() { 554 if _, err := options.GetOutputStream().Write(frame[0:l]); err != nil { 555 return err 556 } 557 } 558 case fd == 2: 559 if options.GetAttachError() { 560 if _, err := options.GetErrorStream().Write(frame[0:l]); err != nil { 561 return err 562 } 563 } 564 case fd == 3: 565 return fmt.Errorf("from service from stream: %s", frame) 566 default: 567 return fmt.Errorf("unrecognized channel '%d' in header, 0-3 supported", fd) 568 } 569 } 570 } 571 return nil 572 }