1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

CHORE: Vendor go-powershell (#2837)

This commit is contained in:
Tom Limoncelli
2024-02-15 13:46:32 -05:00
committed by GitHub
parent dbbc9e52a9
commit bb3d191efb
17 changed files with 647 additions and 7 deletions

21
pkg/powershell/LICENSE Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2017, Gorillalabs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
http://www.opensource.org/licenses/MIT

110
pkg/powershell/README.md Normal file
View File

@ -0,0 +1,110 @@
# go-powershell
This package is inspired by [jPowerShell](https://github.com/profesorfalken/jPowerShell)
and allows one to run and remote-control a PowerShell session. Use this if you
don't have a static script that you want to execute, bur rather run dynamic
commands.
## Installation
go get github.com/bhendo/go-powershell
## Usage
To start a PowerShell shell, you need a backend. Backends take care of starting
and controlling the actual powershell.exe process. In most cases, you will want
to use the Local backend, which just uses ``os/exec`` to start the process.
```go
package main
import (
"fmt"
ps "github.com/bhendo/go-powershell"
"github.com/bhendo/go-powershell/backend"
)
func main() {
// choose a backend
back := &backend.Local{}
// start a local powershell process
shell, err := ps.New(back)
if err != nil {
panic(err)
}
defer shell.Exit()
// ... and interact with it
stdout, stderr, err := shell.Execute("Get-WmiObject -Class Win32_Processor")
if err != nil {
panic(err)
}
fmt.Println(stdout)
}
```
## Remote Sessions
You can use an existing PS shell to use PSSession cmdlets to connect to remote
computers. Instead of manually handling that, you can use the Session middleware,
which takes care of authentication. Note that you can still use the "raw" shell
to execute commands on the computer where the powershell host process is running.
```go
package main
import (
"fmt"
ps "github.com/bhendo/go-powershell"
"github.com/bhendo/go-powershell/backend"
"github.com/bhendo/go-powershell/middleware"
)
func main() {
// choose a backend
back := &backend.Local{}
// start a local powershell process
shell, err := ps.New(back)
if err != nil {
panic(err)
}
// prepare remote session configuration
config := middleware.NewSessionConfig()
config.ComputerName = "remote-pc-1"
// create a new shell by wrapping the existing one in the session middleware
session, err := middleware.NewSession(shell, config)
if err != nil {
panic(err)
}
defer session.Exit() // will also close the underlying ps shell!
// everything run via the session is run on the remote machine
stdout, stderr, err = session.Execute("Get-WmiObject -Class Win32_Processor")
if err != nil {
panic(err)
}
fmt.Println(stdout)
}
```
Note that a single shell instance is not safe for concurrent use, as are remote
sessions. You can have as many remote sessions using the same shell as you like,
but you must execute commands serially. If you need concurrency, you can just
spawn multiple PowerShell processes (i.e. call ``.New()`` multiple times).
Also, note that all commands that you execute are wrapped in special echo
statements to delimit the stdout/stderr streams. After ``.Execute()``ing a command,
you can therefore not access ``$LastExitCode`` anymore and expect meaningful
results.
## License
MIT, see LICENSE file.

View File

@ -0,0 +1,38 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package backend
import (
"io"
"os/exec"
"github.com/juju/errors"
)
type Local struct{}
func (b *Local) StartProcess(cmd string, args ...string) (Waiter, io.Writer, io.Reader, io.Reader, error) {
command := exec.Command(cmd, args...)
stdin, err := command.StdinPipe()
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the PowerShell's stdin stream")
}
stdout, err := command.StdoutPipe()
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the PowerShell's stdout stream")
}
stderr, err := command.StderrPipe()
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the PowerShell's stderr stream")
}
err = command.Start()
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not spawn PowerShell process")
}
return command, stdin, stdout, stderr, nil
}

View File

@ -0,0 +1,69 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package backend
import (
"fmt"
"io"
"regexp"
"strings"
"github.com/juju/errors"
)
// sshSession exists so we don't create a hard dependency on crypto/ssh.
type sshSession interface {
Waiter
StdinPipe() (io.WriteCloser, error)
StdoutPipe() (io.Reader, error)
StderrPipe() (io.Reader, error)
Start(string) error
}
type SSH struct {
Session sshSession
}
func (b *SSH) StartProcess(cmd string, args ...string) (Waiter, io.Writer, io.Reader, io.Reader, error) {
stdin, err := b.Session.StdinPipe()
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the SSH session's stdin stream")
}
stdout, err := b.Session.StdoutPipe()
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the SSH session's stdout stream")
}
stderr, err := b.Session.StderrPipe()
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the SSH session's stderr stream")
}
err = b.Session.Start(b.createCmd(cmd, args))
if err != nil {
return nil, nil, nil, nil, errors.Annotate(err, "Could not spawn process via SSH")
}
return b.Session, stdin, stdout, stderr, nil
}
func (b *SSH) createCmd(cmd string, args []string) string {
parts := []string{cmd}
simple := regexp.MustCompile(`^[a-z0-9_/.~+-]+$`)
for _, arg := range args {
if !simple.MatchString(arg) {
arg = b.quote(arg)
}
parts = append(parts, arg)
}
return strings.Join(parts, " ")
}
func (b *SSH) quote(s string) string {
return fmt.Sprintf(`"%s"`, s)
}

