diff --git a/cmd/osbuild-koji/main.go b/cmd/osbuild-koji/main.go
new file mode 100644
index 000000000..1ed38e90b
--- /dev/null
+++ b/cmd/osbuild-koji/main.go
@@ -0,0 +1,110 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "path"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/osbuild/osbuild-composer/internal/upload/koji"
+)
+
+func main() {
+ var server, user, password, name, version, release, arch, filename string
+ flag.StringVar(&server, "server", "", "url to API")
+ flag.StringVar(&user, "user", "", "koji username")
+ flag.StringVar(&password, "password", "", "koji password")
+ flag.StringVar(&name, "name", "", "image name")
+ flag.StringVar(&version, "version", "", "image verison")
+ flag.StringVar(&release, "release", "", "image release")
+ flag.StringVar(&arch, "arch", "", "image architecture")
+ flag.StringVar(&filename, "filename", "", "filename")
+ flag.Parse()
+
+ id, err := uuid.NewRandom()
+ if err != nil {
+ println(err.Error())
+ return
+ }
+ dir := fmt.Sprintf("osbuild-%v", id)
+
+ file, err := os.Open(filename)
+ if err != nil {
+ println(err.Error())
+ return
+ }
+ defer file.Close()
+
+ k, err := koji.New(server)
+ if err != nil {
+ println(err.Error())
+ return
+ }
+
+ err = k.Login("osbuild", "osbuildpass")
+ if err != nil {
+ println(err.Error())
+ return
+ }
+ defer k.Logout()
+
+ hash, length, err := k.Upload(file, dir, path.Base(filename))
+ if err != nil {
+ println(err.Error())
+ return
+ }
+
+ build := koji.Build{
+ Name: name,
+ Version: version,
+ Release: release,
+ StartTime: time.Now().Unix(),
+ EndTime: time.Now().Unix(),
+ }
+ buildRoots := []koji.BuildRoot{
+ {
+ ID: 1,
+ Host: koji.Host{
+ Os: "RHEL8",
+ Arch: arch,
+ },
+ ContentGenerator: koji.ContentGenerator{
+ Name: "osbuild",
+ Version: "1",
+ },
+ Container: koji.Container{
+ Type: "nspawn",
+ Arch: arch,
+ },
+ Tools: []koji.Tool{},
+ Components: []koji.Component{},
+ },
+ }
+ output := []koji.Output{
+ {
+ BuildRootID: 1,
+ Filename: path.Base(filename),
+ FileSize: length,
+ Arch: arch,
+ ChecksumType: "md5",
+ MD5: hash,
+ Type: "image",
+ Components: []koji.Component{},
+ Extra: koji.OutputExtra{
+ Image: koji.OutputExtraImageInfo{
+ Arch: arch,
+ },
+ },
+ },
+ }
+
+ err = k.CGImport(build, buildRoots, output, dir)
+ if err != nil {
+ println(err.Error())
+ return
+ }
+
+ fmt.Printf("Success!\n")
+}
diff --git a/go.mod b/go.mod
index 869b844ad..123395727 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
github.com/google/go-cmp v0.3.1
github.com/google/uuid v1.1.1
github.com/julienschmidt/httprouter v1.2.0
+ github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.4.0
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
diff --git a/go.sum b/go.sum
index 820021540..49e5999aa 100644
--- a/go.sum
+++ b/go.sum
@@ -58,6 +58,8 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5i
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e h1:JZPIpxHmcXiQn101f6P9wkfRZs2A9268tHHnanj+esA=
+github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
diff --git a/internal/upload/koji/koji.go b/internal/upload/koji/koji.go
new file mode 100644
index 000000000..6bc25b62f
--- /dev/null
+++ b/internal/upload/koji/koji.go
@@ -0,0 +1,288 @@
+package koji
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/json"
+ "fmt"
+ "hash/adler32"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ "github.com/kolo/xmlrpc"
+)
+
+type Koji struct {
+ sessionID int64
+ sessionKey string
+ callnum int
+ xmlrpc *xmlrpc.Client
+ server string
+}
+
+type BuildExtra struct {
+ Image interface{} `json:"image"` // No extra info tracked at build level.
+}
+
+type Build struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Release string `json:"release"`
+ Source string `json:"source"`
+ StartTime int64 `json:"start_time"`
+ EndTime int64 `json:"end_time"`
+ Extra BuildExtra `json:"extra"`
+}
+
+type Host struct {
+ Os string `json:"os"`
+ Arch string `json:"arch"`
+}
+
+type ContentGenerator struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+}
+
+type Container struct {
+ Type string `json:"type"`
+ Arch string `json:"arch"`
+}
+
+type Tool struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+}
+
+type Component struct {
+ Type string `json:"type"` // must be 'rpm'
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Release string `json:"release"`
+ Epoch *uint64 `json:"epoch"`
+ Arch string `json:"arch"`
+ Sigmd5 string `json:"sigmd5"`
+ Signature *string `json:"signature"`
+}
+
+type BuildRoot struct {
+ ID uint64 `json:"id"`
+ Host Host `json:"host"`
+ ContentGenerator ContentGenerator `json:"content_generator"`
+ Container Container `json:"container"`
+ Tools []Tool `json:"tools"`
+ Components []Component `json:"components"`
+}
+
+type OutputExtraImageInfo struct {
+ // TODO: Ideally this is where the pipeline would be passed.
+ Arch string `json:"arch"` // TODO: why?
+}
+
+type OutputExtra struct {
+ Image OutputExtraImageInfo `json:"image"`
+}
+
+type Output struct {
+ BuildRootID uint64 `json:"buildroot_id"`
+ Filename string `json:"filename"`
+ FileSize uint64 `json:"filesize"`
+ Arch string `json:"arch"`
+ ChecksumType string `json:"checksum_type"` // must be 'md5'
+ MD5 string `json:"checksum"`
+ Type string `json:"type"`
+ Components []Component `json:"component"`
+ Extra OutputExtra `json:"extra"`
+}
+
+type Metadata struct {
+ MetadataVersion int `json:"metadata_version"` // must be '0'
+ Build Build `json:"build"`
+ BuildRoots []BuildRoot `json:"buildroots"`
+ Output []Output `json:"output"`
+}
+
+// RoundTrip implements the RoundTripper interface, using the default
+// transport. When a session has been established, also pass along the
+// session credentials. This may not be how the RoundTripper interface
+// is meant to be used, but the underlying XML-RPC helpers don't allow
+// us to adjust the URL per-call (these arguments should really be in
+// the body).
+func (k *Koji) RoundTrip(req *http.Request) (*http.Response, error) {
+ if k.sessionKey == "" {
+ return http.DefaultTransport.RoundTrip(req)
+ }
+
+ // Clone the request, so as not to alter the passed in value.
+ rClone := new(http.Request)
+ *rClone = *req
+ rClone.Header = make(http.Header, len(req.Header))
+ for idx, header := range req.Header {
+ rClone.Header[idx] = append([]string(nil), header...)
+ }
+
+ values := rClone.URL.Query()
+ values.Add("session-id", fmt.Sprintf("%v", k.sessionID))
+ values.Add("session-key", k.sessionKey)
+ values.Add("callnum", fmt.Sprintf("%v", k.callnum))
+ rClone.URL.RawQuery = values.Encode()
+
+ // Each call is given a unique callnum.
+ k.callnum++
+
+ return http.DefaultTransport.RoundTrip(rClone)
+}
+
+func New(server string) (*Koji, error) {
+ k := &Koji{}
+ client, err := xmlrpc.NewClient(server, k)
+ if err != nil {
+ return nil, err
+ }
+ k.xmlrpc = client
+ k.server = server
+ return k, nil
+}
+
+// GetAPIVersion gets the version of the API of the remote Koji instance
+func (k *Koji) GetAPIVersion() (int, error) {
+ var version int
+ err := k.xmlrpc.Call("getAPIVersion", nil, &version)
+ if err != nil {
+ return 0, err
+ }
+
+ return version, nil
+}
+
+// Login sets up a new session with the given user/password
+func (k *Koji) Login(user, password string) error {
+ args := []interface{}{user, password}
+ var reply struct {
+ SessionID int64 `xmlrpc:"session-id"`
+ SessionKey string `xmlrpc:"session-key"`
+ }
+ err := k.xmlrpc.Call("login", args, &reply)
+ if err != nil {
+ return err
+ }
+ k.sessionID = reply.SessionID
+ k.sessionKey = reply.SessionKey
+ k.callnum = 0
+ return nil
+}
+
+// Logout ends the session
+func (k *Koji) Logout() error {
+ err := k.xmlrpc.Call("logout", nil, nil)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// CGImport imports previously uploaded content, by specifying its metadata, and the temporary
+// directory where it is located.
+func (k *Koji) CGImport(build Build, buildRoots []BuildRoot, output []Output, directory string) error {
+ m := &Metadata{
+ Build: build,
+ BuildRoots: buildRoots,
+ Output: output,
+ }
+ metadata, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+
+ var result interface{}
+ err = k.xmlrpc.Call("CGImport", []interface{}{string(metadata), directory}, &result)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// uploadChunk uploads a byte slice to a given filepath/filname at a given offset
+func (k *Koji) uploadChunk(chunk []byte, filepath, filename string, offset uint64) error {
+ // We have to open-code a bastardized version of XML-RPC: We send an octet-stream, as
+ // if it was an RPC call, and get a regular XML-RPC reply back. In addition to the
+ // standard URL parameters, we also have to pass any other parameters as part of the
+ // URL, as the body can only contain the payload.
+ u, err := url.Parse(k.server)
+ if err != nil {
+ return err
+ }
+ q := u.Query()
+ q.Add("filepath", filepath)
+ q.Add("filename", filename)
+ q.Add("offset", fmt.Sprintf("%v", offset))
+ q.Add("fileverify", "adler32")
+ q.Add("session-id", fmt.Sprintf("%v", k.sessionID))
+ q.Add("session-key", k.sessionKey)
+ q.Add("callnum", fmt.Sprintf("%v", k.callnum))
+ u.RawQuery = q.Encode()
+
+ // Each call is given a unique callnum.
+ k.callnum++
+
+ resp, err := http.Post(u.String(), "application/octet-stream", bytes.NewBuffer(chunk))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ var reply struct {
+ Size int `xmlrpc:"size"`
+ Adler32 string `xmlrpc:"hexdigest"`
+ }
+ xmlrpc.Response.Unmarshal(body, &reply)
+
+ if reply.Size != len(chunk) {
+ return fmt.Errorf("Sent a chunk of %d bytes, but server got %d bytes", len(chunk), reply.Size)
+ }
+
+ digest := fmt.Sprintf("%08x", adler32.Checksum(chunk))
+ if reply.Adler32 != digest {
+ return fmt.Errorf("Sent a chunk with Adler32 digest %s, but server computed digest %s", digest, reply.Adler32)
+ }
+
+ return nil
+}
+
+// Upload uploads file to the temporary filepath on the kojiserver under the name filename
+// The md5sum and size of the file is returned on success.
+func (k *Koji) Upload(file io.Reader, filepath, filename string) (string, uint64, error) {
+ chunk := make([]byte, 1024*1024) // upload a megabyte at a time
+ offset := uint64(0)
+ hash := md5.New()
+ for {
+ n, err := file.Read(chunk)
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return "", 0, err
+ }
+ err = k.uploadChunk(chunk[:n], filepath, filename, offset)
+ if err != nil {
+ return "", 0, err
+ }
+ offset += uint64(n)
+
+ m, err := hash.Write(chunk[:n])
+ if err != nil {
+ return "", 0, err
+ }
+ if m != n {
+ return "", 0, fmt.Errorf("sent %d bytes, but hashed %d", n, m)
+ }
+ }
+ return fmt.Sprintf("%x", hash.Sum(nil)), offset, nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/LICENSE b/vendor/github.com/kolo/xmlrpc/LICENSE
new file mode 100644
index 000000000..8103dd139
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2012 Dmitry Maksimov
+
+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.
diff --git a/vendor/github.com/kolo/xmlrpc/README.md b/vendor/github.com/kolo/xmlrpc/README.md
new file mode 100644
index 000000000..8113cfcc3
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/README.md
@@ -0,0 +1,89 @@
+[](https://godoc.org/github.com/kolo/xmlrpc)
+
+## Overview
+
+xmlrpc is an implementation of client side part of XMLRPC protocol in Go language.
+
+## Status
+
+This project is in minimal maintenance mode with no further development. Bug fixes
+are accepted, but it might take some time until they will be merged.
+
+## Installation
+
+To install xmlrpc package run `go get github.com/kolo/xmlrpc`. To use
+it in application add `"github.com/kolo/xmlrpc"` string to `import`
+statement.
+
+## Usage
+
+ client, _ := xmlrpc.NewClient("https://bugzilla.mozilla.org/xmlrpc.cgi", nil)
+ result := struct{
+ Version string `xmlrpc:"version"`
+ }{}
+ client.Call("Bugzilla.version", nil, &result)
+ fmt.Printf("Version: %s\n", result.Version) // Version: 4.2.7+
+
+Second argument of NewClient function is an object that implements
+[http.RoundTripper](http://golang.org/pkg/net/http/#RoundTripper)
+interface, it can be used to get more control over connection options.
+By default it initialized by http.DefaultTransport object.
+
+### Arguments encoding
+
+xmlrpc package supports encoding of native Go data types to method
+arguments.
+
+Data types encoding rules:
+
+* int, int8, int16, int32, int64 encoded to int;
+* float32, float64 encoded to double;
+* bool encoded to boolean;
+* string encoded to string;
+* time.Time encoded to datetime.iso8601;
+* xmlrpc.Base64 encoded to base64;
+* slice encoded to array;
+
+Structs decoded to struct by following rules:
+
+* all public field become struct members;
+* field name become member name;
+* if field has xmlrpc tag, its value become member name.
+
+Server method can accept few arguments, to handle this case there is
+special approach to handle slice of empty interfaces (`[]interface{}`).
+Each value of such slice encoded as separate argument.
+
+### Result decoding
+
+Result of remote function is decoded to native Go data type.
+
+Data types decoding rules:
+
+* int, i4 decoded to int, int8, int16, int32, int64;
+* double decoded to float32, float64;
+* boolean decoded to bool;
+* string decoded to string;
+* array decoded to slice;
+* structs decoded following the rules described in previous section;
+* datetime.iso8601 decoded as time.Time data type;
+* base64 decoded to string.
+
+## Implementation details
+
+xmlrpc package contains clientCodec type, that implements [rpc.ClientCodec](http://golang.org/pkg/net/rpc/#ClientCodec)
+interface of [net/rpc](http://golang.org/pkg/net/rpc) package.
+
+xmlrpc package works over HTTP protocol, but some internal functions
+and data type were made public to make it easier to create another
+implementation of xmlrpc that works over another protocol. To encode
+request body there is EncodeMethodCall function. To decode server
+response Response data type can be used.
+
+## Contribution
+
+See [project status](#status).
+
+## Authors
+
+Dmitry Maksimov (dmtmax@gmail.com)
diff --git a/vendor/github.com/kolo/xmlrpc/client.go b/vendor/github.com/kolo/xmlrpc/client.go
new file mode 100644
index 000000000..643dc1c10
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/client.go
@@ -0,0 +1,161 @@
+package xmlrpc
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/cookiejar"
+ "net/rpc"
+ "net/url"
+ "sync"
+)
+
+type Client struct {
+ *rpc.Client
+}
+
+// clientCodec is rpc.ClientCodec interface implementation.
+type clientCodec struct {
+ // url presents url of xmlrpc service
+ url *url.URL
+
+ // httpClient works with HTTP protocol
+ httpClient *http.Client
+
+ // cookies stores cookies received on last request
+ cookies http.CookieJar
+
+ // responses presents map of active requests. It is required to return request id, that
+ // rpc.Client can mark them as done.
+ responses map[uint64]*http.Response
+ mutex sync.Mutex
+
+ response Response
+
+ // ready presents channel, that is used to link request and it`s response.
+ ready chan uint64
+
+ // close notifies codec is closed.
+ close chan uint64
+}
+
+func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) {
+ httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args)
+
+ if err != nil {
+ return err
+ }
+
+ if codec.cookies != nil {
+ for _, cookie := range codec.cookies.Cookies(codec.url) {
+ httpRequest.AddCookie(cookie)
+ }
+ }
+
+ var httpResponse *http.Response
+ httpResponse, err = codec.httpClient.Do(httpRequest)
+
+ if err != nil {
+ return err
+ }
+
+ if codec.cookies != nil {
+ codec.cookies.SetCookies(codec.url, httpResponse.Cookies())
+ }
+
+ codec.mutex.Lock()
+ codec.responses[request.Seq] = httpResponse
+ codec.mutex.Unlock()
+
+ codec.ready <- request.Seq
+
+ return nil
+}
+
+func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) {
+ var seq uint64
+ select {
+ case seq = <-codec.ready:
+ case <-codec.close:
+ return errors.New("codec is closed")
+ }
+ response.Seq = seq
+
+ codec.mutex.Lock()
+ httpResponse := codec.responses[seq]
+ delete(codec.responses, seq)
+ codec.mutex.Unlock()
+
+ defer httpResponse.Body.Close()
+
+ if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
+ response.Error = fmt.Sprintf("request error: bad status code - %d", httpResponse.StatusCode)
+ return nil
+ }
+
+ body, err := ioutil.ReadAll(httpResponse.Body)
+ if err != nil {
+ response.Error = err.Error()
+ return nil
+ }
+
+ resp := Response(body)
+ if err := resp.Err(); err != nil {
+ response.Error = err.Error()
+ return nil
+ }
+
+ codec.response = resp
+
+ return nil
+}
+
+func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
+ if v == nil {
+ return nil
+ }
+ return codec.response.Unmarshal(v)
+}
+
+func (codec *clientCodec) Close() error {
+ if transport, ok := codec.httpClient.Transport.(*http.Transport); ok {
+ transport.CloseIdleConnections()
+ }
+
+ close(codec.close)
+
+ return nil
+}
+
+// NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service.
+func NewClient(requrl string, transport http.RoundTripper) (*Client, error) {
+ if transport == nil {
+ transport = http.DefaultTransport
+ }
+
+ httpClient := &http.Client{Transport: transport}
+
+ jar, err := cookiejar.New(nil)
+
+ if err != nil {
+ return nil, err
+ }
+
+ u, err := url.Parse(requrl)
+
+ if err != nil {
+ return nil, err
+ }
+
+ codec := clientCodec{
+ url: u,
+ httpClient: httpClient,
+ close: make(chan uint64),
+ ready: make(chan uint64),
+ responses: make(map[uint64]*http.Response),
+ cookies: jar,
+ }
+
+ return &Client{rpc.NewClientWithCodec(&codec)}, nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/decoder.go b/vendor/github.com/kolo/xmlrpc/decoder.go
new file mode 100644
index 000000000..d4dcb19ad
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/decoder.go
@@ -0,0 +1,473 @@
+package xmlrpc
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ iso8601 = "20060102T15:04:05"
+ iso8601Z = "20060102T15:04:05Z07:00"
+ iso8601Hyphen = "2006-01-02T15:04:05"
+ iso8601HyphenZ = "2006-01-02T15:04:05Z07:00"
+)
+
+var (
+ // CharsetReader is a function to generate reader which converts a non UTF-8
+ // charset into UTF-8.
+ CharsetReader func(string, io.Reader) (io.Reader, error)
+
+ timeLayouts = []string{iso8601, iso8601Z, iso8601Hyphen, iso8601HyphenZ}
+ invalidXmlError = errors.New("invalid xml")
+)
+
+type TypeMismatchError string
+
+func (e TypeMismatchError) Error() string { return string(e) }
+
+type decoder struct {
+ *xml.Decoder
+}
+
+func unmarshal(data []byte, v interface{}) (err error) {
+ dec := &decoder{xml.NewDecoder(bytes.NewBuffer(data))}
+
+ if CharsetReader != nil {
+ dec.CharsetReader = CharsetReader
+ }
+
+ var tok xml.Token
+ for {
+ if tok, err = dec.Token(); err != nil {
+ return err
+ }
+
+ if t, ok := tok.(xml.StartElement); ok {
+ if t.Name.Local == "value" {
+ val := reflect.ValueOf(v)
+ if val.Kind() != reflect.Ptr {
+ return errors.New("non-pointer value passed to unmarshal")
+ }
+ if err = dec.decodeValue(val.Elem()); err != nil {
+ return err
+ }
+
+ break
+ }
+ }
+ }
+
+ // read until end of document
+ err = dec.Skip()
+ if err != nil && err != io.EOF {
+ return err
+ }
+
+ return nil
+}
+
+func (dec *decoder) decodeValue(val reflect.Value) error {
+ var tok xml.Token
+ var err error
+
+ if val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ val.Set(reflect.New(val.Type().Elem()))
+ }
+ val = val.Elem()
+ }
+
+ var typeName string
+ for {
+ if tok, err = dec.Token(); err != nil {
+ return err
+ }
+
+ if t, ok := tok.(xml.EndElement); ok {
+ if t.Name.Local == "value" {
+ return nil
+ } else {
+ return invalidXmlError
+ }
+ }
+
+ if t, ok := tok.(xml.StartElement); ok {
+ typeName = t.Name.Local
+ break
+ }
+
+ // Treat value data without type identifier as string
+ if t, ok := tok.(xml.CharData); ok {
+ if value := strings.TrimSpace(string(t)); value != "" {
+ if err = checkType(val, reflect.String); err != nil {
+ return err
+ }
+
+ val.SetString(value)
+ return nil
+ }
+ }
+ }
+
+ switch typeName {
+ case "struct":
+ ismap := false
+ pmap := val
+ valType := val.Type()
+
+ if err = checkType(val, reflect.Struct); err != nil {
+ if checkType(val, reflect.Map) == nil {
+ if valType.Key().Kind() != reflect.String {
+ return fmt.Errorf("only maps with string key type can be unmarshalled")
+ }
+ ismap = true
+ } else if checkType(val, reflect.Interface) == nil && val.IsNil() {
+ var dummy map[string]interface{}
+ valType = reflect.TypeOf(dummy)
+ pmap = reflect.New(valType).Elem()
+ val.Set(pmap)
+ ismap = true
+ } else {
+ return err
+ }
+ }
+
+ var fields map[string]reflect.Value
+
+ if !ismap {
+ fields = make(map[string]reflect.Value)
+
+ for i := 0; i < valType.NumField(); i++ {
+ field := valType.Field(i)
+ fieldVal := val.FieldByName(field.Name)
+
+ if fieldVal.CanSet() {
+ if fn := field.Tag.Get("xmlrpc"); fn != "" {
+ fields[fn] = fieldVal
+ } else {
+ fields[field.Name] = fieldVal
+ }
+ }
+ }
+ } else {
+ // Create initial empty map
+ pmap.Set(reflect.MakeMap(valType))
+ }
+
+ // Process struct members.
+ StructLoop:
+ for {
+ if tok, err = dec.Token(); err != nil {
+ return err
+ }
+ switch t := tok.(type) {
+ case xml.StartElement:
+ if t.Name.Local != "member" {
+ return invalidXmlError
+ }
+
+ tagName, fieldName, err := dec.readTag()
+ if err != nil {
+ return err
+ }
+ if tagName != "name" {
+ return invalidXmlError
+ }
+
+ var fv reflect.Value
+ ok := true
+
+ if !ismap {
+ fv, ok = fields[string(fieldName)]
+ } else {
+ fv = reflect.New(valType.Elem())
+ }
+
+ if ok {
+ for {
+ if tok, err = dec.Token(); err != nil {
+ return err
+ }
+ if t, ok := tok.(xml.StartElement); ok && t.Name.Local == "value" {
+ if err = dec.decodeValue(fv); err != nil {
+ return err
+ }
+
+ //
+ if err = dec.Skip(); err != nil {
+ return err
+ }
+
+ break
+ }
+ }
+ }
+
+ //
+ if err = dec.Skip(); err != nil {
+ return err
+ }
+
+ if ismap {
+ pmap.SetMapIndex(reflect.ValueOf(string(fieldName)), reflect.Indirect(fv))
+ val.Set(pmap)
+ }
+ case xml.EndElement:
+ break StructLoop
+ }
+ }
+ case "array":
+ slice := val
+ if checkType(val, reflect.Interface) == nil && val.IsNil() {
+ slice = reflect.ValueOf([]interface{}{})
+ } else if err = checkType(val, reflect.Slice); err != nil {
+ return err
+ }
+
+ ArrayLoop:
+ for {
+ if tok, err = dec.Token(); err != nil {
+ return err
+ }
+
+ switch t := tok.(type) {
+ case xml.StartElement:
+ var index int
+ if t.Name.Local != "data" {
+ return invalidXmlError
+ }
+ DataLoop:
+ for {
+ if tok, err = dec.Token(); err != nil {
+ return err
+ }
+
+ switch tt := tok.(type) {
+ case xml.StartElement:
+ if tt.Name.Local != "value" {
+ return invalidXmlError
+ }
+
+ if index < slice.Len() {
+ v := slice.Index(index)
+ if v.Kind() == reflect.Interface {
+ v = v.Elem()
+ }
+ if v.Kind() != reflect.Ptr {
+ return errors.New("error: cannot write to non-pointer array element")
+ }
+ if err = dec.decodeValue(v); err != nil {
+ return err
+ }
+ } else {
+ v := reflect.New(slice.Type().Elem())
+ if err = dec.decodeValue(v); err != nil {
+ return err
+ }
+ slice = reflect.Append(slice, v.Elem())
+ }
+
+ //
+ if err = dec.Skip(); err != nil {
+ return err
+ }
+ index++
+ case xml.EndElement:
+ val.Set(slice)
+ break DataLoop
+ }
+ }
+ case xml.EndElement:
+ break ArrayLoop
+ }
+ }
+ default:
+ if tok, err = dec.Token(); err != nil {
+ return err
+ }
+
+ var data []byte
+
+ switch t := tok.(type) {
+ case xml.EndElement:
+ return nil
+ case xml.CharData:
+ data = []byte(t.Copy())
+ default:
+ return invalidXmlError
+ }
+
+ switch typeName {
+ case "int", "i4", "i8":
+ if checkType(val, reflect.Interface) == nil && val.IsNil() {
+ i, err := strconv.ParseInt(string(data), 10, 64)
+ if err != nil {
+ return err
+ }
+
+ pi := reflect.New(reflect.TypeOf(i)).Elem()
+ pi.SetInt(i)
+ val.Set(pi)
+ } else if err = checkType(val, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64); err != nil {
+ return err
+ } else {
+ i, err := strconv.ParseInt(string(data), 10, val.Type().Bits())
+ if err != nil {
+ return err
+ }
+
+ val.SetInt(i)
+ }
+ case "string", "base64":
+ str := string(data)
+ if checkType(val, reflect.Interface) == nil && val.IsNil() {
+ pstr := reflect.New(reflect.TypeOf(str)).Elem()
+ pstr.SetString(str)
+ val.Set(pstr)
+ } else if err = checkType(val, reflect.String); err != nil {
+ return err
+ } else {
+ val.SetString(str)
+ }
+ case "dateTime.iso8601":
+ var t time.Time
+ var err error
+
+ for _, layout := range timeLayouts {
+ t, err = time.Parse(layout, string(data))
+ if err == nil {
+ break
+ }
+ }
+ if err != nil {
+ return err
+ }
+
+ if checkType(val, reflect.Interface) == nil && val.IsNil() {
+ ptime := reflect.New(reflect.TypeOf(t)).Elem()
+ ptime.Set(reflect.ValueOf(t))
+ val.Set(ptime)
+ } else if _, ok := val.Interface().(time.Time); !ok {
+ return TypeMismatchError(fmt.Sprintf("error: type mismatch error - can't decode %v to time", val.Kind()))
+ } else {
+ val.Set(reflect.ValueOf(t))
+ }
+ case "boolean":
+ v, err := strconv.ParseBool(string(data))
+ if err != nil {
+ return err
+ }
+
+ if checkType(val, reflect.Interface) == nil && val.IsNil() {
+ pv := reflect.New(reflect.TypeOf(v)).Elem()
+ pv.SetBool(v)
+ val.Set(pv)
+ } else if err = checkType(val, reflect.Bool); err != nil {
+ return err
+ } else {
+ val.SetBool(v)
+ }
+ case "double":
+ if checkType(val, reflect.Interface) == nil && val.IsNil() {
+ i, err := strconv.ParseFloat(string(data), 64)
+ if err != nil {
+ return err
+ }
+
+ pdouble := reflect.New(reflect.TypeOf(i)).Elem()
+ pdouble.SetFloat(i)
+ val.Set(pdouble)
+ } else if err = checkType(val, reflect.Float32, reflect.Float64); err != nil {
+ return err
+ } else {
+ i, err := strconv.ParseFloat(string(data), val.Type().Bits())
+ if err != nil {
+ return err
+ }
+
+ val.SetFloat(i)
+ }
+ default:
+ return errors.New("unsupported type")
+ }
+
+ //
+ if err = dec.Skip(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (dec *decoder) readTag() (string, []byte, error) {
+ var tok xml.Token
+ var err error
+
+ var name string
+ for {
+ if tok, err = dec.Token(); err != nil {
+ return "", nil, err
+ }
+
+ if t, ok := tok.(xml.StartElement); ok {
+ name = t.Name.Local
+ break
+ }
+ }
+
+ value, err := dec.readCharData()
+ if err != nil {
+ return "", nil, err
+ }
+
+ return name, value, dec.Skip()
+}
+
+func (dec *decoder) readCharData() ([]byte, error) {
+ var tok xml.Token
+ var err error
+
+ if tok, err = dec.Token(); err != nil {
+ return nil, err
+ }
+
+ if t, ok := tok.(xml.CharData); ok {
+ return []byte(t.Copy()), nil
+ } else {
+ return nil, invalidXmlError
+ }
+}
+
+func checkType(val reflect.Value, kinds ...reflect.Kind) error {
+ if len(kinds) == 0 {
+ return nil
+ }
+
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+
+ match := false
+
+ for _, kind := range kinds {
+ if val.Kind() == kind {
+ match = true
+ break
+ }
+ }
+
+ if !match {
+ return TypeMismatchError(fmt.Sprintf("error: type mismatch - can't unmarshal %v to %v",
+ val.Kind(), kinds[0]))
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/encoder.go b/vendor/github.com/kolo/xmlrpc/encoder.go
new file mode 100644
index 000000000..22b9683fb
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/encoder.go
@@ -0,0 +1,174 @@
+package xmlrpc
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "reflect"
+ "sort"
+ "strconv"
+ "time"
+)
+
+// Base64 represents value in base64 encoding
+type Base64 string
+
+type encodeFunc func(reflect.Value) ([]byte, error)
+
+func marshal(v interface{}) ([]byte, error) {
+ if v == nil {
+ return []byte{}, nil
+ }
+
+ val := reflect.ValueOf(v)
+ return encodeValue(val)
+}
+
+func encodeValue(val reflect.Value) ([]byte, error) {
+ var b []byte
+ var err error
+
+ if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
+ if val.IsNil() {
+ return []byte(""), nil
+ }
+
+ val = val.Elem()
+ }
+
+ switch val.Kind() {
+ case reflect.Struct:
+ switch val.Interface().(type) {
+ case time.Time:
+ t := val.Interface().(time.Time)
+ b = []byte(fmt.Sprintf("%s", t.Format(iso8601)))
+ default:
+ b, err = encodeStruct(val)
+ }
+ case reflect.Map:
+ b, err = encodeMap(val)
+ case reflect.Slice:
+ b, err = encodeSlice(val)
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ b = []byte(fmt.Sprintf("%s", strconv.FormatInt(val.Int(), 10)))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ b = []byte(fmt.Sprintf("%s", strconv.FormatUint(val.Uint(), 10)))
+ case reflect.Float32, reflect.Float64:
+ b = []byte(fmt.Sprintf("%s",
+ strconv.FormatFloat(val.Float(), 'f', -1, val.Type().Bits())))
+ case reflect.Bool:
+ if val.Bool() {
+ b = []byte("1")
+ } else {
+ b = []byte("0")
+ }
+ case reflect.String:
+ var buf bytes.Buffer
+
+ xml.Escape(&buf, []byte(val.String()))
+
+ if _, ok := val.Interface().(Base64); ok {
+ b = []byte(fmt.Sprintf("%s", buf.String()))
+ } else {
+ b = []byte(fmt.Sprintf("%s", buf.String()))
+ }
+ default:
+ return nil, fmt.Errorf("xmlrpc encode error: unsupported type")
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return []byte(fmt.Sprintf("%s", string(b))), nil
+}
+
+func encodeStruct(val reflect.Value) ([]byte, error) {
+ var b bytes.Buffer
+
+ b.WriteString("")
+
+ t := val.Type()
+ for i := 0; i < t.NumField(); i++ {
+ b.WriteString("")
+ f := t.Field(i)
+
+ name := f.Tag.Get("xmlrpc")
+ if name == "" {
+ name = f.Name
+ }
+ b.WriteString(fmt.Sprintf("%s", name))
+
+ p, err := encodeValue(val.FieldByName(f.Name))
+ if err != nil {
+ return nil, err
+ }
+ b.Write(p)
+
+ b.WriteString("")
+ }
+
+ b.WriteString("")
+
+ return b.Bytes(), nil
+}
+
+var sortMapKeys bool
+
+func encodeMap(val reflect.Value) ([]byte, error) {
+ var t = val.Type()
+
+ if t.Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("xmlrpc encode error: only maps with string keys are supported")
+ }
+
+ var b bytes.Buffer
+
+ b.WriteString("")
+
+ keys := val.MapKeys()
+
+ if sortMapKeys {
+ sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
+ }
+
+ for i := 0; i < val.Len(); i++ {
+ key := keys[i]
+ kval := val.MapIndex(key)
+
+ b.WriteString("")
+ b.WriteString(fmt.Sprintf("%s", key.String()))
+
+ p, err := encodeValue(kval)
+
+ if err != nil {
+ return nil, err
+ }
+
+ b.Write(p)
+ b.WriteString("")
+ }
+
+ b.WriteString("")
+
+ return b.Bytes(), nil
+}
+
+func encodeSlice(val reflect.Value) ([]byte, error) {
+ var b bytes.Buffer
+
+ b.WriteString("")
+
+ for i := 0; i < val.Len(); i++ {
+ p, err := encodeValue(val.Index(i))
+ if err != nil {
+ return nil, err
+ }
+
+ b.Write(p)
+ }
+
+ b.WriteString("")
+
+ return b.Bytes(), nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/request.go b/vendor/github.com/kolo/xmlrpc/request.go
new file mode 100644
index 000000000..acb8251b2
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/request.go
@@ -0,0 +1,57 @@
+package xmlrpc
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+)
+
+func NewRequest(url string, method string, args interface{}) (*http.Request, error) {
+ var t []interface{}
+ var ok bool
+ if t, ok = args.([]interface{}); !ok {
+ if args != nil {
+ t = []interface{}{args}
+ }
+ }
+
+ body, err := EncodeMethodCall(method, t...)
+ if err != nil {
+ return nil, err
+ }
+
+ request, err := http.NewRequest("POST", url, bytes.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+
+ request.Header.Set("Content-Type", "text/xml")
+ request.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
+
+ return request, nil
+}
+
+func EncodeMethodCall(method string, args ...interface{}) ([]byte, error) {
+ var b bytes.Buffer
+ b.WriteString(``)
+ b.WriteString(fmt.Sprintf("%s", method))
+
+ if args != nil {
+ b.WriteString("")
+
+ for _, arg := range args {
+ p, err := marshal(arg)
+ if err != nil {
+ return nil, err
+ }
+
+ b.WriteString(fmt.Sprintf("%s", string(p)))
+ }
+
+ b.WriteString("")
+ }
+
+ b.WriteString("")
+
+ return b.Bytes(), nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/response.go b/vendor/github.com/kolo/xmlrpc/response.go
new file mode 100644
index 000000000..18e6d366c
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/response.go
@@ -0,0 +1,42 @@
+package xmlrpc
+
+import (
+ "fmt"
+ "regexp"
+)
+
+var (
+ faultRx = regexp.MustCompile(`(\s|\S)+`)
+)
+
+// FaultError is returned from the server when an invalid call is made
+type FaultError struct {
+ Code int `xmlrpc:"faultCode"`
+ String string `xmlrpc:"faultString"`
+}
+
+// Error implements the error interface
+func (e FaultError) Error() string {
+ return fmt.Sprintf("Fault(%d): %s", e.Code, e.String)
+}
+
+type Response []byte
+
+func (r Response) Err() error {
+ if !faultRx.Match(r) {
+ return nil
+ }
+ var fault FaultError
+ if err := unmarshal(r, &fault); err != nil {
+ return err
+ }
+ return fault
+}
+
+func (r Response) Unmarshal(v interface{}) error {
+ if err := unmarshal(r, v); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/test_server.rb b/vendor/github.com/kolo/xmlrpc/test_server.rb
new file mode 100644
index 000000000..1ccfc9ac4
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/test_server.rb
@@ -0,0 +1,25 @@
+# encoding: utf-8
+
+require "xmlrpc/server"
+
+class Service
+ def time
+ Time.now
+ end
+
+ def upcase(s)
+ s.upcase
+ end
+
+ def sum(x, y)
+ x + y
+ end
+
+ def error
+ raise XMLRPC::FaultException.new(500, "Server error")
+ end
+end
+
+server = XMLRPC::Server.new 5001, 'localhost'
+server.add_handler "service", Service.new
+server.serve
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a0871e760..c456ed0df 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -100,6 +100,8 @@ github.com/google/uuid
github.com/jmespath/go-jmespath
# github.com/julienschmidt/httprouter v1.2.0
github.com/julienschmidt/httprouter
+# github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e
+github.com/kolo/xmlrpc
# github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149
github.com/mattn/go-ieproxy
# github.com/mitchellh/go-homedir v1.1.0