1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
|
// Copyright 2016 Canonical ltd.
// Copyright 2016 Cloudbase solutions
// Licensed under the LGPLv3, see licence file for details.
package winrm
import (
"bytes"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"github.com/juju/errors"
"github.com/juju/loggo"
"github.com/masterzen/winrm"
"golang.org/x/crypto/ssh/terminal"
)
const (
httpPort = 5985
httpsPort = 5986
defaultWinndowsUser = "Administrator"
)
var (
logger = loggo.GetLogger("juju.utils.winrm")
)
// Client type retains information about the winrm connection
type Client struct {
conn *winrm.Client
pass string
secure bool
}
// Secure returns true if the client is using a secure connection or false
// if it's just a normal http
func (c Client) Secure() bool {
return c.secure
}
// Password returns the winrm connection password
func (c Client) Password() string {
return c.pass
}
// ClientConfig used for setting up a secure https client connection
type ClientConfig struct {
// User of the client connection
// If you want the default user Administrator, leave this empty
User string
// Host where we want to connect
Host string
// Key Private RSA key
Key []byte
// Cert https client x509 cert
Cert []byte
// CACert of the host we wish to connect
// This can be nil if Insecure is false
CACert []byte
// Timeout on how long we should wait until a connection is made.
// If empty this will use the 60*time.Seconds winrm default value
Timeout time.Duration
// Insecure flag for checking the CACert or not. If this is true there should
// be a valid CACert passed also.
Insecure bool
// Password callback for returning a password in different ways
Password GetPasswd
// Secure flag for specifying if the user wants https or http
Secure bool
}
// errNoPasswdFn sentinel for returning unset password callback
var errNoPasswdFn = fmt.Errorf("No password callback")
// password checks if the password callback is set and returns the password
// from that specific get password handler
func (c ClientConfig) password() (string, error) {
if c.Password != nil {
pass, err := c.Password()
return pass, err
}
return "", errNoPasswdFn
}
// GetPasswd callback for different semantics that a client
// could use for secure authentication
type GetPasswd func() (string, error)
// TTYGetPasswd will be valid if it's used from a valid TTY input,
// This can be passed in ClientConfig
func TTYGetPasswd() (string, error) {
// make it look like a dropdown shell
fmt.Fprintf(os.Stdout, "[Winrm] >> ")
// very important that os.Stdin.Fd() needs to be a valid tty.
// if the file descriptor dosen't point to a terminal this will fail
// with the message inappropriate ioctl for device
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintf(os.Stdout, "\n")
if err != nil {
return "", errors.Annotatef(err, "Can't retrive password from terminal")
}
return string(pass), nil
}
// Validate checks all config options if they are invalid
func (c ClientConfig) Validate() error {
if c.Host == "" {
return fmt.Errorf("Empty host in client config")
}
// if the connection is https
if c.Secure == true {
// check if we we or not the password authentication method
if c.Password == nil {
// that meas we need to be sure if the cert and key is set
// for cert authentication
if c.Key == nil || c.Cert == nil {
return fmt.Errorf("Empty key or cert in client config")
}
// everything is set
logger.Infof("using https winrm connection with cert authentication")
} else { // we are using https with password authentication
logger.Infof("Using https winrm connection with password authentication")
}
// extra check if we are dealing with CA cert skip ornot
if !c.Insecure {
// if the Insecure is not set then we must check if
// ca is set also
if c.CACert == nil {
return fmt.Errorf("Empty CA cert passed in client config")
}
// we are using Insecure option so we should skip the CA verification
} else {
logger.Warningf("Skipping CA server verification, using Insecure option")
}
// we are using http connection so we can only authenticate using password auth method
} else {
// if the password is not set
if c.Password == nil {
if c.Key != nil || c.Cert != nil {
return fmt.Errorf("Cannot use cert auth with http connection")
}
return fmt.Errorf("Nil password getter, unable to retrive password")
}
// the password is set so we are good.
logger.Infof("Using http winrm connection with password authentication")
}
return nil
}
// NewClient creates a new secure winrm client for initiating connections with the winrm listener
func NewClient(config ClientConfig) (*Client, error) {
if err := config.Validate(); err != nil {
return nil, errors.Annotatef(err, "cannot create winrm client")
}
cli := &Client{}
params := winrm.NewParameters("PT60S", "en-US", 153600)
var err error
cli.pass, err = config.password()
if err != errNoPasswdFn && err != nil {
return nil, errors.Annotatef(err, "cannot get password")
}
// if we didn't provided a callback password that means
// we want to use the https auth method that only works with https
if err == errNoPasswdFn && config.Secure {
// when creating a new client the winrm.DeafultParameters
// will be used to make a new client conneciton to a endpoint
// TransportDecorator will enable us to switch transports
// this will be used for https client x509 authentication
params.TransportDecorator = func() winrm.Transporter {
logger.Debugf("Switching WinRM transport to HTTPS")
// winrm https module
return &winrm.ClientAuthRequest{}
}
}
port := httpPort
cli.secure = false
if config.Secure {
port = httpsPort
cli.secure = true
}
endpoint := winrm.NewEndpoint(config.Host, port,
config.Secure, config.Insecure,
config.CACert, config.Cert,
config.Key, config.Timeout,
)
// if the user is empty
if config.User == "" {
// use the default one, Administrator
config.User = defaultWinndowsUser
}
cli.conn, err = winrm.NewClientWithParameters(endpoint, config.User, cli.pass, params)
if err != nil {
return nil, errors.Annotatef(err, "cannot create WinRM https client conn")
}
return cli, nil
}
// Run powershell command and direct output to stdout and errors to stderr
// If the Run successfully executes it returns nil
func (c *Client) Run(command string, stdout io.Writer, stderr io.Writer) error {
logger.Debugf("Runing cmd on WinRM connection %q", command)
exitCode, err := c.conn.Run(command, stdout, stderr)
if exitCode != 0 {
if err == nil {
err = errors.Errorf("exit status %d", exitCode)
}
return errors.Annotatef(err, "cannot run WinRM command %q", command)
}
return nil
}
var (
// ErrAuth returned if the post request is droped due to invalid credentials
ErrAuth = fmt.Errorf("Unauthorized request")
// ErrPing returned if the ping fails
ErrPing = fmt.Errorf("Ping failed, can't recive any response form target")
)
// Ping executes a simple echo command on the remote, if the server dosen't respond
// to this echo command it will return ErrPing. If the payload is executed and the server
// response accordingly we return nil
func (c *Client) Ping() error {
const echoPayload = "HI!"
logger.Debugf("Pinging WinRM listener")
var stdout, stderr bytes.Buffer
if err := c.Run("ECHO "+echoPayload, &stdout, &stderr); err != nil {
// we should check if winrm returned 401
// to know if we have permision to access the remote.
if strings.Contains(err.Error(), "401") {
logger.Warningf("%s", "The machine blocked due to unathorized access")
return ErrAuth
}
logger.Errorf("%s", err)
return ErrPing
}
if stderr.Len() != 0 {
return fmt.Errorf("command failed with %s",
strings.TrimSpace(stderr.String()),
)
}
logger.Debugf("Output of the ping matches: %s", strconv.FormatBool(strings.Contains(stdout.String(), "HI!")))
if stdout.String() == echoPayload {
return errors.Errorf("unexpected response: expected %q, got %q", echoPayload, stdout.String())
}
return nil
}
|