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 @@ +[![GoDoc](https://godoc.org/github.com/kolo/xmlrpc?status.svg)](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