// Package xmlrpc parses and writes XML-RPC calls and responses, keeping // value types and struct member order exactly. package xmlrpc import ( "bytes" "encoding/base64" "encoding/xml" "errors" "fmt" "io" "strconv" "strings" ) // MaxSize is the largest document accepted. const MaxSize = 64 << 20 // MaxDepth is the deepest nesting of arrays and structs accepted. const MaxDepth = 64 // Value is one of: int64, bool, string, float64, []byte (base64), DateTime, // []Value (array), Struct, or nil (). type Value = any // DateTime is dateTime.iso8601 text, kept as is. type DateTime string // Member is a struct member. type Member struct { Name string Value Value } // Struct is an XML-RPC struct, in document order. type Struct []Member // Get returns the value of the first member with the name. func (s Struct) Get(name string) (Value, bool) { for _, m := range s { if m.Name == name { return m.Value, true } } return nil, false } // Call is a methodCall. type Call struct { Method string Params []Value } // Response is a methodResponse: Params, or Fault if it's set. type Response struct { Params []Value Fault *Fault } // Fault is a fault response's code and message. type Fault struct { Code int64 String string } func (f *Fault) Error() string { return fmt.Sprintf("xmlrpc fault %d: %s", f.Code, f.String) } type parser struct{ d *xml.Decoder } func newParser(data []byte) (*parser, error) { if len(data) > MaxSize { return nil, errors.New("xmlrpc: document too large") } d := xml.NewDecoder(bytes.NewReader(data)) d.Strict = true d.CharsetReader = func(label string, r io.Reader) (io.Reader, error) { switch strings.ToLower(label) { case "utf-8", "utf8", "us-ascii", "ascii": return r, nil } return nil, fmt.Errorf("xmlrpc: unsupported encoding %q", label) } return &parser{d}, nil } func (p *parser) token() (xml.Token, error) { t, err := p.d.Token() if err == io.EOF { err = io.ErrUnexpectedEOF } return t, err } // next returns the next start or end element, skipping whitespace, // comments and processing instructions. func (p *parser) next() (xml.Token, error) { for { t, err := p.token() if err != nil { return nil, err } switch t := t.(type) { case xml.StartElement, xml.EndElement: return t, nil case xml.CharData: if len(bytes.TrimSpace(t)) != 0 { return nil, fmt.Errorf("xmlrpc: unexpected text %q", string(t)) } } } } func (p *parser) start(name string) error { t, err := p.next() if err != nil { return err } if s, ok := t.(xml.StartElement); !ok || s.Name.Local != name { return fmt.Errorf("xmlrpc: expected <%s>", name) } return nil } func (p *parser) end(name string) error { t, err := p.next() if err != nil { return err } if e, ok := t.(xml.EndElement); !ok || e.Name.Local != name { return fmt.Errorf("xmlrpc: expected ", name) } return nil } // text reads character data up to the current element's end. func (p *parser) text() (string, error) { var b strings.Builder for { t, err := p.token() if err != nil { return "", err } switch t := t.(type) { case xml.CharData: b.Write(t) case xml.EndElement: return b.String(), nil case xml.StartElement: return "", fmt.Errorf("xmlrpc: unexpected <%s>", t.Name.Local) } } } // eof checks that only whitespace follows the root element. func (p *parser) eof() error { for { t, err := p.d.Token() if err == io.EOF { return nil } if err != nil { return err } switch t := t.(type) { case xml.StartElement, xml.EndElement: return errors.New("xmlrpc: content after document") case xml.CharData: if len(bytes.TrimSpace(t)) != 0 { return errors.New("xmlrpc: content after document") } } } } // value parses a value's content; its start was read. // Bare text is a string, whitespace included. func (p *parser) value(depth int) (Value, error) { var text strings.Builder for { t, err := p.token() if err != nil { return nil, err } switch t := t.(type) { case xml.CharData: text.Write(t) case xml.EndElement: return text.String(), nil case xml.StartElement: if strings.TrimSpace(text.String()) != "" { return nil, errors.New("xmlrpc: text mixed with elements in ") } v, err := p.typed(t.Name.Local, depth) if err != nil { return nil, err } return v, p.end("value") } } } func (p *parser) typed(name string, depth int) (Value, error) { switch name { case "array", "struct": if depth >= MaxDepth { return nil, errors.New("xmlrpc: nested too deeply") } if name == "array" { return p.array(depth + 1) } return p.strct(depth + 1) } s, err := p.text() if err != nil { return nil, err } switch name { case "nil": return nil, nil case "string": return s, nil case "int", "i4", "i8": n, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64) if err != nil { return nil, fmt.Errorf("xmlrpc: bad <%s> %q", name, s) } return n, nil case "boolean": switch strings.TrimSpace(s) { case "0": return false, nil case "1": return true, nil } return nil, fmt.Errorf("xmlrpc: bad %q", s) case "double": f, err := strconv.ParseFloat(strings.TrimSpace(s), 64) if err != nil { return nil, fmt.Errorf("xmlrpc: bad %q", s) } return f, nil case "dateTime.iso8601": return DateTime(s), nil case "base64": b, err := base64.StdEncoding.DecodeString(strings.Join(strings.Fields(s), "")) if err != nil { return nil, fmt.Errorf("xmlrpc: bad : %v", err) } return b, nil } return nil, fmt.Errorf("xmlrpc: unknown type <%s>", name) } func (p *parser) array(depth int) (Value, error) { if err := p.start("data"); err != nil { return nil, err } a := []Value{} for { t, err := p.next() if err != nil { return nil, err } switch t := t.(type) { case xml.EndElement: return a, p.end("array") case xml.StartElement: if t.Name.Local != "value" { return nil, fmt.Errorf("xmlrpc: unexpected <%s> in ", t.Name.Local) } v, err := p.value(depth) if err != nil { return nil, err } a = append(a, v) } } } func (p *parser) strct(depth int) (Value, error) { s := Struct{} for { t, err := p.next() if err != nil { return nil, err } switch t := t.(type) { case xml.EndElement: return s, nil case xml.StartElement: if t.Name.Local != "member" { return nil, fmt.Errorf("xmlrpc: unexpected <%s> in ", t.Name.Local) } if err := p.start("name"); err != nil { return nil, err } name, err := p.text() if err != nil { return nil, err } if err := p.start("value"); err != nil { return nil, err } v, err := p.value(depth) if err != nil { return nil, err } if err := p.end("member"); err != nil { return nil, err } s = append(s, Member{name, v}) } } } // params parses elements up to . func (p *parser) params() ([]Value, error) { ps := []Value{} for { t, err := p.next() if err != nil { return nil, err } switch t := t.(type) { case xml.EndElement: return ps, nil case xml.StartElement: if t.Name.Local != "param" { return nil, fmt.Errorf("xmlrpc: unexpected <%s> in ", t.Name.Local) } if err := p.start("value"); err != nil { return nil, err } v, err := p.value(0) if err != nil { return nil, err } if err := p.end("param"); err != nil { return nil, err } ps = append(ps, v) } } } // ParseCall parses a methodCall document. func ParseCall(data []byte) (*Call, error) { p, err := newParser(data) if err != nil { return nil, err } if err := p.start("methodCall"); err != nil { return nil, err } if err := p.start("methodName"); err != nil { return nil, err } method, err := p.text() if err != nil { return nil, err } c := &Call{Method: strings.TrimSpace(method), Params: []Value{}} t, err := p.next() if err != nil { return nil, err } if s, ok := t.(xml.StartElement); ok && s.Name.Local == "params" { if c.Params, err = p.params(); err != nil { return nil, err } if err := p.end("methodCall"); err != nil { return nil, err } } else if e, ok := t.(xml.EndElement); !ok || e.Name.Local != "methodCall" { return nil, errors.New("xmlrpc: expected ") } return c, p.eof() } // ParseResponse parses a methodResponse document. func ParseResponse(data []byte) (*Response, error) { p, err := newParser(data) if err != nil { return nil, err } if err := p.start("methodResponse"); err != nil { return nil, err } t, err := p.next() if err != nil { return nil, err } s, _ := t.(xml.StartElement) r := &Response{} switch s.Name.Local { case "params": if r.Params, err = p.params(); err != nil { return nil, err } case "fault": if r.Fault, err = p.fault(); err != nil { return nil, err } default: return nil, errors.New("xmlrpc: expected or ") } if err := p.end("methodResponse"); err != nil { return nil, err } return r, p.eof() } func (p *parser) fault() (*Fault, error) { if err := p.start("value"); err != nil { return nil, err } v, err := p.value(0) if err != nil { return nil, err } if err := p.end("fault"); err != nil { return nil, err } st, _ := v.(Struct) code, _ := st.Get("faultCode") msg, _ := st.Get("faultString") f := &Fault{} var ok1, ok2 bool f.Code, ok1 = code.(int64) f.String, ok2 = msg.(string) if !ok1 || !ok2 { return nil, errors.New("xmlrpc: bad fault") } return f, nil } // Marshal writes the call. It panics on a value of an unsupported type. func (c *Call) Marshal() []byte { var b bytes.Buffer b.WriteString("\n\n") xml.EscapeText(&b, []byte(c.Method)) b.WriteString("\n") writeParams(&b, c.Params) b.WriteString("\n") return b.Bytes() } // Marshal writes the response. It panics on a value of an unsupported type. func (r *Response) Marshal() []byte { var b bytes.Buffer b.WriteString("\n\n") if r.Fault != nil { b.WriteString("\n") writeValue(&b, Struct{{"faultCode", r.Fault.Code}, {"faultString", r.Fault.String}}) b.WriteString("\n") } else { writeParams(&b, r.Params) } b.WriteString("\n") return b.Bytes() } func writeParams(b *bytes.Buffer, ps []Value) { b.WriteString("\n") for _, v := range ps { b.WriteString("\n") writeValue(b, v) b.WriteString("\n") } b.WriteString("\n") } // writeText escapes s; xml.EscapeText also escapes quotes and \t\n\r as // character references, so whitespace survives exactly. func writeText(b *bytes.Buffer, s string) { xml.EscapeText(b, []byte(s)) } func writeValue(b *bytes.Buffer, v Value) { b.WriteString("") switch v := v.(type) { case nil: b.WriteString("") case int64: fmt.Fprintf(b, "%d", v) case int: fmt.Fprintf(b, "%d", v) case bool: if v { b.WriteString("1") } else { b.WriteString("0") } case string: b.WriteString("") writeText(b, v) b.WriteString("") case float64: // Shortest round-trip form; Python's float() parses it, NaN and Inf too. fmt.Fprintf(b, "%s", strconv.FormatFloat(v, 'g', -1, 64)) case []byte: fmt.Fprintf(b, "%s", base64.StdEncoding.EncodeToString(v)) case DateTime: b.WriteString("") writeText(b, string(v)) b.WriteString("") case []Value: b.WriteString("\n") for _, e := range v { writeValue(b, e) } b.WriteString("") case Struct: b.WriteString("\n") for _, m := range v { b.WriteString("\n") writeText(b, m.Name) b.WriteString("\n") writeValue(b, m.Value) b.WriteString("\n") } b.WriteString("") default: panic(fmt.Sprintf("xmlrpc: unsupported type %T", v)) } b.WriteString("\n") }