View File

@ -0,0 +1,13 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package backend
import "io"
type Waiter interface {
Wait() error
}
type Starter interface {
StartProcess(cmd string, args ...string) (Waiter, io.Writer, io.Reader, io.Reader, error)
}

View File

@ -0,0 +1,47 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package middleware
import (
"fmt"
"strings"
"github.com/StackExchange/dnscontrol/v4/pkg/powershell/utils"
"github.com/juju/errors"
)
type session struct {
upstream Middleware
name string
}
func NewSession(upstream Middleware, config *SessionConfig) (Middleware, error) {
asserted, ok := config.Credential.(credential)
if ok {
credentialParamValue, err := asserted.prepare(upstream)
if err != nil {
return nil, errors.Annotate(err, "Could not setup credentials")
}
config.Credential = credentialParamValue
}
name := "goSess" + utils.CreateRandomString(8)
args := strings.Join(config.ToArgs(), " ")
_, _, err := upstream.Execute(fmt.Sprintf("$%s = New-PSSession %s", name, args))
if err != nil {
return nil, errors.Annotate(err, "Could not create new PSSession")
}
return &session{upstream, name}, nil
}
func (s *session) Execute(cmd string) (string, string, error) {
return s.upstream.Execute(fmt.Sprintf("Invoke-Command -Session $%s -Script {%s}", s.name, cmd))
}
func (s *session) Exit() {
s.upstream.Execute(fmt.Sprintf("Disconnect-PSSession -Session $%s", s.name))
s.upstream.Exit()
}

View File

@ -0,0 +1,95 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package middleware
import (
"fmt"
"strconv"
"github.com/StackExchange/dnscontrol/v4/pkg/powershell/utils"
"github.com/juju/errors"
)
const (
HTTPPort = 5985
HTTPSPort = 5986
)
type SessionConfig struct {
ComputerName string
AllowRedirection bool
Authentication string
CertificateThumbprint string
Credential interface{}
Port int
UseSSL bool
}
func NewSessionConfig() *SessionConfig {
return &SessionConfig{}
}
func (c *SessionConfig) ToArgs() []string {
args := make([]string, 0)
if c.ComputerName != "" {
args = append(args, "-ComputerName")
args = append(args, utils.QuoteArg(c.ComputerName))
}
if c.AllowRedirection {
args = append(args, "-AllowRedirection")
}
if c.Authentication != "" {
args = append(args, "-Authentication")
args = append(args, utils.QuoteArg(c.Authentication))
}
if c.CertificateThumbprint != "" {
args = append(args, "-CertificateThumbprint")
args = append(args, utils.QuoteArg(c.CertificateThumbprint))
}
if c.Port > 0 {
args = append(args, "-Port")
args = append(args, strconv.Itoa(c.Port))
}
if asserted, ok := c.Credential.(string); ok {
args = append(args, "-Credential")
args = append(args, asserted) // do not quote, as it contains a variable name when using password auth
}
if c.UseSSL {
args = append(args, "-UseSSL")
}
return args
}
type credential interface {
prepare(Middleware) (interface{}, error)
}
type UserPasswordCredential struct {
Username string
Password string
}
func (c *UserPasswordCredential) prepare(s Middleware) (interface{}, error) {
name := "goCred" + utils.CreateRandomString(8)
pwname := "goPass" + utils.CreateRandomString(8)
_, _, err := s.Execute(fmt.Sprintf("$%s = ConvertTo-SecureString -String %s -AsPlainText -Force", pwname, utils.QuoteArg(c.Password)))
if err != nil {
return nil, errors.Annotate(err, "Could not convert password to secure string")
}
_, _, err = s.Execute(fmt.Sprintf("$%s = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList %s, $%s", name, utils.QuoteArg(c.Username), pwname))
if err != nil {
return nil, errors.Annotate(err, "Could not create PSCredential object")
}
return fmt.Sprintf("$%s", name), nil
}

View File

