2014-11-07 07:13:15 +03:00
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
//
|
|
|
|
// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr]
|
2015-08-27 17:41:13 +03:00
|
|
|
|
2015-08-26 21:15:40 +03:00
|
|
|
package client /* import "mig.ninja/mig/client" */
|
2014-11-07 07:13:15 +03:00
|
|
|
|
|
|
|
import (
|
2015-04-07 16:52:32 +03:00
|
|
|
"bufio"
|
2014-11-16 00:21:43 +03:00
|
|
|
"bytes"
|
2014-11-07 17:37:07 +03:00
|
|
|
"crypto/tls"
|
2015-04-07 16:52:32 +03:00
|
|
|
"encoding/hex"
|
2014-11-07 07:13:15 +03:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2014-11-16 00:21:43 +03:00
|
|
|
"io"
|
2014-11-07 07:13:15 +03:00
|
|
|
"io/ioutil"
|
2014-11-16 00:21:43 +03:00
|
|
|
"mime/multipart"
|
2014-11-07 07:13:15 +03:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"os/user"
|
2014-11-07 17:37:46 +03:00
|
|
|
"strings"
|
2014-11-07 07:13:15 +03:00
|
|
|
"time"
|
2015-09-23 19:57:39 +03:00
|
|
|
|
2016-01-14 23:25:36 +03:00
|
|
|
"github.com/cheggaaa/pb"
|
2015-09-23 19:57:39 +03:00
|
|
|
"github.com/jvehent/cljs"
|
|
|
|
"golang.org/x/crypto/openpgp"
|
|
|
|
"gopkg.in/gcfg.v1"
|
|
|
|
"mig.ninja/mig"
|
|
|
|
"mig.ninja/mig/modules"
|
|
|
|
"mig.ninja/mig/pgp"
|
2014-11-07 07:13:15 +03:00
|
|
|
)
|
|
|
|
|
2014-11-15 21:20:26 +03:00
|
|
|
var version string
|
2014-11-07 17:37:46 +03:00
|
|
|
|
2014-11-07 07:13:15 +03:00
|
|
|
// A Client provides all the needed functionalities to interact with the MIG API.
|
|
|
|
// It should be initialized with a proper configuration file.
|
|
|
|
type Client struct {
|
2015-03-16 17:43:39 +03:00
|
|
|
API *http.Client
|
|
|
|
Token string
|
|
|
|
Conf Configuration
|
|
|
|
Version string
|
2015-07-10 18:58:18 +03:00
|
|
|
debug bool
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Configuration stores the live configuration and global parameters of a client
|
|
|
|
type Configuration struct {
|
2015-03-16 17:43:39 +03:00
|
|
|
API ApiConf // location of the MIG API
|
|
|
|
Homedir string // location of the user's home directory
|
|
|
|
GPG GpgConf // location of the user's secring
|
2014-11-07 17:37:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type ApiConf struct {
|
|
|
|
URL string
|
|
|
|
SkipVerifyCert bool
|
|
|
|
}
|
|
|
|
type GpgConf struct {
|
|
|
|
Home string
|
|
|
|
KeyID string
|
|
|
|
Keyserver string
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
|
2015-09-17 21:43:11 +03:00
|
|
|
// Can store the passphrase used to decrypt a GPG private key so the client
|
|
|
|
// does not attempt to prompt for it. We do not store it in the client
|
|
|
|
// configuration, as under normal usage passphrases for MIG should not be
|
|
|
|
// stored in cleartext. However in some cases such as with mig-runner this
|
|
|
|
// behavior is required for automated operation.
|
|
|
|
var clientPassphrase string
|
|
|
|
|
|
|
|
// Set the GPG passphrase to be used by the client for secret key operations.
|
|
|
|
func ClientPassphrase(s string) {
|
|
|
|
clientPassphrase = s
|
|
|
|
}
|
|
|
|
|
2014-11-07 07:13:15 +03:00
|
|
|
// NewClient initiates a new instance of a Client
|
2015-04-30 17:27:34 +03:00
|
|
|
func NewClient(conf Configuration, version string) (cli Client, err error) {
|
2015-03-16 17:43:39 +03:00
|
|
|
cli.Version = version
|
2014-11-07 07:13:15 +03:00
|
|
|
cli.Conf = conf
|
|
|
|
tr := &http.Transport{
|
|
|
|
DisableCompression: false,
|
|
|
|
DisableKeepAlives: false,
|
2014-11-07 17:37:07 +03:00
|
|
|
TLSClientConfig: &tls.Config{
|
2015-10-18 16:11:14 +03:00
|
|
|
MinVersion: tls.VersionTLS12,
|
2014-11-07 17:37:07 +03:00
|
|
|
CipherSuites: []uint16{
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
2015-06-03 01:04:55 +03:00
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
2014-11-07 17:37:07 +03:00
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
|
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
|
|
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
|
|
},
|
|
|
|
InsecureSkipVerify: conf.API.SkipVerifyCert,
|
|
|
|
},
|
2015-09-29 01:12:30 +03:00
|
|
|
Proxy: http.ProxyFromEnvironment,
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
cli.API = &http.Client{Transport: tr}
|
2015-04-30 17:27:34 +03:00
|
|
|
// if the env variable to the gpg agent socket isn't set, try to
|
|
|
|
// find the socket and set the variable
|
|
|
|
if os.Getenv("GPG_AGENT_INFO") == "" {
|
|
|
|
_, err = os.Stat(conf.GPG.Home + "/S.gpg-agent")
|
|
|
|
if err == nil {
|
|
|
|
// socket was found, set it
|
|
|
|
os.Setenv("GPG_AGENT_INFO", conf.GPG.Home+"/S.gpg-agent")
|
|
|
|
}
|
|
|
|
}
|
2015-09-17 21:43:11 +03:00
|
|
|
if clientPassphrase != "" {
|
|
|
|
pgp.CachePassphrase(clientPassphrase)
|
|
|
|
}
|
2015-04-30 17:27:34 +03:00
|
|
|
// try to make a signed token, just to check that we can access the private key
|
|
|
|
_, err = cli.MakeSignedToken()
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("failed to generate a security token using key %s from %s\n",
|
|
|
|
conf.GPG.KeyID, conf.GPG.Home+"/secring.gpg")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// ReadConfiguration loads a client configuration from a local configuration file
|
|
|
|
// and verifies that GnuPG's secring is available
|
|
|
|
func ReadConfiguration(file string) (conf Configuration, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("ReadConfiguration() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
2014-11-15 21:20:26 +03:00
|
|
|
_, err = os.Stat(file)
|
|
|
|
if err != nil {
|
2015-04-07 16:52:32 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "no configuration file found at %s\n", file)
|
|
|
|
err = MakeConfiguration(file)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2014-11-15 21:20:26 +03:00
|
|
|
}
|
2014-11-07 07:13:15 +03:00
|
|
|
err = gcfg.ReadFileInto(&conf, file)
|
|
|
|
if conf.GPG.Home == "" {
|
|
|
|
gnupgdir := os.Getenv("GNUPGHOME")
|
|
|
|
if gnupgdir == "" {
|
|
|
|
gnupgdir = "/.gnupg"
|
|
|
|
}
|
|
|
|
conf.GPG.Home = FindHomedir() + gnupgdir
|
|
|
|
}
|
|
|
|
_, err = os.Stat(conf.GPG.Home + "/secring.gpg")
|
|
|
|
if err != nil {
|
|
|
|
panic("secring.gpg not found")
|
|
|
|
}
|
|
|
|
// if trailing slash is missing from API url, add it
|
|
|
|
if conf.API.URL[len(conf.API.URL)-1] != '/' {
|
|
|
|
conf.API.URL += "/"
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func FindHomedir() string {
|
2015-04-07 16:52:32 +03:00
|
|
|
if os.Getenv("HOME") != "" {
|
2014-11-07 07:13:15 +03:00
|
|
|
return os.Getenv("HOME")
|
2015-04-07 16:52:32 +03:00
|
|
|
}
|
|
|
|
// find keyring in default location
|
|
|
|
u, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return u.HomeDir
|
|
|
|
}
|
|
|
|
|
|
|
|
// MakeConfiguration generates a new configuration file for the current user
|
|
|
|
func MakeConfiguration(file string) (err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("MakeConfiguration() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
var cfg Configuration
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
|
|
fmt.Printf("would you like to generate a new configuration file at %s? Y/n> ", file)
|
|
|
|
scanner.Scan()
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if scanner.Text() != "y" && scanner.Text() != "Y" && scanner.Text() != "" {
|
|
|
|
panic("abort")
|
|
|
|
}
|
|
|
|
cfg.Homedir = FindHomedir()
|
|
|
|
cfg.GPG.Home = cfg.Homedir + "/.gnupg/"
|
|
|
|
_, err = os.Stat(cfg.GPG.Home + "secring.gpg")
|
|
|
|
if err != nil {
|
|
|
|
panic("couldn't find secring at " + cfg.GPG.Home + "secring.gpg")
|
|
|
|
}
|
|
|
|
sr, err := os.Open(cfg.GPG.Home + "secring.gpg")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer sr.Close()
|
|
|
|
keyring, err := openpgp.ReadKeyRing(sr)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
for _, entity := range keyring {
|
|
|
|
fingerprint := strings.ToUpper(hex.EncodeToString(entity.PrivateKey.PublicKey.Fingerprint[:]))
|
|
|
|
// get the first name from the key identity
|
|
|
|
var name string
|
|
|
|
for _, identity := range entity.Identities {
|
|
|
|
name = identity.Name
|
|
|
|
break
|
|
|
|
}
|
|
|
|
fmt.Printf("found key '%s' with fingerprint '%s'.\nuse this key? Y/n> ", name, fingerprint)
|
|
|
|
scanner.Scan()
|
|
|
|
if err := scanner.Err(); err != nil {
|
2014-11-07 07:13:15 +03:00
|
|
|
panic(err)
|
|
|
|
}
|
2015-04-07 16:52:32 +03:00
|
|
|
if scanner.Text() == "y" || scanner.Text() == "Y" || scanner.Text() == "" {
|
|
|
|
fmt.Printf("using key %s\n", fingerprint)
|
|
|
|
cfg.GPG.KeyID = fingerprint
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if cfg.GPG.KeyID == "" {
|
2015-10-09 10:00:56 +03:00
|
|
|
panic("no suitable key found in " + sr.Name())
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
2015-04-07 16:52:32 +03:00
|
|
|
for {
|
|
|
|
fmt.Printf("what is the location of the API? (ex: https://mig.example.net/api/v1/) > ")
|
|
|
|
scanner.Scan()
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
cfg.API.URL = scanner.Text()
|
|
|
|
_, err := http.Get(cfg.API.URL)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("API connection failed. Wrong address?")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
fd, err := os.Create(file)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
fmt.Fprintf(fd, "[api]\n\turl = \"%s\"\n", cfg.API.URL)
|
|
|
|
fmt.Fprintf(fd, "[gpg]\n\thome = \"%s\"\n\tkeyid = \"%s\"\n", cfg.GPG.Home, cfg.GPG.KeyID)
|
|
|
|
fd.Close()
|
|
|
|
fmt.Println("MIG client configuration generated at", file)
|
|
|
|
return
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
|
2014-11-07 17:37:46 +03:00
|
|
|
// Do is a thin wrapper around http.Client.Do() that inserts an authentication header
|
|
|
|
// to the outgoing request
|
|
|
|
func (cli Client) Do(r *http.Request) (resp *http.Response, err error) {
|
2014-11-07 07:13:15 +03:00
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
2014-11-07 17:37:46 +03:00
|
|
|
err = fmt.Errorf("Do() -> %v", e)
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
}()
|
2015-03-16 17:43:39 +03:00
|
|
|
r.Header.Set("User-Agent", "MIG Client "+cli.Version)
|
2014-11-07 07:13:15 +03:00
|
|
|
if cli.Token == "" {
|
|
|
|
cli.Token, err = cli.MakeSignedToken()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
r.Header.Set("X-PGPAUTHORIZATION", cli.Token)
|
2015-07-10 18:58:18 +03:00
|
|
|
if cli.debug {
|
|
|
|
fmt.Printf("debug: %s %s %s\ndebug: User-Agent: %s\ndebug: X-PGPAUTHORIZATION: %s\n",
|
|
|
|
r.Method, r.URL.String(), r.Proto, r.UserAgent(), r.Header.Get("X-PGPAUTHORIZATION"))
|
|
|
|
}
|
2014-11-07 07:13:15 +03:00
|
|
|
// execute the request
|
2014-11-07 17:37:46 +03:00
|
|
|
resp, err = cli.API.Do(r)
|
2015-10-05 17:50:29 +03:00
|
|
|
if resp == nil {
|
|
|
|
panic("failed to contact the API")
|
|
|
|
}
|
2014-11-07 07:13:15 +03:00
|
|
|
if err != nil {
|
2015-01-26 02:43:22 +03:00
|
|
|
msg := fmt.Errorf("request failed error: %d %s (%v)", resp.StatusCode, resp.Status, err)
|
|
|
|
panic(msg)
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
// if the request failed because of an auth issue, it may be that the auth token has expired.
|
|
|
|
// try the request again with a fresh token
|
|
|
|
if resp.StatusCode == 401 {
|
2014-11-07 17:37:46 +03:00
|
|
|
resp.Body.Close()
|
2014-11-07 07:13:15 +03:00
|
|
|
cli.Token, err = cli.MakeSignedToken()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
r.Header.Set("X-PGPAUTHORIZATION", cli.Token)
|
2015-07-10 18:58:18 +03:00
|
|
|
if cli.debug {
|
|
|
|
fmt.Printf("debug: %s %s %s\ndebug: User-Agent: %s\ndebug: X-PGPAUTHORIZATION: %s\n",
|
|
|
|
r.Method, r.URL.String(), r.Proto, r.UserAgent(), r.Header.Get("X-PGPAUTHORIZATION"))
|
|
|
|
}
|
2014-11-07 07:13:15 +03:00
|
|
|
// execute the request
|
|
|
|
resp, err = cli.API.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
2014-11-07 17:37:46 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAPIResource retrieves a cljs resource from a target endpoint. The target must be the relative
|
|
|
|
// to the API URL passed in the configuration. For example, if the API URL is `http://localhost:12345/api/v1/`
|
|
|
|
// then target could only be set to `dashboard` to retrieve `http://localhost:12345/api/v1/dashboard`
|
|
|
|
func (cli Client) GetAPIResource(target string) (resource *cljs.Resource, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("GetAPIResource() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
r, err := http.NewRequest("GET", cli.Conf.API.URL+target, nil)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
resp, err := cli.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2015-07-17 11:48:47 +03:00
|
|
|
hasResource := false
|
2015-10-05 17:50:29 +03:00
|
|
|
if resp.Body != nil && resp.StatusCode < 500 {
|
2015-09-18 19:03:42 +03:00
|
|
|
defer resp.Body.Close()
|
2015-01-26 02:43:22 +03:00
|
|
|
// unmarshal the body. don't attempt to interpret it, as long as it
|
|
|
|
// fits into a cljs.Resource, it's acceptable
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
2014-11-07 07:13:15 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2015-01-26 02:43:22 +03:00
|
|
|
if len(body) > 1 {
|
2015-07-10 18:58:18 +03:00
|
|
|
if cli.debug {
|
|
|
|
fmt.Printf("debug: RESPONSE BODY:\ndebug: %s\n", body)
|
|
|
|
}
|
2015-01-26 02:43:22 +03:00
|
|
|
err = json.Unmarshal(body, &resource)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2015-07-17 11:48:47 +03:00
|
|
|
hasResource = true
|
2015-07-10 18:58:18 +03:00
|
|
|
} else {
|
|
|
|
if cli.debug {
|
|
|
|
fmt.Printf("debug: RESPONSE BODY: EMPTY\n")
|
|
|
|
}
|
2015-01-26 02:43:22 +03:00
|
|
|
}
|
2014-11-07 07:13:15 +03:00
|
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
2015-07-17 11:48:47 +03:00
|
|
|
if hasResource {
|
|
|
|
err = fmt.Errorf("error: HTTP %d. API call failed with error '%v' (code %s)",
|
|
|
|
resp.StatusCode, resource.Collection.Error.Message, resource.Collection.Error.Code)
|
|
|
|
} else {
|
|
|
|
err = fmt.Errorf("error: HTTP %d %s. No response body.", resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
|
|
}
|
2014-11-07 07:13:15 +03:00
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAction retrieves a MIG Action from the API using its Action ID
|
|
|
|
func (cli Client) GetAction(aid float64) (a mig.Action, links []cljs.Link, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("GetAction() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
target := fmt.Sprintf("action?actionid=%.0f", aid)
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if resource.Collection.Items[0].Data[0].Name != "action" {
|
|
|
|
panic("API returned something that is not an action... something's wrong.")
|
|
|
|
}
|
|
|
|
a, err = ValueToAction(resource.Collection.Items[0].Data[0].Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
links = resource.Collection.Items[0].Links
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-07-31 00:36:02 +03:00
|
|
|
// GetManifestRecord retrieves a MIG manifest record from the API using the
|
|
|
|
// record ID
|
|
|
|
func (cli Client) GetManifestRecord(mid float64) (mr mig.ManifestRecord, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("GetManifestRecord() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
target := fmt.Sprintf("manifest?manifestid=%.0f", mid)
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if resource.Collection.Items[0].Data[0].Name != "manifest" {
|
|
|
|
panic("API returned something that is not a manifest... something's wrong.")
|
|
|
|
}
|
|
|
|
mr, err = ValueToManifestRecord(resource.Collection.Items[0].Data[0].Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-02-03 23:39:21 +03:00
|
|
|
// Change the status of an existing manifest record
|
|
|
|
func (cli Client) ManifestRecordStatus(mr mig.ManifestRecord, status string) (err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("ResetManifestRecord() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
data := url.Values{"manifestid": {fmt.Sprintf("%.0f", mr.ID)}, "status": {status}}
|
|
|
|
r, err := http.NewRequest("POST", cli.Conf.API.URL+"manifest/status/",
|
|
|
|
strings.NewReader(data.Encode()))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := cli.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
var resource *cljs.Resource
|
|
|
|
if len(body) > 1 {
|
|
|
|
err = json.Unmarshal(body, &resource)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
err = fmt.Errorf("error: HTTP %d. Status update failed with error '%v' (code %s).",
|
|
|
|
resp.StatusCode, resource.Collection.Error.Message, resource.Collection.Error.Code)
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-02-03 22:13:57 +03:00
|
|
|
// Add a new signature to an existing manifest known to the API
|
2015-07-31 00:36:02 +03:00
|
|
|
func (cli Client) PostManifestSignature(mr mig.ManifestRecord, sig string) (err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("PostManifestSignature() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
data := url.Values{"manifestid": {fmt.Sprintf("%.0f", mr.ID)}, "signature": {sig}}
|
|
|
|
r, err := http.NewRequest("POST", cli.Conf.API.URL+"manifest/sign/",
|
|
|
|
strings.NewReader(data.Encode()))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := cli.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
var resource *cljs.Resource
|
2016-02-03 22:13:57 +03:00
|
|
|
if len(body) > 1 {
|
|
|
|
err = json.Unmarshal(body, &resource)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
err = fmt.Errorf("error: HTTP %d. Signature update failed with error '%v' (code %s).",
|
|
|
|
resp.StatusCode, resource.Collection.Error.Message, resource.Collection.Error.Code)
|
2015-07-31 00:36:02 +03:00
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-11-07 07:13:15 +03:00
|
|
|
// PostAction submits a MIG Action to the API and returns the reflected action with API ID
|
|
|
|
func (cli Client) PostAction(a mig.Action) (a2 mig.Action, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("PostAction() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
2014-11-24 19:57:11 +03:00
|
|
|
a.SyntaxVersion = mig.ActionVersion
|
2014-11-07 07:13:15 +03:00
|
|
|
// serialize
|
|
|
|
ajson, err := json.Marshal(a)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
actionstr := string(ajson)
|
2014-11-07 17:37:46 +03:00
|
|
|
data := url.Values{"action": {actionstr}}
|
|
|
|
r, err := http.NewRequest("POST", cli.Conf.API.URL+"action/create/", strings.NewReader(data.Encode()))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := cli.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2014-11-07 07:13:15 +03:00
|
|
|
defer resp.Body.Close()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if resp.StatusCode != 202 {
|
|
|
|
err = fmt.Errorf("error: HTTP %d. action creation failed.", resp.StatusCode)
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
var resource *cljs.Resource
|
|
|
|
err = json.Unmarshal(body, &resource)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
a2, err = ValueToAction(resource.Collection.Items[0].Data[0].Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func ValueToAction(v interface{}) (a mig.Action, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("ValueToAction() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
bData, err := json.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(bData, &a)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-07-31 00:36:02 +03:00
|
|
|
func ValueToManifestRecord(v interface{}) (m mig.ManifestRecord, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("ValueToManifestRecord() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
bData, err := json.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(bData, &m)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-11-07 07:13:15 +03:00
|
|
|
func (cli Client) GetCommand(cmdid float64) (cmd mig.Command, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("GetCommand() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
target := "command?commandid=" + fmt.Sprintf("%.0f", cmdid)
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if resource.Collection.Items[0].Data[0].Name != "command" {
|
|
|
|
panic("API returned something that is not a command... something's wrong.")
|
|
|
|
}
|
|
|
|
cmd, err = ValueToCommand(resource.Collection.Items[0].Data[0].Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func ValueToCommand(v interface{}) (cmd mig.Command, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("ValueToCommand() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
bData, err := json.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(bData, &cmd)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cli Client) GetAgent(agtid float64) (agt mig.Agent, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("GetAgent() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
target := "agent?agentid=" + fmt.Sprintf("%.0f", agtid)
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if resource.Collection.Items[0].Data[0].Name != "agent" {
|
|
|
|
panic("API returned something that is not an agent... something's wrong.")
|
|
|
|
}
|
|
|
|
agt, err = ValueToAgent(resource.Collection.Items[0].Data[0].Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func ValueToAgent(v interface{}) (agt mig.Agent, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("valueToAgent() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
bData, err := json.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(bData, &agt)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cli Client) GetInvestigator(iid float64) (inv mig.Investigator, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("GetInvestigator() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
target := "investigator?investigatorid=" + fmt.Sprintf("%.0f", iid)
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if resource.Collection.Items[0].Data[0].Name != "investigator" {
|
|
|
|
panic("API returned something that is not an investigator... something's wrong.")
|
|
|
|
}
|
|
|
|
inv, err = ValueToInvestigator(resource.Collection.Items[0].Data[0].Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-11-16 00:21:43 +03:00
|
|
|
// PostInvestigator creates an Investigator and returns the reflected investigator
|
|
|
|
func (cli Client) PostInvestigator(name string, pubkey []byte) (inv mig.Investigator, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("PostInvestigator() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
// build the body into buf using a multipart writer
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
writer := multipart.NewWriter(buf)
|
|
|
|
// set the name form value
|
|
|
|
err = writer.WriteField("name", name)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
// set the publickey form value
|
|
|
|
part, err := writer.CreateFormFile("publickey", fmt.Sprintf("%s.asc", name))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
_, err = io.Copy(part, bytes.NewReader(pubkey))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
// must close the writer to write trailing boundary
|
|
|
|
err = writer.Close()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
// post the request
|
|
|
|
r, err := http.NewRequest("POST", cli.Conf.API.URL+"investigator/create/", buf)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
resp, err := cli.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
// get the investigator back from the response body
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
var resource *cljs.Resource
|
|
|
|
err = json.Unmarshal(body, &resource)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if resp.StatusCode != 201 {
|
|
|
|
err = fmt.Errorf("HTTP %d: %v (code %s)", resp.StatusCode,
|
|
|
|
resource.Collection.Error.Message, resource.Collection.Error.Code)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
inv, err = ValueToInvestigator(resource.Collection.Items[0].Data[0].Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// PostInvestigatorStatus updates the status of an Investigator
|
|
|
|
func (cli Client) PostInvestigatorStatus(iid float64, newstatus string) (err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("PostInvestigatorStatus() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
data := url.Values{"id": {fmt.Sprintf("%.0f", iid)}, "status": {newstatus}}
|
|
|
|
r, err := http.NewRequest("POST", cli.Conf.API.URL+"investigator/update/", strings.NewReader(data.Encode()))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := cli.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
var resource *cljs.Resource
|
|
|
|
if len(body) > 1 {
|
|
|
|
err = json.Unmarshal(body, &resource)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
err = fmt.Errorf("error: HTTP %d. status update failed with error '%v' (code %s)",
|
|
|
|
resp.StatusCode, resource.Collection.Error.Message, resource.Collection.Error.Code)
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-11-07 07:13:15 +03:00
|
|
|
func ValueToInvestigator(v interface{}) (inv mig.Investigator, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("valueToInvestigator) -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
bData, err := json.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(bData, &inv)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// MakeSignedToken encrypts a timestamp and a random number with the users GPG key
|
|
|
|
// to use as an auth token with the API
|
|
|
|
func (cli Client) MakeSignedToken() (token string, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("MakeSignedToken() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
2014-11-15 22:46:38 +03:00
|
|
|
tokenVersion := 1
|
|
|
|
str := fmt.Sprintf("%d;%s;%.0f", tokenVersion, time.Now().UTC().Format(time.RFC3339), mig.GenID())
|
2014-11-07 07:13:15 +03:00
|
|
|
secringFile, err := os.Open(cli.Conf.GPG.Home + "/secring.gpg")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2014-12-09 20:37:24 +03:00
|
|
|
defer secringFile.Close()
|
2014-11-07 07:13:15 +03:00
|
|
|
sig, err := pgp.Sign(str+"\n", cli.Conf.GPG.KeyID, secringFile)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
token = str + ";" + sig
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-01-22 21:53:11 +03:00
|
|
|
// CompressAction takens a MIG action, and applies compression to any operations
|
|
|
|
// within the action for which compression is requested.
|
|
|
|
//
|
|
|
|
// This function should be called on the action prior to signing it for submission
|
|
|
|
// to the API.
|
|
|
|
func (cli Client) CompressAction(a mig.Action) (comp_action mig.Action, err error) {
|
|
|
|
comp_action = a
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("CompressAction() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
for i := range comp_action.Operations {
|
|
|
|
if !comp_action.Operations[i].WantCompressed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if comp_action.Operations[i].IsCompressed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
err = comp_action.Operations[i].CompressOperationParam()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-11-07 07:13:15 +03:00
|
|
|
// SignAction takes a MIG Action, signs it with the key identified in the configuration
|
|
|
|
// and returns the signed action
|
|
|
|
func (cli Client) SignAction(a mig.Action) (signed_action mig.Action, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("SignAction() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
secring, err := os.Open(cli.Conf.GPG.Home + "/secring.gpg")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer secring.Close()
|
2014-11-20 21:24:05 +03:00
|
|
|
sig, err := a.Sign(cli.Conf.GPG.KeyID, secring)
|
2014-11-07 07:13:15 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
a.PGPSignatures = append(a.PGPSignatures, sig)
|
|
|
|
signed_action = a
|
|
|
|
return
|
|
|
|
}
|
2014-11-16 06:05:18 +03:00
|
|
|
|
2015-07-31 00:36:02 +03:00
|
|
|
// SignManifest takes a MIG manifest record, signs it with the key identified
|
|
|
|
// in the configuration and returns the signature
|
|
|
|
func (cli Client) SignManifest(m mig.ManifestRecord) (ret string, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("SignManifest() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
secring, err := os.Open(cli.Conf.GPG.Home + "/secring.gpg")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer secring.Close()
|
|
|
|
ret, err = m.Sign(cli.Conf.GPG.KeyID, secring)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-11-16 06:05:18 +03:00
|
|
|
// EvaluateAgentTarget runs a search against the api to find all agents that match an action target string
|
|
|
|
func (cli Client) EvaluateAgentTarget(target string) (agents []mig.Agent, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("EvaluateAgentTarget() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
2015-09-23 18:10:42 +03:00
|
|
|
query := "search?type=agent&limit=1000000&target=" + url.QueryEscape(target)
|
2014-11-16 06:05:18 +03:00
|
|
|
resource, err := cli.GetAPIResource(query)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
for _, item := range resource.Collection.Items {
|
|
|
|
for _, data := range item.Data {
|
|
|
|
if data.Name != "agent" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
agt, err := ValueToAgent(data.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
agents = append(agents, agt)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
|
|
|
|
// FollowAction continuously loops over an action and prints its completion status in os.Stderr.
|
|
|
|
// when the action reaches its expiration date, FollowAction prints its final status and returns.
|
2016-01-14 23:25:36 +03:00
|
|
|
func (cli Client) FollowAction(a mig.Action, total int) (err error) {
|
2014-11-24 20:01:26 +03:00
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("followAction() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
2016-01-14 23:25:36 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[34mFollowing action ID %.0f.\x1b[0m\n", a.ID)
|
2014-11-24 20:01:26 +03:00
|
|
|
previousctr := 0
|
|
|
|
status := ""
|
|
|
|
attempts := 0
|
|
|
|
var completion float64
|
2016-01-14 23:25:36 +03:00
|
|
|
bar := pb.New(total)
|
|
|
|
bar.ShowSpeed = true
|
|
|
|
bar.SetMaxWidth(80)
|
|
|
|
bar.Output = os.Stderr
|
|
|
|
bar.Start()
|
2014-11-24 20:01:26 +03:00
|
|
|
for {
|
|
|
|
a, _, err = cli.GetAction(a.ID)
|
|
|
|
if err != nil {
|
|
|
|
attempts++
|
2016-01-14 23:25:36 +03:00
|
|
|
time.Sleep(time.Second)
|
2014-11-24 20:01:26 +03:00
|
|
|
if attempts >= 30 {
|
|
|
|
panic("failed to retrieve action after 30 seconds. launch may have failed")
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if status == "" {
|
|
|
|
status = a.Status
|
|
|
|
}
|
|
|
|
// exit follower mode if status isn't one we follow,
|
|
|
|
// or enough commands have returned
|
|
|
|
// or expiration time has passed
|
2015-01-04 22:17:05 +03:00
|
|
|
if (status != "pending" && status != "scheduled" && status != "preparing" && status != "inflight") ||
|
2014-11-24 20:01:26 +03:00
|
|
|
(a.Counters.Done > 0 && a.Counters.Done >= a.Counters.Sent) ||
|
2015-04-21 19:13:35 +03:00
|
|
|
(time.Now().After(a.ExpireAfter.Add(10 * time.Second))) {
|
2014-11-24 20:01:26 +03:00
|
|
|
goto finish
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if a.Counters.Done > 0 && a.Counters.Done > previousctr {
|
|
|
|
completion = (float64(a.Counters.Done) / float64(a.Counters.Sent)) * 100
|
2015-04-21 19:13:35 +03:00
|
|
|
if completion < 99.5 {
|
2016-01-14 23:25:36 +03:00
|
|
|
bar.Add(a.Counters.Done - previousctr)
|
|
|
|
bar.Update()
|
2015-04-21 19:13:35 +03:00
|
|
|
previousctr = a.Counters.Done
|
2014-11-24 20:01:26 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
}
|
|
|
|
finish:
|
2016-01-14 23:25:36 +03:00
|
|
|
bar.Add(total - previousctr)
|
|
|
|
bar.Update()
|
|
|
|
bar.Finish()
|
2015-03-18 16:17:45 +03:00
|
|
|
a, _, err = cli.GetAction(a.ID)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "[error] failed to retrieve action counters\n")
|
|
|
|
} else {
|
|
|
|
completion = (float64(a.Counters.Done) / float64(a.Counters.Sent)) * 100
|
2016-01-14 23:25:36 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[34m%2.1f%% done in %s\x1b[0m\n", completion, time.Now().Sub(a.StartTime).String())
|
2015-03-18 16:17:45 +03:00
|
|
|
}
|
2015-04-21 19:13:35 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[34m")
|
2014-11-24 20:01:26 +03:00
|
|
|
a.PrintCounters()
|
2015-04-21 19:13:35 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[0m")
|
2014-11-24 20:01:26 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-09-11 01:27:59 +03:00
|
|
|
// FetchActionResults retrieves mig command results associated with a
|
|
|
|
// particular action. This function differs from PrintActionResults in
|
|
|
|
// that it returns a slice of mig.Command structs, rather then printing
|
|
|
|
// results to stdout.
|
|
|
|
//
|
|
|
|
// XXX Note in the future it may be worth refactoring the action print
|
|
|
|
// functions to make use of this, but it would require additional work.
|
|
|
|
func (cli Client) FetchActionResults(a mig.Action) (ret []mig.Command, err error) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("FetchActionResults() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
limit := 37
|
|
|
|
offset := 0
|
|
|
|
ret = make([]mig.Command, 0)
|
|
|
|
|
|
|
|
for {
|
|
|
|
target := fmt.Sprintf("search?type=command&limit=%d&offset=%d", limit, offset)
|
|
|
|
target = target + fmt.Sprintf("&actionid=%.0f", a.ID)
|
|
|
|
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
if resource.Collection.Error.Message == "no results found" {
|
|
|
|
err = nil
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
count := 0
|
|
|
|
for _, item := range resource.Collection.Items {
|
|
|
|
for _, data := range item.Data {
|
|
|
|
if data.Name != "command" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
cmd, err := ValueToCommand(data.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
ret = append(ret, cmd)
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if count == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
offset += limit
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2015-03-23 21:54:00 +03:00
|
|
|
func (cli Client) PrintActionResults(a mig.Action, show, render string) (err error) {
|
2014-11-24 20:01:26 +03:00
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("PrintActionResults() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
2015-03-23 21:54:00 +03:00
|
|
|
var (
|
2015-07-17 17:47:50 +03:00
|
|
|
found bool
|
|
|
|
report, foundQ string
|
|
|
|
locs []CommandLocation
|
2015-07-20 17:27:42 +03:00
|
|
|
limit, offset, agtCount int = 37, 0, 0
|
2015-03-23 21:54:00 +03:00
|
|
|
)
|
2015-07-17 17:47:50 +03:00
|
|
|
if render == "map" {
|
2015-03-23 21:54:00 +03:00
|
|
|
report = "&report=geolocations"
|
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
switch show {
|
2015-03-23 21:54:00 +03:00
|
|
|
case "found":
|
|
|
|
found = true
|
2015-07-17 17:47:50 +03:00
|
|
|
foundQ = "&foundanything=true"
|
2015-03-23 21:54:00 +03:00
|
|
|
case "notfound":
|
2015-07-17 17:47:50 +03:00
|
|
|
found = false
|
|
|
|
foundQ = "&foundanything=false"
|
|
|
|
case "all":
|
|
|
|
found = false
|
2014-11-24 20:01:26 +03:00
|
|
|
default:
|
|
|
|
return fmt.Errorf("invalid parameter '%s'", show)
|
|
|
|
}
|
2015-07-17 17:47:50 +03:00
|
|
|
|
|
|
|
// loop until all results have been retrieved using paginated queries
|
|
|
|
for {
|
|
|
|
target := fmt.Sprintf("search?type=command&limit=%d&offset=%d&actionid=%.0f%s%s", limit, offset, a.ID, foundQ, report)
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
// because we query using pagination, the last query will return a 404 with no result.
|
|
|
|
// When that happens, GetAPIResource returns an error which we do not report to the user
|
|
|
|
if resource.Collection.Error.Message == "no results found" {
|
|
|
|
err = nil
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
count := 0
|
|
|
|
for _, item := range resource.Collection.Items {
|
|
|
|
for _, data := range item.Data {
|
|
|
|
switch render {
|
|
|
|
case "map":
|
|
|
|
if data.Name != "geolocation" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
loc, err := ValueToLocation(data.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
locs = append(locs, loc)
|
|
|
|
default:
|
|
|
|
if data.Name != "command" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
cmd, err := ValueToCommand(data.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
err = PrintCommandResults(cmd, found, true)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
count++
|
2015-03-23 21:54:00 +03:00
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
}
|
|
|
|
}
|
2015-07-17 17:47:50 +03:00
|
|
|
// if count is still at zero, we didn't get any results from the query and exit the loop
|
|
|
|
if count == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// else increase limit and offset and continue
|
2015-07-20 17:27:42 +03:00
|
|
|
offset += limit
|
2015-07-17 17:47:50 +03:00
|
|
|
agtCount += count
|
2014-11-24 20:01:26 +03:00
|
|
|
}
|
2015-03-23 21:54:00 +03:00
|
|
|
switch render {
|
|
|
|
case "map":
|
|
|
|
title := fmt.Sprintf("Geolocation of %s results for action ID %.0f %s", show, a.ID, a.Name)
|
|
|
|
err = PrintMap(locs, title)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
default:
|
2015-04-21 19:13:35 +03:00
|
|
|
s := "agent has"
|
2015-07-17 17:47:50 +03:00
|
|
|
if agtCount > 1 {
|
2015-04-21 19:13:35 +03:00
|
|
|
s = "agents have"
|
|
|
|
}
|
2015-07-17 17:47:50 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[31m%d %s %s results\x1b[0m\n", agtCount, s, show)
|
2015-03-23 21:54:00 +03:00
|
|
|
}
|
2015-07-20 17:28:35 +03:00
|
|
|
if show != "all" {
|
|
|
|
var unsuccessful map[string][]string
|
|
|
|
unsuccessful = make(map[string][]string)
|
|
|
|
for _, status := range []string{mig.StatusCancelled, mig.StatusExpired, mig.StatusFailed, mig.StatusTimeout} {
|
|
|
|
offset = 0
|
|
|
|
for {
|
|
|
|
// print commands that have not returned successfully
|
|
|
|
target := fmt.Sprintf("search?type=command&limit=%d&offset=%d&actionid=%.0f&status=%s", limit, offset, a.ID, status)
|
|
|
|
resource, err := cli.GetAPIResource(target)
|
|
|
|
// because we query using pagination, the last query will return a 404 with no result.
|
|
|
|
// When that happens, GetAPIResource returns an error which we do not report to the user
|
|
|
|
if resource.Collection.Error.Message == "no results found" {
|
|
|
|
err = nil
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
for _, item := range resource.Collection.Items {
|
|
|
|
for _, data := range item.Data {
|
|
|
|
if data.Name != "command" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
cmd, err := ValueToCommand(data.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
unsuccessful[status] = append(unsuccessful[status], cmd.Agent.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
offset += limit
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for status, agents := range unsuccessful {
|
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[35m%s: %s\x1b[0m\n", status, strings.Join(agents, ", "))
|
|
|
|
}
|
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-11-24 20:43:22 +03:00
|
|
|
func PrintCommandResults(cmd mig.Command, onlyFound, showAgent bool) (err error) {
|
2014-11-24 20:01:26 +03:00
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
err = fmt.Errorf("PrintCommandResults() -> %v", e)
|
|
|
|
}
|
|
|
|
}()
|
2014-11-24 20:43:22 +03:00
|
|
|
var prefix string
|
|
|
|
if showAgent {
|
|
|
|
prefix = cmd.Agent.Name + " "
|
|
|
|
}
|
2015-04-07 01:07:33 +03:00
|
|
|
if cmd.Status != mig.StatusSuccess {
|
|
|
|
if !onlyFound {
|
|
|
|
fmt.Fprintf(os.Stderr, "%scommand did not succeed. status=%s\n", prefix, cmd.Status)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
for i, result := range cmd.Results {
|
2014-11-24 21:41:09 +03:00
|
|
|
if len(cmd.Action.Operations) <= i {
|
|
|
|
if !onlyFound {
|
2015-04-19 19:07:13 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "%s[error] operation %d did not return results\n", prefix, i)
|
2014-11-24 21:41:09 +03:00
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
// verify that we know the module
|
|
|
|
moduleName := cmd.Action.Operations[i].Module
|
2015-04-19 19:07:13 +03:00
|
|
|
if _, ok := modules.Available[moduleName]; !ok {
|
2014-11-24 21:41:09 +03:00
|
|
|
if !onlyFound {
|
|
|
|
fmt.Fprintf(os.Stderr, "%s[error] unknown module '%s'\n", prefix, moduleName)
|
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
continue
|
|
|
|
}
|
2015-05-22 05:08:44 +03:00
|
|
|
run := modules.Available[moduleName].NewRun()
|
2014-11-24 20:01:26 +03:00
|
|
|
// look for a result printer in the module
|
2015-05-22 05:08:44 +03:00
|
|
|
if _, ok := run.(modules.HasResultsPrinter); ok {
|
|
|
|
outRes, err := run.(modules.HasResultsPrinter).PrintResults(result, onlyFound)
|
2014-11-24 20:01:26 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2015-04-19 19:07:13 +03:00
|
|
|
for _, resLine := range outRes {
|
|
|
|
fmt.Printf("%s%s\n", prefix, resLine)
|
2014-11-24 20:01:26 +03:00
|
|
|
}
|
|
|
|
} else {
|
2014-11-24 21:41:09 +03:00
|
|
|
if !onlyFound {
|
|
|
|
fmt.Fprintf(os.Stderr, "%s[error] no printer available for module '%s'\n", prefix, moduleName)
|
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
}
|
|
|
|
}
|
2014-11-24 20:43:22 +03:00
|
|
|
if !onlyFound {
|
|
|
|
fmt.Printf("%scommand %s\n", prefix, cmd.Status)
|
|
|
|
}
|
2014-11-24 20:01:26 +03:00
|
|
|
return
|
|
|
|
}
|
2015-07-10 18:58:18 +03:00
|
|
|
|
|
|
|
// EnableDebug() prints debug messages to stdout
|
|
|
|
func (cli *Client) EnableDebug() {
|
|
|
|
cli.debug = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// DisableDebug() disables the printing of debug messages to stdout
|
|
|
|
func (cli *Client) DisableDebug() {
|
|
|
|
cli.debug = false
|
|
|
|
return
|
|
|
|
}
|