Skip to content

Commit a7b304f

Browse files
quick proof of concept on how to send a configuration to a remote server
1 parent a615444 commit a7b304f

File tree

8 files changed

+226
-1
lines changed

8 files changed

+226
-1
lines changed

commands.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ func getOwnCommands() []ownCommand {
158158
needConfiguration: false,
159159
hide: true,
160160
},
161+
{
162+
name: "send",
163+
description: "send a configuration profile to a remote client",
164+
action: sendProfileCommand,
165+
needConfiguration: true,
166+
noProfile: true,
167+
hide: true,
168+
},
161169
}
162170
}
163171

config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,17 @@ func (c *Config) getProfilePath(key string) string {
701701
return c.flatKey(constants.SectionConfigurationProfiles, key)
702702
}
703703

704+
func (c *Config) GetRemote(remoteName string) (*Remote, error) {
705+
if c.GetVersion() < Version02 {
706+
return nil, ErrNotSupportedInVersion1
707+
}
708+
709+
remote := NewRemote(c, remoteName)
710+
err := c.unmarshalKey(c.flatKey(constants.SectionConfigurationRemotes, remoteName), remote)
711+
712+
return remote, err
713+
}
714+
704715
// unmarshalConfig returns the decoder config options depending on the configuration version and format
705716
func (c *Config) unmarshalConfig() viper.DecoderConfigOption {
706717
if c.GetVersion() == Version01 {

config/error.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ package config
33
import "errors"
44

55
var (
6-
ErrNotFound = errors.New("not found")
6+
ErrNotFound = errors.New("not found")
7+
ErrNotSupportedInVersion1 = errors.New("not supported in configuration version 1")
78
)

config/remote .go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package config
2+
3+
type Remote struct {
4+
name string
5+
config *Config
6+
Connection string `mapstructure:"connection" default:"ssh" description:"Connection type to use to connect to the remote client"`
7+
Host string `mapstructure:"host" description:"Address of the remote client. Format: <host>:<port>"`
8+
Username string `mapstructure:"username" description:"User to connect to the remote client"`
9+
PrivateKeyPath string `mapstructure:"private-key" description:"Path to the private key to use for authentication"`
10+
KnownHostsPath string `mapstructure:"known-hosts" description:"Path to the known hosts file"`
11+
SendFiles []string `mapstructure:"send-files" description:"Configuration files to transfer to the remote client"`
12+
}
13+
14+
func NewRemote(config *Config, name string) *Remote {
15+
remote := &Remote{
16+
name: name,
17+
config: config,
18+
}
19+
return remote
20+
}

constants/section.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
SectionConfigurationMixins = "mixins"
1414
SectionConfigurationMixinUse = "use"
1515
SectionConfigurationSchedule = "schedule"
16+
SectionConfigurationRemotes = "remotes"
1617

1718
SectionDefinitionCommon = "common"
1819
SectionDefinitionForget = "forget"

send.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/creativeprojects/resticprofile/ssh"
8+
)
9+
10+
func sendProfileCommand(w io.Writer, cmdCtx commandContext) error {
11+
if len(cmdCtx.flags.resticArgs) < 2 {
12+
return fmt.Errorf("missing argument: remote name")
13+
}
14+
remote, err := cmdCtx.config.GetRemote(cmdCtx.flags.resticArgs[1])
15+
if err != nil {
16+
return err
17+
}
18+
cnx := ssh.NewSSH(ssh.Config{
19+
Host: remote.Host,
20+
Username: remote.Username,
21+
PrivateKeyPath: remote.PrivateKeyPath,
22+
KnownHostsPath: remote.KnownHostsPath,
23+
})
24+
err = cnx.Connect()
25+
if err != nil {
26+
return err
27+
}
28+
defer cnx.Close()
29+
return nil
30+
}

ssh/config.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package ssh
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
type Config struct {
11+
Host string
12+
Username string
13+
PrivateKeyPath string
14+
KnownHostsPath string
15+
}
16+
17+
func (c *Config) Validate() error {
18+
if c.Host == "" {
19+
return fmt.Errorf("host is required")
20+
}
21+
if c.Username == "" {
22+
return fmt.Errorf("username is required")
23+
}
24+
if c.PrivateKeyPath == "" {
25+
home, err := os.UserHomeDir()
26+
if err != nil {
27+
return fmt.Errorf("unable to get current user home directory: %w", err)
28+
}
29+
c.PrivateKeyPath = filepath.Join(home, ".ssh/id_rsa") // we can go through all the default name for each key type
30+
}
31+
if c.KnownHostsPath == "" {
32+
home, err := os.UserHomeDir()
33+
if err != nil {
34+
return fmt.Errorf("unable to get current user home directory: %w", err)
35+
}
36+
c.KnownHostsPath = filepath.Join(home, ".ssh/known_hosts")
37+
}
38+
if !strings.Contains(c.Host, ":") {
39+
c.Host = c.Host + ":22"
40+
}
41+
return nil
42+
}

ssh/ssh.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package ssh
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"log"
7+
"net"
8+
"net/http"
9+
"os"
10+
11+
"github.com/creativeprojects/clog"
12+
"golang.org/x/crypto/ssh"
13+
"golang.org/x/crypto/ssh/knownhosts"
14+
)
15+
16+
const startPort = 10001
17+
18+
type SSH struct {
19+
config Config
20+
port int
21+
client *ssh.Client
22+
tunnel net.Listener
23+
}
24+
25+
func NewSSH(config Config) *SSH {
26+
return &SSH{
27+
config: config,
28+
port: startPort,
29+
}
30+
}
31+
32+
func (s *SSH) Connect() error {
33+
err := s.config.Validate()
34+
if err != nil {
35+
return err
36+
}
37+
hostKeyCallback, err := knownhosts.New(s.config.KnownHostsPath)
38+
if err != nil {
39+
return fmt.Errorf("cannot load host keys from known_hosts: %w", err)
40+
}
41+
key, err := os.ReadFile(s.config.PrivateKeyPath)
42+
if err != nil {
43+
return fmt.Errorf("unable to read private key: %w", err)
44+
}
45+
46+
// Create the Signer for this private key.
47+
signer, err := ssh.ParsePrivateKey(key)
48+
if err != nil {
49+
return fmt.Errorf("unable to parse private key: %w", err)
50+
}
51+
52+
config := &ssh.ClientConfig{
53+
User: s.config.Username,
54+
Auth: []ssh.AuthMethod{
55+
// Use the PublicKeys method for remote authentication.
56+
ssh.PublicKeys(signer),
57+
},
58+
HostKeyCallback: hostKeyCallback,
59+
}
60+
61+
// Connect to the remote server and perform the SSH handshake.
62+
s.client, err = ssh.Dial("tcp", s.config.Host, config)
63+
if err != nil {
64+
return fmt.Errorf("unable to connect: %w", err)
65+
}
66+
67+
// Request the remote side to open a local port
68+
s.tunnel, err = s.client.Listen("tcp", fmt.Sprintf("localhost:%d", s.port))
69+
if err != nil {
70+
log.Fatal("unable to register tcp forward: ", err)
71+
}
72+
73+
go func() {
74+
// Serve HTTP with your SSH server acting as a reverse proxy.
75+
http.Serve(s.tunnel, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
76+
fmt.Fprintf(resp, "Hello world!\n")
77+
}))
78+
}()
79+
80+
// Each ClientConn can support multiple interactive sessions,
81+
// represented by a Session.
82+
session, err := s.client.NewSession()
83+
if err != nil {
84+
log.Fatal("Failed to create session: ", err)
85+
}
86+
defer session.Close()
87+
88+
// Once a Session is created, we can execute a single command on
89+
// the remote side using the Run method.
90+
var b bytes.Buffer
91+
session.Stdout = &b
92+
if err := session.Run(fmt.Sprintf("curl http://localhost:%d", s.port)); err != nil {
93+
log.Fatal("Failed to run: " + err.Error())
94+
}
95+
fmt.Println(b.String())
96+
return nil
97+
}
98+
99+
func (s *SSH) Close() {
100+
if s.tunnel != nil {
101+
err := s.tunnel.Close()
102+
if err != nil {
103+
clog.Warningf("unable to close tunnel: %s", err)
104+
}
105+
}
106+
if s.client != nil {
107+
err := s.client.Close()
108+
if err != nil {
109+
clog.Warningf("unable to close ssh connection: %s", err)
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)