github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/synchronization/protocols/ssh/protocol.go (about)

     1  package ssh
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  
     8  	"github.com/mutagen-io/mutagen/pkg/agent"
     9  	"github.com/mutagen-io/mutagen/pkg/agent/transport/ssh"
    10  	"github.com/mutagen-io/mutagen/pkg/logging"
    11  	"github.com/mutagen-io/mutagen/pkg/synchronization"
    12  	"github.com/mutagen-io/mutagen/pkg/synchronization/endpoint/remote"
    13  	urlpkg "github.com/mutagen-io/mutagen/pkg/url"
    14  )
    15  
    16  // protocolHandler implements the synchronization.ProtocolHandler interface for
    17  // connecting to remote endpoints over SSH. It uses the agent infrastructure
    18  // over an SSH transport.
    19  type protocolHandler struct{}
    20  
    21  // dialResult provides asynchronous agent dialing results.
    22  type dialResult struct {
    23  	// stream is the stream returned by agent dialing.
    24  	stream io.ReadWriteCloser
    25  	// error is the error returned by agent dialing.
    26  	error error
    27  }
    28  
    29  // Connect connects to an SSH endpoint.
    30  func (h *protocolHandler) Connect(
    31  	ctx context.Context,
    32  	logger *logging.Logger,
    33  	url *urlpkg.URL,
    34  	prompter string,
    35  	session string,
    36  	version synchronization.Version,
    37  	configuration *synchronization.Configuration,
    38  	alpha bool,
    39  ) (synchronization.Endpoint, error) {
    40  	// Verify that the URL is of the correct kind and protocol.
    41  	if url.Kind != urlpkg.Kind_Synchronization {
    42  		panic("non-synchronization URL dispatched to synchronization protocol handler")
    43  	} else if url.Protocol != urlpkg.Protocol_SSH {
    44  		panic("non-SSH URL dispatched to SSH protocol handler")
    45  	}
    46  
    47  	// Create an SSH agent transport.
    48  	transport, err := ssh.NewTransport(url.User, url.Host, uint16(url.Port), prompter)
    49  	if err != nil {
    50  		return nil, fmt.Errorf("unable to create SSH transport: %w", err)
    51  	}
    52  
    53  	// Create a channel to deliver the dialing result.
    54  	results := make(chan dialResult)
    55  
    56  	// Perform dialing in a background Goroutine so that we can monitor for
    57  	// cancellation.
    58  	go func() {
    59  		// Perform the dialing operation.
    60  		stream, err := agent.Dial(logger, transport, agent.CommandSynchronizer, prompter)
    61  
    62  		// Transmit the result or, if cancelled, close the stream.
    63  		select {
    64  		case results <- dialResult{stream, err}:
    65  		case <-ctx.Done():
    66  			if stream != nil {
    67  				stream.Close()
    68  			}
    69  		}
    70  	}()
    71  
    72  	// Wait for dialing results or cancellation.
    73  	var stream io.ReadWriteCloser
    74  	select {
    75  	case result := <-results:
    76  		if result.error != nil {
    77  			return nil, fmt.Errorf("unable to dial agent endpoint: %w", result.error)
    78  		}
    79  		stream = result.stream
    80  	case <-ctx.Done():
    81  		return nil, context.Canceled
    82  	}
    83  
    84  	// Create the endpoint client.
    85  	return remote.NewEndpoint(logger, stream, url.Path, session, version, configuration, alpha)
    86  }
    87  
    88  func init() {
    89  	// Register the SSH protocol handler with the synchronization package.
    90  	synchronization.ProtocolHandlers[urlpkg.Protocol_SSH] = &protocolHandler{}
    91  }