github.com/rothwerx/packer@v0.9.0/communicator/ssh/communicator.go (about) 1 package ssh 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "net" 12 "os" 13 "path/filepath" 14 "strconv" 15 "strings" 16 "sync" 17 "time" 18 19 "github.com/mitchellh/packer/packer" 20 "github.com/pkg/sftp" 21 "golang.org/x/crypto/ssh" 22 "golang.org/x/crypto/ssh/agent" 23 ) 24 25 // ErrHandshakeTimeout is returned from New() whenever we're unable to establish 26 // an ssh connection within a certain timeframe. By default the handshake time- 27 // out period is 1 minute. You can change it with Config.HandshakeTimeout. 28 var ErrHandshakeTimeout = fmt.Errorf("Timeout during SSH handshake") 29 30 type comm struct { 31 client *ssh.Client 32 config *Config 33 conn net.Conn 34 address string 35 } 36 37 // Config is the structure used to configure the SSH communicator. 38 type Config struct { 39 // The configuration of the Go SSH connection 40 SSHConfig *ssh.ClientConfig 41 42 // Connection returns a new connection. The current connection 43 // in use will be closed as part of the Close method, or in the 44 // case an error occurs. 45 Connection func() (net.Conn, error) 46 47 // Pty, if true, will request a pty from the remote end. 48 Pty bool 49 50 // DisableAgent, if true, will not forward the SSH agent. 51 DisableAgent bool 52 53 // HandshakeTimeout limits the amount of time we'll wait to handshake before 54 // saying the connection failed. 55 HandshakeTimeout time.Duration 56 57 // UseSftp, if true, sftp will be used instead of scp for file transfers 58 UseSftp bool 59 } 60 61 // Creates a new packer.Communicator implementation over SSH. This takes 62 // an already existing TCP connection and SSH configuration. 63 func New(address string, config *Config) (result *comm, err error) { 64 // Establish an initial connection and connect 65 result = &comm{ 66 config: config, 67 address: address, 68 } 69 70 if err = result.reconnect(); err != nil { 71 result = nil 72 return 73 } 74 75 return 76 } 77 78 func (c *comm) Start(cmd *packer.RemoteCmd) (err error) { 79 session, err := c.newSession() 80 if err != nil { 81 return 82 } 83 84 // Setup our session 85 session.Stdin = cmd.Stdin 86 session.Stdout = cmd.Stdout 87 session.Stderr = cmd.Stderr 88 89 if c.config.Pty { 90 // Request a PTY 91 termModes := ssh.TerminalModes{ 92 ssh.ECHO: 0, // do not echo 93 ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 94 ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 95 } 96 97 if err = session.RequestPty("xterm", 80, 40, termModes); err != nil { 98 return 99 } 100 } 101 102 log.Printf("starting remote command: %s", cmd.Command) 103 err = session.Start(cmd.Command + "\n") 104 if err != nil { 105 return 106 } 107 108 // A channel to keep track of our done state 109 doneCh := make(chan struct{}) 110 sessionLock := new(sync.Mutex) 111 timedOut := false 112 113 // Start a goroutine to wait for the session to end and set the 114 // exit boolean and status. 115 go func() { 116 defer session.Close() 117 118 err := session.Wait() 119 exitStatus := 0 120 if err != nil { 121 exitErr, ok := err.(*ssh.ExitError) 122 if ok { 123 exitStatus = exitErr.ExitStatus() 124 } 125 } 126 127 sessionLock.Lock() 128 defer sessionLock.Unlock() 129 130 if timedOut { 131 // We timed out, so set the exit status to -1 132 exitStatus = -1 133 } 134 135 log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command) 136 cmd.SetExited(exitStatus) 137 close(doneCh) 138 }() 139 140 return 141 } 142 143 func (c *comm) Upload(path string, input io.Reader, fi *os.FileInfo) error { 144 if c.config.UseSftp { 145 return c.sftpUploadSession(path, input, fi) 146 } else { 147 return c.scpUploadSession(path, input, fi) 148 } 149 } 150 151 func (c *comm) UploadDir(dst string, src string, excl []string) error { 152 log.Printf("Upload dir '%s' to '%s'", src, dst) 153 if c.config.UseSftp { 154 return c.sftpUploadDirSession(dst, src, excl) 155 } else { 156 return c.scpUploadDirSession(dst, src, excl) 157 } 158 } 159 160 func (c *comm) DownloadDir(src string, dst string, excl []string) error { 161 log.Printf("Download dir '%s' to '%s'", src, dst) 162 scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error { 163 for { 164 fmt.Fprint(w, "\x00") 165 166 // read file info 167 fi, err := stdoutR.ReadString('\n') 168 if err != nil { 169 return err 170 } 171 172 if len(fi) < 0 { 173 return fmt.Errorf("empty response from server") 174 } 175 176 switch fi[0] { 177 case '\x01', '\x02': 178 return fmt.Errorf("%s", fi[1:len(fi)]) 179 case 'C', 'D': 180 break 181 default: 182 return fmt.Errorf("unexpected server response (%x)", fi[0]) 183 } 184 185 var mode string 186 var size int64 187 var name string 188 log.Printf("Download dir str:%s", fi) 189 n, err := fmt.Sscanf(fi, "%6s %d %s", &mode, &size, &name) 190 if err != nil || n != 3 { 191 return fmt.Errorf("can't parse server response (%s)", fi) 192 } 193 if size < 0 { 194 return fmt.Errorf("negative file size") 195 } 196 197 log.Printf("Download dir mode:%s size:%d name:%s", mode, size, name) 198 switch fi[0] { 199 case 'D': 200 err = os.MkdirAll(filepath.Join(dst, name), os.FileMode(0755)) 201 if err != nil { 202 return err 203 } 204 fmt.Fprint(w, "\x00") 205 return nil 206 case 'C': 207 fmt.Fprint(w, "\x00") 208 err = scpDownloadFile(filepath.Join(dst, name), stdoutR, size, os.FileMode(0644)) 209 if err != nil { 210 return err 211 } 212 } 213 214 if err := checkSCPStatus(stdoutR); err != nil { 215 return err 216 } 217 } 218 } 219 return c.scpSession("scp -vrf "+src, scpFunc) 220 } 221 222 func (c *comm) Download(path string, output io.Writer) error { 223 if c.config.UseSftp { 224 return c.sftpDownloadSession(path, output) 225 } 226 return c.scpDownloadSession(path, output) 227 } 228 229 func (c *comm) newSession() (session *ssh.Session, err error) { 230 log.Println("opening new ssh session") 231 if c.client == nil { 232 err = errors.New("client not available") 233 } else { 234 session, err = c.client.NewSession() 235 } 236 237 if err != nil { 238 log.Printf("ssh session open error: '%s', attempting reconnect", err) 239 if err := c.reconnect(); err != nil { 240 return nil, err 241 } 242 243 return c.client.NewSession() 244 } 245 246 return session, nil 247 } 248 249 func (c *comm) reconnect() (err error) { 250 if c.conn != nil { 251 c.conn.Close() 252 } 253 254 // Set the conn and client to nil since we'll recreate it 255 c.conn = nil 256 c.client = nil 257 258 log.Printf("reconnecting to TCP connection for SSH") 259 c.conn, err = c.config.Connection() 260 if err != nil { 261 // Explicitly set this to the REAL nil. Connection() can return 262 // a nil implementation of net.Conn which will make the 263 // "if c.conn == nil" check fail above. Read here for more information 264 // on this psychotic language feature: 265 // 266 // http://golang.org/doc/faq#nil_error 267 c.conn = nil 268 269 log.Printf("reconnection error: %s", err) 270 return 271 } 272 273 log.Printf("handshaking with SSH") 274 275 // Default timeout to 1 minute if it wasn't specified (zero value). For 276 // when you need to handshake from low orbit. 277 var duration time.Duration 278 if c.config.HandshakeTimeout == 0 { 279 duration = 1 * time.Minute 280 } else { 281 duration = c.config.HandshakeTimeout 282 } 283 284 connectionEstablished := make(chan struct{}, 1) 285 286 var sshConn ssh.Conn 287 var sshChan <-chan ssh.NewChannel 288 var req <-chan *ssh.Request 289 290 go func() { 291 sshConn, sshChan, req, err = ssh.NewClientConn(c.conn, c.address, c.config.SSHConfig) 292 close(connectionEstablished) 293 }() 294 295 select { 296 case <-connectionEstablished: 297 // We don't need to do anything here. We just want select to block until 298 // we connect or timeout. 299 case <-time.After(duration): 300 if c.conn != nil { 301 c.conn.Close() 302 } 303 if sshConn != nil { 304 sshConn.Close() 305 } 306 return ErrHandshakeTimeout 307 } 308 309 if err != nil { 310 log.Printf("handshake error: %s", err) 311 return 312 } 313 log.Printf("handshake complete!") 314 if sshConn != nil { 315 c.client = ssh.NewClient(sshConn, sshChan, req) 316 } 317 c.connectToAgent() 318 319 return 320 } 321 322 func (c *comm) connectToAgent() { 323 if c.client == nil { 324 return 325 } 326 327 if c.config.DisableAgent { 328 log.Printf("[INFO] SSH agent forwarding is disabled.") 329 return 330 } 331 332 // open connection to the local agent 333 socketLocation := os.Getenv("SSH_AUTH_SOCK") 334 if socketLocation == "" { 335 log.Printf("[INFO] no local agent socket, will not connect agent") 336 return 337 } 338 agentConn, err := net.Dial("unix", socketLocation) 339 if err != nil { 340 log.Printf("[ERROR] could not connect to local agent socket: %s", socketLocation) 341 return 342 } 343 344 // create agent and add in auth 345 forwardingAgent := agent.NewClient(agentConn) 346 if forwardingAgent == nil { 347 log.Printf("[ERROR] Could not create agent client") 348 agentConn.Close() 349 return 350 } 351 352 // add callback for forwarding agent to SSH config 353 // XXX - might want to handle reconnects appending multiple callbacks 354 auth := ssh.PublicKeysCallback(forwardingAgent.Signers) 355 c.config.SSHConfig.Auth = append(c.config.SSHConfig.Auth, auth) 356 agent.ForwardToAgent(c.client, forwardingAgent) 357 358 // Setup a session to request agent forwarding 359 session, err := c.newSession() 360 if err != nil { 361 return 362 } 363 defer session.Close() 364 365 err = agent.RequestAgentForwarding(session) 366 if err != nil { 367 log.Printf("[ERROR] RequestAgentForwarding: %#v", err) 368 return 369 } 370 371 log.Printf("[INFO] agent forwarding enabled") 372 return 373 } 374 375 func (c *comm) sftpUploadSession(path string, input io.Reader, fi *os.FileInfo) error { 376 sftpFunc := func(client *sftp.Client) error { 377 return sftpUploadFile(path, input, client, fi) 378 } 379 380 return c.sftpSession(sftpFunc) 381 } 382 383 func sftpUploadFile(path string, input io.Reader, client *sftp.Client, fi *os.FileInfo) error { 384 log.Printf("[DEBUG] sftp: uploading %s", path) 385 386 f, err := client.Create(path) 387 if err != nil { 388 return err 389 } 390 defer f.Close() 391 392 if _, err = io.Copy(f, input); err != nil { 393 return err 394 } 395 396 if fi != nil && (*fi).Mode().IsRegular() { 397 mode := (*fi).Mode().Perm() 398 err = client.Chmod(path, mode) 399 if err != nil { 400 return err 401 } 402 } 403 404 return nil 405 } 406 407 func (c *comm) sftpUploadDirSession(dst string, src string, excl []string) error { 408 sftpFunc := func(client *sftp.Client) error { 409 rootDst := dst 410 if src[len(src)-1] != '/' { 411 log.Printf("No trailing slash, creating the source directory name") 412 rootDst = filepath.Join(dst, filepath.Base(src)) 413 } 414 walkFunc := func(path string, info os.FileInfo, err error) error { 415 if err != nil { 416 return err 417 } 418 // Calculate the final destination using the 419 // base source and root destination 420 relSrc, err := filepath.Rel(src, path) 421 if err != nil { 422 return err 423 } 424 finalDst := filepath.Join(rootDst, relSrc) 425 426 // In Windows, Join uses backslashes which we don't want to get 427 // to the sftp server 428 finalDst = filepath.ToSlash(finalDst) 429 430 // Skip the creation of the target destination directory since 431 // it should exist and we might not even own it 432 if finalDst == dst { 433 return nil 434 } 435 436 return sftpVisitFile(finalDst, path, info, client) 437 } 438 439 return filepath.Walk(src, walkFunc) 440 } 441 442 return c.sftpSession(sftpFunc) 443 } 444 445 func sftpMkdir(path string, client *sftp.Client, fi os.FileInfo) error { 446 log.Printf("[DEBUG] sftp: creating dir %s", path) 447 448 if err := client.Mkdir(path); err != nil { 449 // Do not consider it an error if the directory existed 450 remoteFi, fiErr := client.Lstat(path) 451 if fiErr != nil || !remoteFi.IsDir() { 452 return err 453 } 454 } 455 456 mode := fi.Mode().Perm() 457 if err := client.Chmod(path, mode); err != nil { 458 return err 459 } 460 return nil 461 } 462 463 func sftpVisitFile(dst string, src string, fi os.FileInfo, client *sftp.Client) error { 464 if !fi.IsDir() { 465 f, err := os.Open(src) 466 if err != nil { 467 return err 468 } 469 defer f.Close() 470 return sftpUploadFile(dst, f, client, &fi) 471 } else { 472 err := sftpMkdir(dst, client, fi) 473 return err 474 } 475 } 476 477 func (c *comm) sftpDownloadSession(path string, output io.Writer) error { 478 sftpFunc := func(client *sftp.Client) error { 479 f, err := client.Open(path) 480 if err != nil { 481 return err 482 } 483 defer f.Close() 484 485 if _, err = io.Copy(output, f); err != nil { 486 return err 487 } 488 489 return nil 490 } 491 492 return c.sftpSession(sftpFunc) 493 } 494 495 func (c *comm) sftpSession(f func(*sftp.Client) error) error { 496 client, err := c.newSftpClient() 497 if err != nil { 498 return err 499 } 500 defer client.Close() 501 502 return f(client) 503 } 504 505 func (c *comm) newSftpClient() (*sftp.Client, error) { 506 session, err := c.newSession() 507 if err != nil { 508 return nil, err 509 } 510 511 if err := session.RequestSubsystem("sftp"); err != nil { 512 return nil, err 513 } 514 515 pw, err := session.StdinPipe() 516 if err != nil { 517 return nil, err 518 } 519 pr, err := session.StdoutPipe() 520 if err != nil { 521 return nil, err 522 } 523 524 return sftp.NewClientPipe(pr, pw) 525 } 526 527 func (c *comm) scpUploadSession(path string, input io.Reader, fi *os.FileInfo) error { 528 529 // The target directory and file for talking the SCP protocol 530 target_dir := filepath.Dir(path) 531 target_file := filepath.Base(path) 532 533 // On windows, filepath.Dir uses backslash seperators (ie. "\tmp"). 534 // This does not work when the target host is unix. Switch to forward slash 535 // which works for unix and windows 536 target_dir = filepath.ToSlash(target_dir) 537 538 scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error { 539 return scpUploadFile(target_file, input, w, stdoutR, fi) 540 } 541 542 return c.scpSession("scp -vt "+target_dir, scpFunc) 543 } 544 545 func (c *comm) scpUploadDirSession(dst string, src string, excl []string) error { 546 scpFunc := func(w io.Writer, r *bufio.Reader) error { 547 uploadEntries := func() error { 548 f, err := os.Open(src) 549 if err != nil { 550 return err 551 } 552 defer f.Close() 553 554 entries, err := f.Readdir(-1) 555 if err != nil { 556 return err 557 } 558 559 return scpUploadDir(src, entries, w, r) 560 } 561 562 if src[len(src)-1] != '/' { 563 log.Printf("No trailing slash, creating the source directory name") 564 fi, err := os.Stat(src) 565 if err != nil { 566 return err 567 } 568 return scpUploadDirProtocol(filepath.Base(src), w, r, uploadEntries, fi) 569 } else { 570 // Trailing slash, so only upload the contents 571 return uploadEntries() 572 } 573 } 574 575 return c.scpSession("scp -rvt "+dst, scpFunc) 576 } 577 578 func (c *comm) scpDownloadSession(path string, output io.Writer) error { 579 scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error { 580 fmt.Fprint(w, "\x00") 581 582 // read file info 583 fi, err := stdoutR.ReadString('\n') 584 if err != nil { 585 return err 586 } 587 588 if len(fi) < 0 { 589 return fmt.Errorf("empty response from server") 590 } 591 592 switch fi[0] { 593 case '\x01', '\x02': 594 return fmt.Errorf("%s", fi[1:len(fi)]) 595 case 'C': 596 case 'D': 597 return fmt.Errorf("remote file is directory") 598 default: 599 return fmt.Errorf("unexpected server response (%x)", fi[0]) 600 } 601 602 var mode string 603 var size int64 604 605 n, err := fmt.Sscanf(fi, "%6s %d ", &mode, &size) 606 if err != nil || n != 2 { 607 return fmt.Errorf("can't parse server response (%s)", fi) 608 } 609 if size < 0 { 610 return fmt.Errorf("negative file size") 611 } 612 613 fmt.Fprint(w, "\x00") 614 615 if _, err := io.CopyN(output, stdoutR, size); err != nil { 616 return err 617 } 618 619 fmt.Fprint(w, "\x00") 620 621 if err := checkSCPStatus(stdoutR); err != nil { 622 return err 623 } 624 625 return nil 626 } 627 628 if strings.Index(path, " ") == -1 { 629 return c.scpSession("scp -vf "+path, scpFunc) 630 } 631 return c.scpSession("scp -vf "+strconv.Quote(path), scpFunc) 632 } 633 634 func (c *comm) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error { 635 session, err := c.newSession() 636 if err != nil { 637 return err 638 } 639 defer session.Close() 640 641 // Get a pipe to stdin so that we can send data down 642 stdinW, err := session.StdinPipe() 643 if err != nil { 644 return err 645 } 646 647 // We only want to close once, so we nil w after we close it, 648 // and only close in the defer if it hasn't been closed already. 649 defer func() { 650 if stdinW != nil { 651 stdinW.Close() 652 } 653 }() 654 655 // Get a pipe to stdout so that we can get responses back 656 stdoutPipe, err := session.StdoutPipe() 657 if err != nil { 658 return err 659 } 660 stdoutR := bufio.NewReader(stdoutPipe) 661 662 // Set stderr to a bytes buffer 663 stderr := new(bytes.Buffer) 664 session.Stderr = stderr 665 666 // Start the sink mode on the other side 667 // TODO(mitchellh): There are probably issues with shell escaping the path 668 log.Println("Starting remote scp process: ", scpCommand) 669 if err := session.Start(scpCommand); err != nil { 670 return err 671 } 672 673 // Call our callback that executes in the context of SCP. We ignore 674 // EOF errors if they occur because it usually means that SCP prematurely 675 // ended on the other side. 676 log.Println("Started SCP session, beginning transfers...") 677 if err := f(stdinW, stdoutR); err != nil && err != io.EOF { 678 return err 679 } 680 681 // Close the stdin, which sends an EOF, and then set w to nil so that 682 // our defer func doesn't close it again since that is unsafe with 683 // the Go SSH package. 684 log.Println("SCP session complete, closing stdin pipe.") 685 stdinW.Close() 686 stdinW = nil 687 688 // Wait for the SCP connection to close, meaning it has consumed all 689 // our data and has completed. Or has errored. 690 log.Println("Waiting for SSH session to complete.") 691 err = session.Wait() 692 if err != nil { 693 if exitErr, ok := err.(*ssh.ExitError); ok { 694 // Otherwise, we have an ExitErorr, meaning we can just read 695 // the exit status 696 log.Printf("non-zero exit status: %d", exitErr.ExitStatus()) 697 698 // If we exited with status 127, it means SCP isn't available. 699 // Return a more descriptive error for that. 700 if exitErr.ExitStatus() == 127 { 701 return errors.New( 702 "SCP failed to start. This usually means that SCP is not\n" + 703 "properly installed on the remote system.") 704 } 705 } 706 707 return err 708 } 709 710 log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String()) 711 return nil 712 } 713 714 // checkSCPStatus checks that a prior command sent to SCP completed 715 // successfully. If it did not complete successfully, an error will 716 // be returned. 717 func checkSCPStatus(r *bufio.Reader) error { 718 code, err := r.ReadByte() 719 if err != nil { 720 return err 721 } 722 723 if code != 0 { 724 // Treat any non-zero (really 1 and 2) as fatal errors 725 message, _, err := r.ReadLine() 726 if err != nil { 727 return fmt.Errorf("Error reading error message: %s", err) 728 } 729 730 return errors.New(string(message)) 731 } 732 733 return nil 734 } 735 736 func scpDownloadFile(dst string, src io.Reader, size int64, mode os.FileMode) error { 737 f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) 738 if err != nil { 739 return err 740 } 741 defer f.Close() 742 if _, err := io.CopyN(f, src, size); err != nil { 743 return err 744 } 745 return nil 746 } 747 748 func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader, fi *os.FileInfo) error { 749 var mode os.FileMode 750 var size int64 751 752 if fi != nil && (*fi).Mode().IsRegular() { 753 mode = (*fi).Mode().Perm() 754 size = (*fi).Size() 755 } else { 756 // Create a temporary file where we can copy the contents of the src 757 // so that we can determine the length, since SCP is length-prefixed. 758 tf, err := ioutil.TempFile("", "packer-upload") 759 if err != nil { 760 return fmt.Errorf("Error creating temporary file for upload: %s", err) 761 } 762 defer os.Remove(tf.Name()) 763 defer tf.Close() 764 765 mode = 0644 766 767 log.Println("Copying input data into temporary file so we can read the length") 768 if _, err := io.Copy(tf, src); err != nil { 769 return err 770 } 771 772 // Sync the file so that the contents are definitely on disk, then 773 // read the length of it. 774 if err := tf.Sync(); err != nil { 775 return fmt.Errorf("Error creating temporary file for upload: %s", err) 776 } 777 778 // Seek the file to the beginning so we can re-read all of it 779 if _, err := tf.Seek(0, 0); err != nil { 780 return fmt.Errorf("Error creating temporary file for upload: %s", err) 781 } 782 783 tfi, err := tf.Stat() 784 if err != nil { 785 return fmt.Errorf("Error creating temporary file for upload: %s", err) 786 } 787 788 size = tfi.Size() 789 src = tf 790 } 791 792 // Start the protocol 793 perms := fmt.Sprintf("C%04o", mode) 794 log.Printf("[DEBUG] scp: Uploading %s: perms=%s size=%d", dst, perms, size) 795 796 fmt.Fprintln(w, perms, size, dst) 797 if err := checkSCPStatus(r); err != nil { 798 return err 799 } 800 801 if _, err := io.CopyN(w, src, size); err != nil { 802 return err 803 } 804 805 fmt.Fprint(w, "\x00") 806 if err := checkSCPStatus(r); err != nil { 807 return err 808 } 809 810 return nil 811 } 812 813 func scpUploadDirProtocol(name string, w io.Writer, r *bufio.Reader, f func() error, fi os.FileInfo) error { 814 log.Printf("SCP: starting directory upload: %s", name) 815 816 mode := fi.Mode().Perm() 817 818 perms := fmt.Sprintf("D%04o 0", mode) 819 820 fmt.Fprintln(w, perms, name) 821 err := checkSCPStatus(r) 822 if err != nil { 823 return err 824 } 825 826 if err := f(); err != nil { 827 return err 828 } 829 830 fmt.Fprintln(w, "E") 831 if err != nil { 832 return err 833 } 834 835 return nil 836 } 837 838 func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) error { 839 for _, fi := range fs { 840 realPath := filepath.Join(root, fi.Name()) 841 842 // Track if this is actually a symlink to a directory. If it is 843 // a symlink to a file we don't do any special behavior because uploading 844 // a file just works. If it is a directory, we need to know so we 845 // treat it as such. 846 isSymlinkToDir := false 847 if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 848 symPath, err := filepath.EvalSymlinks(realPath) 849 if err != nil { 850 return err 851 } 852 853 symFi, err := os.Lstat(symPath) 854 if err != nil { 855 return err 856 } 857 858 isSymlinkToDir = symFi.IsDir() 859 } 860 861 if !fi.IsDir() && !isSymlinkToDir { 862 // It is a regular file (or symlink to a file), just upload it 863 f, err := os.Open(realPath) 864 if err != nil { 865 return err 866 } 867 868 err = func() error { 869 defer f.Close() 870 return scpUploadFile(fi.Name(), f, w, r, &fi) 871 }() 872 873 if err != nil { 874 return err 875 } 876 877 continue 878 } 879 880 // It is a directory, recursively upload 881 err := scpUploadDirProtocol(fi.Name(), w, r, func() error { 882 f, err := os.Open(realPath) 883 if err != nil { 884 return err 885 } 886 defer f.Close() 887 888 entries, err := f.Readdir(-1) 889 if err != nil { 890 return err 891 } 892 893 return scpUploadDir(realPath, entries, w, r) 894 }, fi) 895 if err != nil { 896 return err 897 } 898 } 899 900 return nil 901 }