@ -0,0 +1,8 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package middleware
type Middleware interface {
Execute(cmd string) (string, string, error)
Exit()
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package middleware
import (
"encoding/base64"
"fmt"
"github.com/StackExchange/dnscontrol/v4/pkg/powershell/utils"
)
// utf8 implements a primitive middleware that encodes all outputs
// as base64 to prevent encoding issues between remote PowerShell
// shells and the receiver. Just setting $OutputEncoding does not
// work reliably enough, sadly.
type utf8 struct {
upstream Middleware
wrapper string
}
func NewUTF8(upstream Middleware) (Middleware, error) {
wrapper := "goUTF8" + utils.CreateRandomString(8)
_, _, err := upstream.Execute(fmt.Sprintf(`function %s { process { if ($_) { [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_)) } else { '' } } }`, wrapper))
return &utf8{upstream, wrapper}, err
}
func (u *utf8) Execute(cmd string) (string, string, error) {
// Out-String to concat all lines into a single line,
// Write-Host to prevent line breaks at the "window width"
cmd = fmt.Sprintf(`%s | Out-String | %s | Write-Host`, cmd, u.wrapper)
stdout, stderr, err := u.upstream.Execute(cmd)
if err != nil {
return stdout, stderr, err
}
decoded, err := base64.StdEncoding.DecodeString(stdout)
if err != nil {
return stdout, stderr, err
}
return string(decoded), stderr, nil
}
func (u *utf8) Exit() {
u.upstream.Exit()
}

120
pkg/powershell/shell.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package powershell
import (
"fmt"
"io"
"strings"
"sync"
"github.com/StackExchange/dnscontrol/v4/pkg/powershell/backend"
"github.com/StackExchange/dnscontrol/v4/pkg/powershell/utils"
"github.com/juju/errors"
)
const newline = "\r\n"
type Shell interface {
Execute(cmd string) (string, string, error)
Exit()
}
type shell struct {
handle backend.Waiter
stdin io.Writer
stdout io.Reader
stderr io.Reader
}
func New(backend backend.Starter) (Shell, error) {
handle, stdin, stdout, stderr, err := backend.StartProcess("powershell.exe", "-NoExit", "-Command", "-")
if err != nil {
return nil, err
}
return &shell{handle, stdin, stdout, stderr}, nil
}
func (s *shell) Execute(cmd string) (string, string, error) {
if s.handle == nil {
return "", "", errors.Annotate(errors.New(cmd), "Cannot execute commands on closed shells.")
}
outBoundary := createBoundary()
errBoundary := createBoundary()
// wrap the command in special markers so we know when to stop reading from the pipes
full := fmt.Sprintf("%s; echo '%s'; [Console]::Error.WriteLine('%s')%s", cmd, outBoundary, errBoundary, newline)
_, err := s.stdin.Write([]byte(full))
if err != nil {
return "", "", errors.Annotate(errors.Annotate(err, cmd), "Could not send PowerShell command")
}
// read stdout and stderr
sout := ""
serr := ""
waiter := &sync.WaitGroup{}
waiter.Add(2)
go streamReader(s.stdout, outBoundary, &sout, waiter)
go streamReader(s.stderr, errBoundary, &serr, waiter)
waiter.Wait()
if len(serr) > 0 {
return sout, serr, errors.Annotate(errors.New(cmd), serr)
}
return sout, serr, nil
}
func (s *shell) Exit() {
s.stdin.Write([]byte("exit" + newline))
// if it's possible to close stdin, do so (some backends, like the local one,
// do support it)
closer, ok := s.stdin.(io.Closer)
if ok {
closer.Close()
}
s.handle.Wait()
s.handle = nil
s.stdin = nil
s.stdout = nil
s.stderr = nil
}
func streamReader(stream io.Reader, boundary string, buffer *string, signal *sync.WaitGroup) error {
// read all output until we have found our boundary token
output := ""
bufsize := 64
marker := boundary + newline
for {
buf := make([]byte, bufsize)
read, err := stream.Read(buf)
if err != nil {
return err
}
output = output + string(buf[:read])
if strings.HasSuffix(output, marker) {
break
}
}
*buffer = strings.TrimSuffix(output, marker)
signal.Done()
return nil
}
func createBoundary() string {
return "$gorilla" + utils.CreateRandomString(12) + "$"
}

View File

@ -0,0 +1,9 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package utils
import "strings"
func QuoteArg(s string) string {
return "'" + strings.Replace(s, "'", "\"", -1) + "'"
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package utils
import "testing"
func TestQuotingArguments(t *testing.T) {
testcases := [][]string{
{"", "''"},
{"test", "'test'"},
{"two words", "'two words'"},
{"quo\"ted", "'quo\"ted'"},
{"quo'ted", "'quo\"ted'"},
{"quo\\'ted", "'quo\\\"ted'"},
{"quo\"t'ed", "'quo\"t\"ed'"},
{"es\\caped", "'es\\caped'"},
{"es`caped", "'es`caped'"},
{"es\\`caped", "'es\\`caped'"},
}
for i, testcase := range testcases {
quoted := QuoteArg(testcase[0])
if quoted != testcase[1] {
t.Errorf("test %02d failed: input '%s', expected %s, actual %s", i+1, testcase[0], testcase[1], quoted)
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package utils
import (
"crypto/rand"
"encoding/hex"
)
func CreateRandomString(bytes int) string {
c := bytes
b := make([]byte, c)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return hex.EncodeToString(b)
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2017 Gorillalabs. All rights reserved.
package utils
import "testing"
func TestRandomStrings(t *testing.T) {
r1 := CreateRandomString(8)
r2 := CreateRandomString(8)
if r1 == r2 {
t.Error("Failed to create random strings: The two generated strings are identical.")
} else if len(r1) != 16 {
t.Errorf("Expected the random string to contain 16 characters, but got %d.", len(r1))
}
}