github.com/puellanivis/breton@v0.2.16/lib/files/sftpfiles/host.go (about)

     1  package sftpfiles
     2  
     3  import (
     4  	"errors"
     5  	"net/url"
     6  	"sync"
     7  
     8  	"github.com/puellanivis/breton/lib/os/user"
     9  
    10  	"github.com/pkg/sftp"
    11  	"golang.org/x/crypto/ssh"
    12  )
    13  
    14  // Host defines a set of connection settings to a specific host/user combination,
    15  // and manages a common SFTP connection to that host with those credentials.
    16  type Host struct {
    17  	mu   sync.Mutex
    18  	conn *ssh.Client
    19  	cl   *sftp.Client
    20  
    21  	uri *url.URL
    22  
    23  	auths []ssh.AuthMethod
    24  
    25  	ignoreHostkey bool
    26  	hostkey       ssh.HostKeyCallback
    27  	hostkeyAlgos  []string
    28  }
    29  
    30  var (
    31  	userInit    sync.Once
    32  	defaultUser *url.Userinfo
    33  )
    34  
    35  func getUser() *url.Userinfo {
    36  	userInit.Do(func() {
    37  		name, err := user.CurrentUsername()
    38  		if err != nil {
    39  			return
    40  		}
    41  
    42  		defaultUser = url.User(name)
    43  	})
    44  
    45  	return defaultUser
    46  }
    47  
    48  // NewHost returns a Host defined for a specific host/user based on a given URL.
    49  // No connection is made, and no authentication or hostkey validation is defined.
    50  func NewHost(uri *url.URL) *Host {
    51  	var auths []ssh.AuthMethod
    52  
    53  	user := getUser()
    54  	if uri.User != nil {
    55  		user = url.User(uri.User.Username())
    56  
    57  		if pw, ok := uri.User.Password(); ok {
    58  			auths = append(auths, ssh.Password(pw))
    59  		}
    60  	}
    61  
    62  	uri = &url.URL{
    63  		Scheme: "ssh",
    64  		Host:   uri.Host,
    65  		User:   user,
    66  	}
    67  
    68  	if uri.Port() == "" {
    69  		uri.Host += ":22"
    70  	}
    71  
    72  	return &Host{
    73  		uri:   uri,
    74  		auths: auths,
    75  	}
    76  }
    77  
    78  // Name returns an identifying name of the Host composed of the authority section of the URL: //user[:pass]@hostname:port
    79  func (h *Host) Name() string {
    80  	return h.uri.String()
    81  }
    82  
    83  func (h *Host) close() error {
    84  	if h.cl == nil {
    85  		return nil
    86  	}
    87  
    88  	err := h.cl.Close()
    89  	if err2 := h.conn.Close(); err == nil {
    90  		err = err2
    91  	}
    92  
    93  	h.cl, h.conn = nil, nil
    94  
    95  	return err
    96  }
    97  
    98  // Close closes and invalidates the Host's current connection.
    99  func (h *Host) Close() error {
   100  	h.mu.Lock()
   101  	defer h.mu.Unlock()
   102  
   103  	return h.close()
   104  }
   105  
   106  func (h *Host) client() *sftp.Client {
   107  	if h.cl == nil {
   108  		return nil
   109  	}
   110  
   111  	if _, err := h.cl.Getwd(); err != nil {
   112  		// We cannot get the current working directory,
   113  		// So, invalidate our connections, and return nil.
   114  		_ = h.close()
   115  
   116  		return nil
   117  	}
   118  
   119  	return h.cl
   120  }
   121  
   122  // GetClient returns the currently connected Client connected to by the Host.
   123  // It returns nil if the Host is not currently connected.
   124  func (h *Host) GetClient() *sftp.Client {
   125  	h.mu.Lock()
   126  	defer h.mu.Unlock()
   127  
   128  	return h.client()
   129  }
   130  
   131  // Connect either returns the currently connected Client, or makes a new connection based on Host.
   132  func (h *Host) Connect() (*sftp.Client, error) {
   133  	h.mu.Lock()
   134  	defer h.mu.Unlock()
   135  
   136  	if cl := h.client(); cl != nil {
   137  		return cl, nil
   138  	}
   139  
   140  	hk := h.hostkey
   141  	if h.ignoreHostkey {
   142  		hk = ssh.InsecureIgnoreHostKey()
   143  	}
   144  
   145  	if hk == nil {
   146  		return nil, errors.New("no hostkey validation defined")
   147  	}
   148  
   149  	conn, err := ssh.Dial("tcp", h.uri.Host, &ssh.ClientConfig{
   150  		User:              h.uri.User.Username(),
   151  		Auth:              h.cloneAuths(),
   152  		HostKeyCallback:   hk,
   153  		HostKeyAlgorithms: h.hostkeyAlgos,
   154  	})
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	cl, err := sftp.NewClient(conn)
   160  	if err != nil {
   161  		conn.Close()
   162  		return nil, err
   163  	}
   164  
   165  	h.conn, h.cl = conn, cl
   166  
   167  	return cl, nil
   168  }
   169  
   170  func (h *Host) cloneAuths() []ssh.AuthMethod {
   171  	return append([]ssh.AuthMethod{}, h.auths...)
   172  }
   173  
   174  // addAuths is an internal convenience func to add any number of auths.
   175  func (h *Host) addAuths(auths ...ssh.AuthMethod) []ssh.AuthMethod {
   176  	return h.SetAuths(append(h.cloneAuths(), auths...))
   177  }
   178  
   179  // AddAuth adds the given ssh.AuthMethod to the authorization methods for the Host, and return the previous value.
   180  func (h *Host) AddAuth(auth ssh.AuthMethod) []ssh.AuthMethod {
   181  	return h.addAuths(auth)
   182  }
   183  
   184  // SetAuths sets the slice of ssh.AuthMethod on the Host, and returns the previous value.
   185  func (h *Host) SetAuths(auths []ssh.AuthMethod) []ssh.AuthMethod {
   186  	save := h.auths
   187  
   188  	h.auths = auths
   189  
   190  	return save
   191  }
   192  
   193  // IgnoreHostKeys sets a flag that Host should ignore Host keys when connecting.
   194  // THIS IS INSECURE.
   195  func (h *Host) IgnoreHostKeys(state bool) bool {
   196  	save := h.ignoreHostkey
   197  
   198  	h.ignoreHostkey = state
   199  
   200  	return save
   201  }
   202  
   203  // SetHostKeyCallback sets the current hostkey callback for the Host, and returns the previous value.
   204  func (h *Host) SetHostKeyCallback(cb ssh.HostKeyCallback, algos []string) (ssh.HostKeyCallback, []string) {
   205  	saveHK, saveAlgos := h.hostkey, h.hostkeyAlgos
   206  
   207  	h.hostkey = cb
   208  	h.hostkeyAlgos = algos
   209  
   210  	return saveHK, saveAlgos
   211  }