diff --git a/cmd/samldemo/main.go b/cmd/samldemo/main.go
new file mode 100644
index 0000000000..98fe23475e
--- /dev/null
+++ b/cmd/samldemo/main.go
@@ -0,0 +1,26 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "yunion.io/x/onecloud/pkg/util/atexit"
+ "yunion.io/x/onecloud/pkg/util/samlutils/demo"
+)
+
+func main() {
+ defer atexit.Handle()
+
+ demo.StartServer()
+}
diff --git a/go.mod b/go.mod
index 3810925ee4..c0f070bbea 100644
--- a/go.mod
+++ b/go.mod
@@ -24,6 +24,7 @@ require (
github.com/aokoli/goutils v1.0.1
github.com/aws/aws-sdk-go v1.21.4
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
+ github.com/beevik/etree v1.1.0 // indirect
github.com/benbjohnson/clock v1.0.0
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
@@ -70,6 +71,7 @@ require (
github.com/kr/pty v1.1.5
github.com/lib/pq v1.2.0 // indirect
github.com/libvirt/libvirt-go-xml v5.2.0+incompatible
+ github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/mattn/go-sqlite3 v1.10.0 // indirect
github.com/mattn/go-tty v0.0.0-20181127064339-e4f871175a2f // indirect
diff --git a/go.sum b/go.sum
index 15b75df5c1..18f4ed1350 100644
--- a/go.sum
+++ b/go.sum
@@ -150,6 +150,8 @@ github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/bazelbuild/bazel-gazelle v0.0.0-20181012220611-c728ce9f663e/go.mod h1:uHBSeeATKpVazAACZBDPL/Nk/UhQDDsJWDlqYJo8/Us=
github.com/bazelbuild/buildtools v0.0.0-20180226164855-80c7f0d45d7e/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=
+github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w=
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -511,6 +513,8 @@ github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH
github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk=
github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao=
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=
+github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60 h1:q5rqPuvxdOzg1NC6sls3XSpRokQ5KAmLIYdLW1/kyOo=
+github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60/go.mod h1:KEgVcb43+f5KFUH/x6Vd3NROG0AIL2CuKMrIqYsmx6E=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
diff --git a/pkg/appsrv/send.go b/pkg/appsrv/send.go
index d0169f61d2..1c02f368dc 100644
--- a/pkg/appsrv/send.go
+++ b/pkg/appsrv/send.go
@@ -73,8 +73,18 @@ func SendHeader(w http.ResponseWriter, hdr http.Header) {
}
func SendXml(w http.ResponseWriter, hdr http.Header, obj interface{}) {
+ SendXmlWithIndent(w, hdr, obj, false)
+}
+
+func SendXmlWithIndent(w http.ResponseWriter, hdr http.Header, obj interface{}, indent bool) {
if !gotypes.IsNil(obj) {
- xmlBytes, err := xml.Marshal(obj)
+ var xmlBytes []byte
+ var err error
+ if indent {
+ xmlBytes, err = xml.MarshalIndent(obj, "", " ")
+ } else {
+ xmlBytes, err = xml.Marshal(obj)
+ }
if err == nil {
for k, v := range hdr {
if k != "Content-Type" && k != "Content-Length" {
diff --git a/pkg/util/samlutils/consts.go b/pkg/util/samlutils/consts.go
new file mode 100644
index 0000000000..541d34263a
--- /dev/null
+++ b/pkg/util/samlutils/consts.go
@@ -0,0 +1,59 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+const (
+ XMLNS_MD = "urn:oasis:names:tc:SAML:2.0:metadata"
+ XMLNS_DS = "http://www.w3.org/2000/09/xmldsig#"
+ XMLNS_PROTO = "urn:oasis:names:tc:SAML:2.0:protocol"
+ XMLNS_ASSERT = "urn:oasis:names:tc:SAML:2.0:assertion"
+
+ PROTOCOL_SAML2 = "urn:oasis:names:tc:SAML:2.0:protocol"
+
+ KEY_USE_SIGNING = "signing"
+ KEY_USE_ENCRYPTION = "encryption"
+
+ NAME_ID_FORMAT_PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
+ NAME_ID_FORMAT_TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
+ NAME_ID_FORMAT_EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ NAME_ID_FORMAT_UNSPEC = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ NAME_ID_FORMAT_X509 = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"
+ NAME_ID_FORMAT_WINDOWS = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"
+ NAME_ID_FORMAT_KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"
+ NAME_ID_FORMAT_ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
+
+ SAML2_VERSION = "2.0"
+
+ STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
+
+ BINDING_HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ BINDING_HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+
+ HTML_SAML_FORM_TOKEN = "$FORM$"
+ DEFAULT_HTML_TEMPLATE = `
$FORM$`
+)
+
+var (
+ NAME_ID_FORMATS = []string{
+ NAME_ID_FORMAT_PERSISTENT,
+ NAME_ID_FORMAT_TRANSIENT,
+ NAME_ID_FORMAT_EMAIL,
+ NAME_ID_FORMAT_UNSPEC,
+ NAME_ID_FORMAT_X509,
+ NAME_ID_FORMAT_WINDOWS,
+ NAME_ID_FORMAT_KERBEROS,
+ NAME_ID_FORMAT_ENTITY,
+ }
+)
diff --git a/pkg/util/samlutils/demo/doc.go b/pkg/util/samlutils/demo/doc.go
new file mode 100644
index 0000000000..220a34214c
--- /dev/null
+++ b/pkg/util/samlutils/demo/doc.go
@@ -0,0 +1,15 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package demo // import "yunion.io/x/onecloud/pkg/util/samlutils/demo"
diff --git a/pkg/util/samlutils/demo/service.go b/pkg/util/samlutils/demo/service.go
new file mode 100644
index 0000000000..55d8026dcf
--- /dev/null
+++ b/pkg/util/samlutils/demo/service.go
@@ -0,0 +1,413 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package demo
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+
+ "yunion.io/x/log"
+ "yunion.io/x/pkg/errors"
+ "yunion.io/x/structarg"
+
+ "yunion.io/x/onecloud/pkg/appsrv"
+ "yunion.io/x/onecloud/pkg/httperrors"
+ "yunion.io/x/onecloud/pkg/util/fileutils2"
+ "yunion.io/x/onecloud/pkg/util/httputils"
+ "yunion.io/x/onecloud/pkg/util/samlutils"
+ "yunion.io/x/onecloud/pkg/util/samlutils/idp"
+)
+
+type Options struct {
+ Help bool `help:"show help"`
+ Cert string `help:"certificate file"`
+ Key string `help:"certificate private key file"`
+ Port int `help:"listening port"`
+ Entity string `help:"SAML entityID"`
+ SpMeta []string `help:"ServiceProvider metadata filename"`
+}
+
+func showErrorAndExit(e error) {
+ fmt.Fprintf(os.Stderr, "%s", e)
+ fmt.Fprintln(os.Stderr)
+ os.Exit(1)
+}
+
+func StartServer() {
+ err := prepareServer()
+ if err != nil {
+ showErrorAndExit(err)
+ } else {
+ fmt.Println("exit cleanly")
+ }
+}
+
+func prepareServer() error {
+ parser, err := structarg.NewArgumentParser(
+ &Options{},
+ "samldemo",
+ "A demo SAML 2.0 https server",
+ `See "ipmicli help COMMAND" for help on a specific command.`,
+ )
+ if err != nil {
+ return errors.Wrap(err, "NewArgumentParser")
+ }
+
+ err = parser.ParseArgs(os.Args[1:], false)
+ options := parser.Options().(*Options)
+
+ if options.Help {
+ fmt.Print(parser.HelpString())
+ return nil
+ }
+
+ if len(options.Entity) == 0 {
+ return errors.Wrap(httperrors.ErrInputParameter, "empty entityID")
+ }
+ if options.Port <= 0 {
+ return errors.Wrap(httperrors.ErrInputParameter, "port must be positive integer")
+ }
+ if len(options.Key) == 0 {
+ return errors.Wrap(httperrors.ErrInputParameter, "key file must be present")
+ }
+ if !fileutils2.Exists(options.Key) {
+ return errors.Wrapf(httperrors.ErrInputParameter, "key %s not found", options.Key)
+ }
+ if len(options.Cert) == 0 {
+ return errors.Wrap(httperrors.ErrInputParameter, "cert file must be present")
+ }
+ if !fileutils2.Exists(options.Cert) {
+ return errors.Wrapf(httperrors.ErrInputParameter, "cert %s not found", options.Cert)
+ }
+
+ app := appsrv.NewApplication("samldemo", 4, false)
+
+ saml, err := samlutils.NewSAMLInstance(options.Entity, options.Cert, options.Key)
+ if err != nil {
+ return errors.Wrap(err, "NewSAMLInstance")
+ }
+
+ spFunc := func(ctx context.Context, sp *idp.SSAMLServiceProvider) samlutils.SSAMLSpInitiatedLoginData {
+ log.Debugf("Recive SP initiated Login: %s", sp.GetEntityId())
+ data := samlutils.SSAMLSpInitiatedLoginData{}
+ switch sp.GetEntityId() {
+ case "https://auth.huaweicloud.com/": // 华为云 SSO
+ data.NameId = "yunionoss"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
+ data.AudienceRestriction = sp.GetEntityId()
+ for k, v := range map[string]string{
+ // "xUserId": "052d45a3e70010440f92c000d9e3f260",
+ // "xAccountId": "052d45a3e70010440f92c000d9e3f260",
+ // "bpId": "c58a60a2e0a046c8afa77286924c2b0d",
+ // "name": "yunionoss",
+ // "email": "qiujian@yunion.cn",
+ // "mobile": "13811299225",
+ "User": "ec2admin",
+ "Group": "ec2admin",
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: k, FriendlyName: k,
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []string{v},
+ })
+ }
+ case "https://samltest.id/saml/sp": // samltest.id SSO
+ data.NameId = "yunion"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
+ data.AudienceRestriction = sp.GetEntityId()
+ for _, v := range []struct {
+ name string
+ friendlyName string
+ value string
+ }{
+ {
+ name: "urn:oid:0.9.2342.19200300.100.1.1",
+ friendlyName: "uid",
+ value: "9646D89D-F5E7-F0E4-C545A9B2F4B7956B",
+ },
+ {
+ name: "urn:oid:0.9.2342.19200300.100.1.3",
+ friendlyName: "mail",
+ value: "samltest@yunion.io",
+ },
+ {
+ name: "urn:oid:2.5.4.4",
+ friendlyName: "sn",
+ value: "Jian",
+ },
+ {
+ name: "urn:oid:2.5.4.42",
+ friendlyName: "givenName",
+ value: "Jian",
+ },
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: v.name,
+ FriendlyName: v.friendlyName,
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []string{v.value},
+ })
+ }
+ case "cloud.tencent.com": // 腾讯云 role SSO
+ data.NameId = "cvmcosreadonly"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
+ data.AudienceRestriction = "https://cloud.tencent.com"
+ for _, v := range []struct {
+ name string
+ friendlyName string
+ value string
+ }{
+ {
+ name: "https://cloud.tencent.com/SAML/Attributes/Role",
+ friendlyName: "RoleEntitlement",
+ value: "qcs::cam::uin/100008182714:roleName/cvmcosreadonly,qcs::cam::uin/100008182714:saml-provider/saml.yunion.io",
+ },
+ {
+ name: "https://cloud.tencent.com/SAML/Attributes/RoleSessionName",
+ friendlyName: "RoleSessionName",
+ value: "cvmcosreadonly",
+ },
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: v.name,
+ FriendlyName: v.friendlyName,
+ Values: []string{v.value},
+ })
+ }
+ case "google.com/a/yunion-hk.com":
+ data.NameId = "qiujian"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
+ data.AudienceRestriction = sp.GetEntityId()
+ for k, v := range map[string]string{
+ "user.email": "qiujian@yunion-hk.com",
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: k, FriendlyName: k,
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []string{v},
+ })
+ }
+ case "google.com":
+ data.NameId = "qiujian@yunion-hk.com"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_EMAIL
+ data.AudienceRestriction = sp.GetEntityId()
+ for k, v := range map[string]string{
+ "user.email": "qiujian@yunion-hk.com",
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: k, FriendlyName: k,
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ Values: []string{v},
+ })
+ }
+ }
+ return data
+ }
+
+ idpFunc := func(ctx context.Context, sp *idp.SSAMLServiceProvider, state string) samlutils.SSAMLIdpInitiatedLoginData {
+ log.Debugf("Recive IDP initiated Login: %s", sp.GetEntityId())
+ data := samlutils.SSAMLIdpInitiatedLoginData{}
+ switch sp.GetEntityId() {
+ case "urn:alibaba:cloudcomputing": // 阿里云role SSO
+ data.NameId = "ecsossreadonly"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
+ data.AudienceRestriction = sp.GetEntityId()
+ for k, v := range map[string]string{
+ "https://www.aliyun.com/SAML-Role/Attributes/Role": "acs:ram::1123247935774897:role/administrator,acs:ram::1123247935774897:saml-provider/saml.yunion.io",
+ "https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName": "ecsossreadonly",
+ "https://www.aliyun.com/SAML-Role/Attributes/SessionDuration": "1800",
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: k,
+ Values: []string{v},
+ })
+ }
+ data.RelayState = "https://homenew.console.aliyun.com/"
+ case "urn:amazon:webservices:cn-north-1": // AWS CN role SSO
+ data.NameId = "ec2s3readonly"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
+ data.AudienceRestriction = "https://signin.amazonaws.cn/saml"
+ for _, v := range []struct {
+ name string
+ friendlyName string
+ value string
+ }{
+ {
+ name: "https://aws.amazon.com/SAML/Attributes/Role",
+ friendlyName: "RoleEntitlement",
+ value: "arn:aws-cn:iam::248697896586:role/ec2s3readonly,arn:aws-cn:iam::248697896586:saml-provider/saml.yunion.io",
+ },
+ {
+ name: "https://aws.amazon.com/SAML/Attributes/RoleSessionName",
+ friendlyName: "RoleSessionName",
+ value: "ec2s3readonly",
+ },
+ {
+ name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.3",
+ friendlyName: "eduPersonOrgDN",
+ value: "ec2s3readonly",
+ },
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: v.name,
+ FriendlyName: v.friendlyName,
+ Values: []string{v.value},
+ })
+ }
+ data.RelayState = "https://console.amazonaws.cn/"
+ case "urn:amazon:webservices": // AWS Global role SSO
+ data.NameId = "ec2s3readonly"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
+ data.AudienceRestriction = "https://signin.aws.amazon.com/saml"
+ for _, v := range []struct {
+ name string
+ friendlyName string
+ value string
+ }{
+ {
+ name: "https://aws.amazon.com/SAML/Attributes/Role",
+ friendlyName: "RoleEntitlement",
+ value: "arn:aws:iam::285906155448:role/ec2s3readonly,arn:aws:iam::285906155448:saml-provider/saml.yunion.cn",
+ },
+ {
+ name: "https://aws.amazon.com/SAML/Attributes/RoleSessionName",
+ friendlyName: "RoleSessionName",
+ value: "ec2s3readonly",
+ },
+ {
+ name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.3",
+ friendlyName: "eduPersonOrgDN",
+ value: "ec2s3readonly",
+ },
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: v.name,
+ FriendlyName: v.friendlyName,
+ Values: []string{v.value},
+ })
+ }
+ data.RelayState = "https://console.aws.amazon.com/"
+ case "cloud.tencent.com": // 腾讯云 role SSO
+ data.NameId = "cvmcosreadonly"
+ data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT
+ data.AudienceRestriction = "https://cloud.tencent.com"
+ for _, v := range []struct {
+ name string
+ friendlyName string
+ value string
+ }{
+ {
+ name: "https://cloud.tencent.com/SAML/Attributes/Role",
+ friendlyName: "RoleEntitlement",
+ value: "qcs::cam::uin/100008182714:roleName/cvmcosreadonly,qcs::cam::uin/100008182714:saml-provider/saml.yunion.io",
+ },
+ {
+ name: "https://cloud.tencent.com/SAML/Attributes/RoleSessionName",
+ friendlyName: "RoleSessionName",
+ value: "cvmcosreadonly",
+ },
+ } {
+ data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+ Name: v.name,
+ FriendlyName: v.friendlyName,
+ Values: []string{v.value},
+ })
+ }
+ data.RelayState = "https://console.cloud.tencent.com/"
+ }
+ return data
+ }
+
+ logoutFunc := func(ctx context.Context) string {
+ return fmt.Sprintf(``, httputils.JoinPath(options.Entity, "SAML/idp"))
+ }
+
+ idpInst := idp.NewIdpInstance(saml, spFunc, idpFunc, logoutFunc)
+ for _, spMetaFile := range options.SpMeta {
+ err := idpInst.AddSPMetadataFile(spMetaFile)
+ if err != nil {
+ return errors.Wrapf(err, "AddSPMetadataFile %s", spMetaFile)
+ }
+ }
+ idpInst.AddHandlers(app, "SAML/idp")
+ idpInst.SetHtmlTemplate(`正在跳转到云控制台,请等待。。。
$FORM$`)
+
+ app.AddHandler("GET", "SAML/idp", func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
+ idpInitUrl := httputils.JoinPath(options.Entity, "SAML/idp/sso")
+
+ htmlBuf := strings.Builder{}
+ htmlBuf.WriteString(``)
+ // IDP initiated
+ for _, v := range []struct {
+ name string
+ entityID string
+ }{
+ {
+ name: "Aliyun Role SSO",
+ entityID: "urn:alibaba:cloudcomputing",
+ },
+ {
+ name: "AWS CN Role SSO",
+ entityID: "urn:amazon:webservices:cn-north-1",
+ },
+ {
+ name: "AWS Global Role SSO",
+ entityID: "urn:amazon:webservices",
+ },
+ {
+ name: "Tencent Cloud Role SSO",
+ entityID: "cloud.tencent.com",
+ },
+ } {
+ htmlBuf.WriteString(fmt.Sprintf(`- %s (IDP-Initiated)
`, idpInitUrl, url.QueryEscape(v.entityID), v.name))
+ }
+
+ for _, v := range []struct {
+ name string
+ url string
+ }{
+ /*{
+ name: "Huawei cloud partner SSO",
+ url: "https://auth.huaweicloud.com/authui/saml/login?xAccountType=yunion_IDP&isFirstLogin=false&service=https%3a%2f%2fconsole.huaweicloud.com%2fiam%2f",
+ },*/
+ {
+ name: "Huawei cloud SSO",
+ url: "https://auth.huaweicloud.com/authui/federation/websso?domain_id=052d45a3e70010440f92c000d9e3f260&idp=yunion&protocol=saml",
+ },
+ {
+ name: "Tencent cloud SSO",
+ url: "https://cloud.tencent.com/login/forwardIdp/100008182714/saml.yunion.io",
+ },
+ {
+ name: "Google cloud SSO",
+ url: "https://www.google.com/a/yunion-hk.com/ServiceLogin?continue=https://console.cloud.google.com",
+ },
+ } {
+ htmlBuf.WriteString(fmt.Sprintf(`- %s (SP-Initiated)
`, v.url, v.name))
+ }
+
+ htmlBuf.WriteString(`
`)
+ appsrv.SendHTML(w, htmlBuf.String())
+ })
+
+ addr := fmt.Sprintf(":%d", options.Port)
+ app.ListenAndServeTLS(addr, options.Cert, options.Key)
+
+ return nil
+}
diff --git a/pkg/util/samlutils/doc.go b/pkg/util/samlutils/doc.go
new file mode 100644
index 0000000000..5e131aa2cb
--- /dev/null
+++ b/pkg/util/samlutils/doc.go
@@ -0,0 +1,15 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils // import "yunion.io/x/onecloud/pkg/util/samlutils"
diff --git a/pkg/util/samlutils/encrypt.go b/pkg/util/samlutils/encrypt.go
new file mode 100644
index 0000000000..4ab0c07377
--- /dev/null
+++ b/pkg/util/samlutils/encrypt.go
@@ -0,0 +1,81 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "io/ioutil"
+
+ "yunion.io/x/log"
+ "yunion.io/x/pkg/errors"
+
+ "yunion.io/x/onecloud/pkg/httperrors"
+ "yunion.io/x/onecloud/pkg/util/seclib2"
+)
+
+func decodePrivateKey(keyString []byte) (*rsa.PrivateKey, error) {
+ block, _ := pem.Decode(keyString)
+ log.Debugf("pem.Decode privateKey data: type=%s header: %s", block.Type, block.Headers)
+ privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if err == nil {
+ return privKey.(*rsa.PrivateKey), nil
+ }
+ priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+ if err == nil {
+ return priv, nil
+ }
+ return nil, errors.Wrap(httperrors.ErrInvalidFormat, "not a valid private key")
+}
+
+func (saml *SSAMLInstance) parseKeys() error {
+ privData, err := ioutil.ReadFile(saml.privateKeyFile)
+ if err != nil {
+ return errors.Wrapf(err, "ioutil.ReadFile %s", saml.privateKeyFile)
+ }
+ saml.privateKey, err = decodePrivateKey(privData)
+ if err != nil {
+ return errors.Wrap(err, "decodePrivateKey")
+ }
+
+ certData, err := ioutil.ReadFile(saml.certFile)
+ if err != nil {
+ return errors.Wrapf(err, "ioutil.Readfile %s", saml.certFile)
+ }
+
+ var block *pem.Block
+ saml.certs = make([]*x509.Certificate, 0)
+ first := true
+ for {
+ block, certData = pem.Decode(certData)
+ if block == nil {
+ break
+ }
+ if first {
+ first = false
+ saml.certString = seclib2.CleanCertificate(string(pem.EncodeToMemory(block)))
+
+ log.Debugf("cert: %s", saml.certString)
+ }
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return errors.Wrap(err, "x509.ParseCertificate")
+ }
+ saml.certs = append(saml.certs, cert)
+ }
+
+ return nil
+}
diff --git a/pkg/util/samlutils/idp/doc.go b/pkg/util/samlutils/idp/doc.go
new file mode 100644
index 0000000000..24b820be86
--- /dev/null
+++ b/pkg/util/samlutils/idp/doc.go
@@ -0,0 +1,15 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package idp // import "yunion.io/x/onecloud/pkg/util/samlutils/idp"
diff --git a/pkg/util/samlutils/idp/idp.go b/pkg/util/samlutils/idp/idp.go
new file mode 100644
index 0000000000..5b79dca9aa
--- /dev/null
+++ b/pkg/util/samlutils/idp/idp.go
@@ -0,0 +1,302 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package idp
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/xml"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "yunion.io/x/log"
+ "yunion.io/x/pkg/errors"
+
+ "yunion.io/x/onecloud/pkg/appctx"
+ "yunion.io/x/onecloud/pkg/appsrv"
+ "yunion.io/x/onecloud/pkg/httperrors"
+ "yunion.io/x/onecloud/pkg/util/httputils"
+ "yunion.io/x/onecloud/pkg/util/samlutils"
+)
+
+type OnSpInitiatedLogin func(ctx context.Context, sp *SSAMLServiceProvider) samlutils.SSAMLSpInitiatedLoginData
+type OnIdpInitiatedLogin func(ctx context.Context, sp *SSAMLServiceProvider, state string) samlutils.SSAMLIdpInitiatedLoginData
+type OnLogout func(ctx context.Context) string
+
+type SSAMLIdpInstance struct {
+ saml *samlutils.SSAMLInstance
+
+ metadataPath string
+ redirectLoginPath string
+ redirectLogoutPath string
+ idpInitiatedSSOPath string
+
+ serviceProviders []*SSAMLServiceProvider
+
+ onSpInitiatedLogin OnSpInitiatedLogin
+ onIdpInitiatedLogin OnIdpInitiatedLogin
+ onLogout OnLogout
+
+ htmlTemplate string
+}
+
+func NewIdpInstance(saml *samlutils.SSAMLInstance, spLoginFunc OnSpInitiatedLogin, idpLoginFunc OnIdpInitiatedLogin, logoutFunc OnLogout) *SSAMLIdpInstance {
+ return &SSAMLIdpInstance{
+ saml: saml,
+ onSpInitiatedLogin: spLoginFunc,
+ onIdpInitiatedLogin: idpLoginFunc,
+ onLogout: logoutFunc,
+ }
+}
+
+func (idp *SSAMLIdpInstance) AddHandlers(app *appsrv.Application, prefix string) {
+ idp.metadataPath = httputils.JoinPath(prefix, "metadata")
+ idp.redirectLoginPath = httputils.JoinPath(prefix, "redirect/login")
+ idp.redirectLogoutPath = httputils.JoinPath(prefix, "redirect/logout")
+ idp.idpInitiatedSSOPath = httputils.JoinPath(prefix, "sso")
+
+ app.AddHandler("GET", idp.metadataPath, idp.metadataHandler)
+ app.AddHandler("GET", idp.redirectLoginPath, idp.redirectLoginHandler)
+ app.AddHandler("GET", idp.redirectLogoutPath, idp.redirectLogoutHandler)
+ app.AddHandler("GET", idp.idpInitiatedSSOPath, idp.idpInitiatedSSOHandler)
+
+ log.Infof("IDP metadata: %s", idp.getMetadataUrl())
+ log.Infof("IDP redirect login: %s", idp.getRedirectLoginUrl())
+ log.Infof("IDP redirect logout: %s", idp.getRedirectLogoutUrl())
+ log.Infof("IDP initated SSO: %s", idp.getIdpInitiatedSSOUrl())
+}
+
+func (idp *SSAMLIdpInstance) SetHtmlTemplate(tmp string) error {
+ if strings.Index(tmp, samlutils.HTML_SAML_FORM_TOKEN) < 0 {
+ return errors.Wrapf(httperrors.ErrInvalidFormat, "no %s found", samlutils.HTML_SAML_FORM_TOKEN)
+ }
+ idp.htmlTemplate = tmp
+ return nil
+}
+
+func (idp *SSAMLIdpInstance) AddSPMetadataFile(filename string) error {
+ metadata, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return errors.Wrap(err, "ioutil.ReadFile")
+ }
+ return idp.AddSPMetadata(metadata)
+}
+
+func (idp *SSAMLIdpInstance) AddSPMetadata(metadata []byte) error {
+ ed, err := samlutils.ParseMetadata(metadata)
+ if err != nil {
+ return errors.Wrap(err, "samlutils.ParseMetadata")
+ }
+ sp := &SSAMLServiceProvider{desc: ed}
+ err = sp.IsValid()
+ if err != nil {
+ return errors.Wrap(err, "NewSAMLServiceProvider")
+ }
+ log.Debugf("Register SP metadata: %s", sp.GetEntityId())
+ idp.serviceProviders = append(idp.serviceProviders, sp)
+ return nil
+}
+
+func (idp *SSAMLIdpInstance) getMetadataUrl() string {
+ return httputils.JoinPath(idp.saml.GetEntityId(), idp.metadataPath)
+}
+
+func (idp *SSAMLIdpInstance) getRedirectLoginUrl() string {
+ return httputils.JoinPath(idp.saml.GetEntityId(), idp.redirectLoginPath)
+}
+
+func (idp *SSAMLIdpInstance) getRedirectLogoutUrl() string {
+ return httputils.JoinPath(idp.saml.GetEntityId(), idp.redirectLogoutPath)
+}
+
+func (idp *SSAMLIdpInstance) getIdpInitiatedSSOUrl() string {
+ return httputils.JoinPath(idp.saml.GetEntityId(), idp.idpInitiatedSSOPath)
+}
+
+func (idp *SSAMLIdpInstance) metadataHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
+ desc := idp.getMetadata(ctx)
+ appsrv.SendXmlWithIndent(w, nil, desc, true)
+}
+
+func (idp *SSAMLIdpInstance) redirectLoginHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
+ _, query, _ := appsrv.FetchEnv(ctx, w, r)
+ input := samlutils.SIdpRedirectLoginInput{}
+ err := query.Unmarshal(&input)
+ if err != nil {
+ httperrors.InputParameterError(w, "query.Unmarshal error %s", err)
+ return
+ }
+ log.Debugf("recv input %s", input)
+ respHtml, err := idp.processLoginRequest(ctx, input)
+ if err != nil {
+ httperrors.InputParameterError(w, "parse parameter error %s", err)
+ return
+ }
+ appsrv.SendHTML(w, respHtml)
+}
+
+func (idp *SSAMLIdpInstance) redirectLogoutHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
+ log.Debugf("logout: %s", r.Header)
+ html := idp.onLogout(ctx)
+ appsrv.SendHTML(w, html)
+}
+
+func (idp *SSAMLIdpInstance) idpInitiatedSSOHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
+ _, query, _ := appsrv.FetchEnv(ctx, w, r)
+ input := samlutils.SIdpInitiatedLoginInput{}
+ err := query.Unmarshal(&input)
+ if err != nil {
+ httperrors.InputParameterError(w, "unmarshal input fail %s", err)
+ return
+ }
+ respHtml, err := idp.processIdpInitiatedLogin(ctx, input)
+ if err != nil {
+ httperrors.GeneralServerError(w, err)
+ return
+ }
+ appsrv.SendHTML(w, respHtml)
+}
+
+func (idp *SSAMLIdpInstance) getMetadata(ctx context.Context) samlutils.EntityDescriptor {
+ input := samlutils.SSAMLIdpMetadataInput{
+ EntityId: idp.saml.GetEntityId(),
+ CertString: idp.saml.GetCertString(),
+ RedirectLoginUrl: idp.getRedirectLoginUrl(),
+ RedirectLogoutUrl: idp.getRedirectLogoutUrl(),
+ }
+ hostId := appctx.AppContextHostId(ctx)
+ return samlutils.NewIdpMetadata(hostId, input)
+}
+
+func (idp *SSAMLIdpInstance) processLoginRequest(ctx context.Context, input samlutils.SIdpRedirectLoginInput) (string, error) {
+ plainText, err := samlutils.SAMLDecode(input.SAMLRequest)
+ if err != nil {
+ return "", errors.Wrap(err, "samlutils.SAMLDecode")
+ }
+
+ log.Debugf("AuthnRequest: %s", string(plainText))
+
+ authReq := samlutils.AuthnRequest{}
+ err = xml.Unmarshal(plainText, &authReq)
+ if err != nil {
+ return "", errors.Wrap(err, "xml.Unmarshal")
+ }
+
+ sp := idp.getServiceProvider(authReq.Issuer.Issuer)
+ if sp == nil {
+ return "", errors.Wrapf(httperrors.ErrResourceNotFound, "issuer %s not found", authReq.Issuer.Issuer)
+ }
+
+ if len(authReq.Destination) > 0 && authReq.Destination != idp.getRedirectLoginUrl() {
+ return "", errors.Wrapf(httperrors.ErrInputParameter, "Destination not match: get %s want %s", authReq.Destination, idp.getRedirectLoginUrl())
+ }
+
+ if authReq.AssertionConsumerServiceURL != sp.GetPostAssertionConsumerServiceUrl() {
+ return "", errors.Wrapf(httperrors.ErrInputParameter, "AssertionConsumerServiceURL not match: get %s want %s", authReq.AssertionConsumerServiceURL, sp.GetPostAssertionConsumerServiceUrl())
+ }
+
+ resp, err := idp.getLoginResponse(ctx, authReq, sp)
+ if err != nil {
+ return "", errors.Wrap(err, "getLoginResponse")
+ }
+
+ form, err := idp.samlResponse2Form(authReq.AssertionConsumerServiceURL, resp, input.RelayState)
+ if err != nil {
+ return "", errors.Wrap(err, "samlResponse2Form")
+ }
+
+ return form, nil
+}
+
+func (idp *SSAMLIdpInstance) samlResponse2Form(url string, resp *samlutils.Response, state string) (string, error) {
+ respXml, err := xml.Marshal(resp)
+ if err != nil {
+ return "", errors.Wrap(err, "xml.Marshal")
+ }
+
+ signed, err := idp.saml.SignXML(string(respXml))
+ if err != nil {
+ return "", errors.Wrap(err, "saml.SignXML")
+ }
+
+ log.Debugf("ResponseXML: %s", signed)
+
+ samlResp := base64.StdEncoding.EncodeToString([]byte(signed))
+
+ form := samlutils.SAMLForm(url, map[string]string{
+ "SAMLResponse": samlResp,
+ "RelayState": state,
+ })
+ template := samlutils.DEFAULT_HTML_TEMPLATE
+ if len(idp.htmlTemplate) > 0 {
+ template = idp.htmlTemplate
+ }
+ form = strings.Replace(template, samlutils.HTML_SAML_FORM_TOKEN, form, 1)
+ return form, nil
+}
+
+func (idp *SSAMLIdpInstance) getServiceProvider(eId string) *SSAMLServiceProvider {
+ for _, sp := range idp.serviceProviders {
+ if sp.GetEntityId() == eId {
+ return sp
+ }
+ }
+ return nil
+}
+
+func (idp *SSAMLIdpInstance) getLoginResponse(ctx context.Context, req samlutils.AuthnRequest, sp *SSAMLServiceProvider) (*samlutils.Response, error) {
+ data := idp.onSpInitiatedLogin(ctx, sp)
+ input := samlutils.SSAMLResponseInput{
+ IssuerCertString: idp.saml.GetCertString(),
+ IssuerEntityId: idp.saml.GetEntityId(),
+ RequestID: req.ID,
+ RequestEntityId: req.Issuer.Issuer,
+ AssertionConsumerServiceURL: req.AssertionConsumerServiceURL,
+ SSAMLSpInitiatedLoginData: data,
+ }
+ resp := samlutils.NewResponse(input)
+ return &resp, nil
+}
+
+func (idp *SSAMLIdpInstance) processIdpInitiatedLogin(ctx context.Context, input samlutils.SIdpInitiatedLoginInput) (string, error) {
+ sp := idp.getServiceProvider(input.EntityID)
+ if sp == nil {
+ return "", errors.Wrapf(httperrors.ErrResourceNotFound, "issuer %s not found", input.EntityID)
+ }
+ var state []byte
+ if len(input.State) > 0 {
+ var err error
+ state, err = samlutils.SAMLDecode(input.State)
+ if err != nil {
+ return "", errors.Wrapf(httperrors.ErrInputParameter, "invalid state %s: %s", input.State, err)
+ }
+ }
+ data := idp.onIdpInitiatedLogin(ctx, sp, string(state))
+ respInput := samlutils.SSAMLResponseInput{
+ IssuerCertString: idp.saml.GetCertString(),
+ IssuerEntityId: idp.saml.GetEntityId(),
+ RequestID: "",
+ RequestEntityId: sp.GetEntityId(),
+ AssertionConsumerServiceURL: sp.GetPostAssertionConsumerServiceUrl(),
+ SSAMLSpInitiatedLoginData: data.SSAMLSpInitiatedLoginData,
+ }
+ resp := samlutils.NewResponse(respInput)
+ form, err := idp.samlResponse2Form(sp.GetPostAssertionConsumerServiceUrl(), &resp, data.RelayState)
+ if err != nil {
+ return "", errors.Wrap(err, "samlResponse2Form")
+ }
+ return form, nil
+}
diff --git a/pkg/util/samlutils/idp/sp.go b/pkg/util/samlutils/idp/sp.go
new file mode 100644
index 0000000000..cc2ae96668
--- /dev/null
+++ b/pkg/util/samlutils/idp/sp.go
@@ -0,0 +1,56 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package idp
+
+import (
+ "context"
+
+ "yunion.io/x/pkg/errors"
+
+ "yunion.io/x/onecloud/pkg/httperrors"
+ "yunion.io/x/onecloud/pkg/util/samlutils"
+)
+
+type ISAMLSpDriver interface {
+ RenderResponse(ctx context.Context, resp samlutils.Response) samlutils.Response
+}
+
+type SSAMLServiceProvider struct {
+ desc samlutils.EntityDescriptor
+ driver ISAMLSpDriver
+}
+
+func (sp *SSAMLServiceProvider) GetEntityId() string {
+ return sp.desc.EntityId
+}
+
+func (sp *SSAMLServiceProvider) GetPostAssertionConsumerServiceUrl() string {
+ for _, srv := range sp.desc.SPSSODescriptor.AssertionConsumerServices {
+ if srv.Binding == samlutils.BINDING_HTTP_POST {
+ return srv.Location
+ }
+ }
+ return ""
+}
+
+func (sp *SSAMLServiceProvider) IsValid() error {
+ if sp.GetEntityId() == "" {
+ return errors.Wrap(httperrors.ErrInputParameter, "empty entityID")
+ }
+ if sp.GetPostAssertionConsumerServiceUrl() == "" {
+ return errors.Wrap(httperrors.ErrInputParameter, "empty HTTP_Post AssertionConsumerServiceUrl")
+ }
+ return nil
+}
diff --git a/pkg/util/samlutils/instance.go b/pkg/util/samlutils/instance.go
new file mode 100644
index 0000000000..22e38ddf39
--- /dev/null
+++ b/pkg/util/samlutils/instance.go
@@ -0,0 +1,59 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+
+ "yunion.io/x/pkg/errors"
+)
+
+type SSAMLInstance struct {
+ entityID string
+
+ privateKeyFile string
+ certFile string
+ certString string
+
+ privateKey *rsa.PrivateKey
+
+ certs []*x509.Certificate
+}
+
+func NewSAMLInstance(entityID string, cert, key string) (*SSAMLInstance, error) {
+ saml := SSAMLInstance{
+ privateKeyFile: key,
+ certFile: cert,
+ entityID: entityID,
+ }
+ err := saml.parseKeys()
+ if err != nil {
+ return nil, errors.Wrap(err, "saml.parseKeys")
+ }
+ return &saml, nil
+}
+
+func (saml *SSAMLInstance) GetEntityId() string {
+ return saml.entityID
+}
+
+func (saml *SSAMLInstance) GetCertString() string {
+ return "\n" + saml.certString + "\n"
+}
+
+func (saml *SSAMLInstance) SignXML(xmlstr string) (string, error) {
+ return SignXML(xmlstr, saml.privateKey)
+}
diff --git a/pkg/util/samlutils/metadata.go b/pkg/util/samlutils/metadata.go
new file mode 100644
index 0000000000..4b01321c2b
--- /dev/null
+++ b/pkg/util/samlutils/metadata.go
@@ -0,0 +1,138 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import (
+ "encoding/xml"
+
+ "yunion.io/x/pkg/errors"
+)
+
+func ParseMetadata(data []byte) (EntityDescriptor, error) {
+ ed := EntityDescriptor{}
+ err := xml.Unmarshal(data, &ed)
+ if err != nil {
+ return ed, errors.Wrap(err, "xml.Unmarshal")
+ }
+ return ed, nil
+}
+
+type SSAMLIdpMetadataInput struct {
+ EntityId string
+ CertString string
+ RedirectLoginUrl string
+ RedirectLogoutUrl string
+}
+
+func NewIdpMetadata(id string, input SSAMLIdpMetadataInput) EntityDescriptor {
+ desc := EntityDescriptor{
+ XMLName: xml.Name{
+ Space: XMLNS_MD,
+ Local: "EntityDescriptor",
+ },
+ EntityId: input.EntityId,
+ IDPSSODescriptor: &SSODescriptor{
+ XMLName: xml.Name{
+ Space: XMLNS_MD,
+ Local: "IDPSSODescriptor",
+ },
+ ProtocolSupportEnumeration: PROTOCOL_SAML2,
+ KeyDescriptors: []KeyDescriptor{
+ {
+ XMLName: xml.Name{
+ Space: XMLNS_MD,
+ Local: "KeyDescriptor",
+ },
+ Use: KEY_USE_SIGNING,
+ KeyInfo: KeyInfo{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "KeyInfo",
+ },
+ X509Data: X509Data{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "X509Data",
+ },
+ X509Certificate: X509Certificate{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "X509Certificate",
+ },
+ Cert: input.CertString,
+ },
+ },
+ },
+ },
+ {
+ XMLName: xml.Name{
+ Space: XMLNS_MD,
+ Local: "KeyDescriptor",
+ },
+ Use: KEY_USE_ENCRYPTION,
+ KeyInfo: KeyInfo{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "KeyInfo",
+ },
+ X509Data: X509Data{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "X509Data",
+ },
+ X509Certificate: X509Certificate{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "X509Certificate",
+ },
+ Cert: input.CertString,
+ },
+ },
+ },
+ },
+ },
+ SingleLogoutServices: []SSAMLService{
+ {
+ XMLName: xml.Name{
+ Space: XMLNS_MD,
+ Local: "SingleLogoutService",
+ },
+ Binding: BINDING_HTTP_REDIRECT,
+ Location: input.RedirectLogoutUrl,
+ },
+ },
+ NameIDFormat: []SSAMLNameIDFormat{
+ {
+ XMLName: xml.Name{
+ Space: XMLNS_MD,
+ Local: "NameIDFormat",
+ },
+ Format: NAME_ID_FORMAT_TRANSIENT,
+ },
+ },
+ SingleSignOnServices: []SSAMLService{
+ {
+ XMLName: xml.Name{
+ Space: XMLNS_MD,
+ Local: "SingleSignOnService",
+ },
+ Binding: BINDING_HTTP_REDIRECT,
+ Location: input.RedirectLoginUrl,
+ },
+ },
+ },
+ }
+ return desc
+}
diff --git a/pkg/util/samlutils/metadata_test.go b/pkg/util/samlutils/metadata_test.go
new file mode 100644
index 0000000000..54e07799e6
--- /dev/null
+++ b/pkg/util/samlutils/metadata_test.go
@@ -0,0 +1,670 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import (
+ "encoding/xml"
+ "testing"
+)
+
+var (
+ spMetadata1 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SAMLtest SP
+ A free and basic SP for testing SAML deployments
+ https://samltest.id/saml/logo.png
+
+
+
+
+
+
+
+
+MIIERTCCAq2gAwIBAgIJAKmtzjCD1+tqMA0GCSqGSIb3DQEBCwUAMDUxMzAxBgNV
+BAMTKmlwLTE3Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDAe
+Fw0xODA4MTgyMzI0MjNaFw0yODA4MTUyMzI0MjNaMDUxMzAxBgNVBAMTKmlwLTE3
+Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDCCAaIwDQYJKoZI
+hvcNAQEBBQADggGPADCCAYoCggGBALhUlY3SkIOze+l8y6dBzM6p7B8OykJWlwiz
+szU16Lih8D7KLhNJfahoVxbPxB3YFM/81PJLOeK2krvJ5zY6CJyQY3sPQAkZKI7I
+8qq9lmZ2g4QPqybNstXS6YUXJNUt/ixbbK/N97+LKTiSutbD1J7AoFnouMuLjlhN
+5VRZ43jez4xLSHVZaYuUFKn01Y9oLKbj46LQnZnJCAGpTgPqEQJr6GpVGw43bKyU
+pGoaPrdDRgRgtPMUWgFDkgcI3QiV1lsKfBs1t1E2UA7ACFnlJZpEuBtwgivzo3Ve
+itiSaF3Jxh25EY5/vABpcgQQRz3RH2l8MMKdRsxb8VT3yh2S+CX55s+cN67LiCPr
+6f2u+KS1iKfB9mWN6o2S4lcmo82HIBbsuXJV0oA1HrGMyyc4Y9nng/I8iuAp8or1
+JrWRHQ+8NzO85DWK0rtvtLPxkvw0HK32glyuOP/9F05Z7+tiVIgn67buC0EdoUm1
+RSpibqmB1ST2PikslOlVbJuy4Ah93wIDAQABo1gwVjA1BgNVHREELjAsgippcC0x
+NzItMzEtMjgtNjQudXMtd2VzdC0yLmNvbXB1dGUuaW50ZXJuYWwwHQYDVR0OBBYE
+FAdsTxYfulJ5yunYtgYJHC9IcevzMA0GCSqGSIb3DQEBCwUAA4IBgQB3J6i7Krei
+HL8NPMglfWLHk1PZOgvIEEpKL+GRebvcbyqgcuc3VVPylq70VvGqhJxp1q/mzLfr
+aUiypzfWFGm9zfwIg0H5TqRZYEPTvgIhIICjaDWRwZBDJG8D5G/KoV60DlUG0crP
+BlIuCCr/SRa5ZoDQqvucTfr3Rx4Ha6koXFSjoSXllR+jn4GnInhm/WH137a+v35P
+UcffNxfuehoGn6i4YeXF3cwJK4e35cOFW+dLbnaLk+Ty7HOGvpw86h979C6mJ9qE
+HYgq9rQyzlSPbLZGZSgVcIezunOaOsWm81BsXRNNJjzHGCqKf8RMhd8oZP55+2/S
+VRBwnkGyUNCuDPrJcymC95ZT2NW/KeWkz28HF2i31xQmecT2r3lQRSM8acvOXQsN
+EDCDvJvCzJT9c2AnsnO24r6arPXs/UWAxOI+MjclXPLkLD6uTHV+Oo8XZ7bOjegD
+5hL6/bKUWnNMurQNGrmi/jvqsCFLDKftl7ajuxKjtodnSuwhoY7NQy8=
+
+
+
+
+
+
+
+
+
+MIIERTCCAq2gAwIBAgIJAKGA/tV7hXUvMA0GCSqGSIb3DQEBCwUAMDUxMzAxBgNV
+BAMTKmlwLTE3Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDAe
+Fw0xODA4MTgyMzI0MjVaFw0yODA4MTUyMzI0MjVaMDUxMzAxBgNVBAMTKmlwLTE3
+Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDCCAaIwDQYJKoZI
+hvcNAQEBBQADggGPADCCAYoCggGBANoi7TtbPz5DD5b+pGj2bWHUWcOm135Dl+kf
+KWcJV6x4Z4VRMa33nwSfFg6U0DhPaA6rYr8BfcmCIY4V4cGlJkLNsYbgbZNnrLh2
+3mj7jkaUeyv/DlGtLBcqr0gP6eDtcOf3MMGAkhROcicMj6i+uF6hqLDh4eNcpqEV
+DVn+ADBsosIPiAx+RkcyZkfAF3UeGEV5WTSiQw7qYpI7x+c4ViiBzV4waBgXjvNN
+72Dqlc01AylpmMKaUPfxIpPC+Ctr0bHu5xn7NxMS8Zt5NDWsP9T15qrpYatW68sX
+VyE5nJRYpiRiRbo8i7QpUEya+TkXEI8PVD3KBw9UwhqL8qPPe0T+EeaawF6BVRTE
+Pc+Mn4lGBr4cCFcGk/PLHeyksgPdjNmO1g7y5TWQzu21WzkXRTWJq7wGwWeW6Nrc
+NqweYPLbXEo0JlmHqunkUs+NsLQAFqSPX02P2xzkA/eOU2o/jN4jAPNpzqxJouvm
+iWGXl8Qy4U7vQZ0tGvlTDSltATOQ/QIDAQABo1gwVjA1BgNVHREELjAsgippcC0x
+NzItMzEtMjgtNjQudXMtd2VzdC0yLmNvbXB1dGUuaW50ZXJuYWwwHQYDVR0OBBYE
+FBBtS9YNKSIwViH37GJCTxjNBzLAMA0GCSqGSIb3DQEBCwUAA4IBgQDWXcaI7zMn
+hGsLVTUA6dgzZCa88QkN/Z6n7lCY2oaKj1neBAWA1Mxg7GBJsmLOrHN8ie0D/uKA
+F+7NqKCXYqd0PpTX7c1NICL92DvbugG/Ow50j5Dw6rU4Y8dPS7Y/T1ddbT2F9/5l
+HCIWP/O2E9HREJ0JAIbu/Mi0CE1qui2aSJMDWKuiGK63M/7fvP51m6xSJOfZBhmj
+gllIwEhIzfh4hVPhH0C7iqVls34UyLCZ8IZOCuGPJyTaJN6Pi3Uo1Otkz/1igN5M
+pQhVaeYG7SMgha6skTLrVXTt4CuMVsOZ6cG3kHqw8XZoRld+I50iyHqansf5qwzm
+NoPeXyjGRFQzV/EH3SUu8eAISTt9pfirwjKsVNHrmMRnQEB/hJYYbTWSsvdS8ghw
+7a/A0EKQPVaZGCP/hcpt9JMMb66y2L8VgBbb6aTsR+Uabf6aiMnj1UBMUz9yaMka
+kKM7e66uHdXUDZ/s8F5rPOGCK+O8O6EsLRf8XetRWLa1TXRDkJZVPX4=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+ idpMetadata = `
+
+
+
+
+
+ samltest.id
+
+
+
+ SAMLtest IdP
+ A free and basic IdP for testing SAML deployments
+ https://samltest.id/saml/logo.png
+
+
+
+
+
+
+
+MIIDETCCAfmgAwIBAgIUZRpDhkNKl5eWtJqk0Bu1BgTTargwDQYJKoZIhvcNAQEL
+BQAwFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwHhcNMTgwODI0MjExNDEwWhcNMzgw
+ODI0MjExNDEwWjAWMRQwEgYDVQQDDAtzYW1sdGVzdC5pZDCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAJrh9/PcDsiv3UeL8Iv9rf4WfLPxuOm9W6aCntEA
+8l6c1LQ1Zyrz+Xa/40ZgP29ENf3oKKbPCzDcc6zooHMji2fBmgXp6Li3fQUzu7yd
++nIC2teejijVtrNLjn1WUTwmqjLtuzrKC/ePoZyIRjpoUxyEMJopAd4dJmAcCq/K
+k2eYX9GYRlqvIjLFoGNgy2R4dWwAKwljyh6pdnPUgyO/WjRDrqUBRFrLQJorR2kD
+c4seZUbmpZZfp4MjmWMDgyGM1ZnR0XvNLtYeWAyt0KkSvFoOMjZUeVK/4xR74F8e
+8ToPqLmZEg9ZUx+4z2KjVK00LpdRkH9Uxhh03RQ0FabHW6UCAwEAAaNXMFUwHQYD
+VR0OBBYEFJDbe6uSmYQScxpVJhmt7PsCG4IeMDQGA1UdEQQtMCuCC3NhbWx0ZXN0
+LmlkhhxodHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwMA0GCSqGSIb3DQEBCwUA
+A4IBAQBNcF3zkw/g51q26uxgyuy4gQwnSr01Mhvix3Dj/Gak4tc4XwvxUdLQq+jC
+cxr2Pie96klWhY/v/JiHDU2FJo9/VWxmc/YOk83whvNd7mWaNMUsX3xGv6AlZtCO
+L3JhCpHjiN+kBcMgS5jrtGgV1Lz3/1zpGxykdvS0B4sPnFOcaCwHe2B9SOCWbDAN
+JXpTjz1DmJO4ImyWPJpN1xsYKtm67Pefxmn0ax0uE2uuzq25h0xbTkqIQgJzyoE/
+DPkBFK1vDkMfAW11dQ0BXatEnW7Gtkc0lh2/PIbHWj4AzxYMyBf5Gy6HSVOftwjC
+voQR2qr2xJBixsg+MIORKtmKHLfU
+
+
+
+
+
+
+
+
+
+MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEB
+CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4
+MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFKs71ufbQwoQoW7qkNAJRIANGA4iM0
+ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyjxj0uJ4lArgkr4AOE
+jj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVNc1kl
+bN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF
+/cL5fOpdVa54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8n
+spXiH/MZW8o2cqWRkrw3MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0G
+A1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE4k2ZNTA0BgNVHREELTArggtzYW1sdGVz
+dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF
+AAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3YaMb2RSn
+7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHT
+TNiLArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nbl
+D1JJKSQ3AdhxK/weP3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcU
+ZOpx4swtgGdeoSpeRyrtMvRwdcciNBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu
+3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
+
+
+
+
+
+
+
+
+
+MIIDEjCCAfqgAwIBAgIVAPVbodo8Su7/BaHXUHykx0Pi5CFaMA0GCSqGSIb3DQEB
+CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4
+MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCQb+1a7uDdTTBBFfwOUun3IQ9nEuKM98SmJDWa
+MwM877elswKUTIBVh5gB2RIXAPZt7J/KGqypmgw9UNXFnoslpeZbA9fcAqqu28Z4
+sSb2YSajV1ZgEYPUKvXwQEmLWN6aDhkn8HnEZNrmeXihTFdyr7wjsLj0JpQ+VUlc
+4/J+hNuU7rGYZ1rKY8AA34qDVd4DiJ+DXW2PESfOu8lJSOteEaNtbmnvH8KlwkDs
+1NvPTsI0W/m4SK0UdXo6LLaV8saIpJfnkVC/FwpBolBrRC/Em64UlBsRZm2T89ca
+uzDee2yPUvbBd5kLErw+sC7i4xXa2rGmsQLYcBPhsRwnmBmlAgMBAAGjVzBVMB0G
+A1UdDgQWBBRZ3exEu6rCwRe5C7f5QrPcAKRPUjA0BgNVHREELTArggtzYW1sdGVz
+dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF
+AAOCAQEABZDFRNtcbvIRmblnZItoWCFhVUlq81ceSQddLYs8DqK340//hWNAbYdj
+WcP85HhIZnrw6NGCO4bUipxZXhiqTA/A9d1BUll0vYB8qckYDEdPDduYCOYemKkD
+dmnHMQWs9Y6zWiYuNKEJ9mf3+1N8knN/PK0TYVjVjXAf2CnOETDbLtlj6Nqb8La3
+sQkYmU+aUdopbjd5JFFwbZRaj6KiHXHtnIRgu8sUXNPrgipUgZUOVhP0C0N5OfE4
+JW8ZBrKgQC/6vJ2rSa9TlzI6JAa5Ww7gMXMP9M+cJUNQklcq+SBnTK8G+uBHgPKR
+zBDsMIEzRtQZm4GIoHJae4zmnCekkQ==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+ hwSpMetadata = `
+
+
+
+
+
+
+
+
+
+
+diBMwyuN633Q/kBf0M+SQZ4fNCI=
+
+
+
+k6+6QX8T5+S5wEIWlxwvd5a48xLWmp4Jd0bcs33vNaQVQ93YiztGcroUUqnLYK8uqAsmk2ZP4NtKFL/MyqoznrStglSi7uycwX6X3fswMwvkHJ9UVhH6Gp2Txl2JQ9/0vXy/tlXjQNFbfqzLhqxKC/K/PmKd684XkPYXlkiEJa6nGibu0gse9rvP2hE0G9bDMfdiyQnanfBvN+oNRte3xyI3DWh2P/jDTAPJMYzGM76JIneS8jLPa4gKDz5KcumG/8JV7GUWTDTZZ4ftujsjEuUFdPzKqwMYogMXOnTt8wbisSF8ZlLSMH/TIj6xAO9aSkEAo8HtlHJbekYlVA5Tz5IamLLzfaPpf7+NghLSuATIwY8/pvBZ8qhY8PVOjzRCeoEZJUrlFOcZ2CvO9zVKYdkEa1JC3mUW7CgeQc7G2/9niub7Vu00eyp9AAd9nkPfLoiWam8/yg2TBPRRJ9VKsv2UMFuITWrcFJayjezlTzY3dI3tI8lfoIMEVRaEP1v1D8XbxnxiaiKZXsGQHtpTSLc1ZL441jeZDLa661raUJDlGA6mBc1QukJklEocGg+Q+FeU33MJCaN/rIZCGvrjIZNng3yKFw1+R7/CqeJJlFWw0hPJQGiy6wfrDYSmhwuXJ2vQn3dTHjlT3kWsniiZtuOUi2TjHbq8gVHqlxmPxSs=
+
+
+
+
+MIIF6TCCA9GgAwIBAgIEPHSCijANBgkqhkiG9w0BAQsFADCBpDELMAkGA1UEBhMCQ04xEjAQBgNV BAgTCUd1YW5nRG9uZzERMA8GA1UEBxMIU2hlblpoZW4xJTAjBgNVBAoTHEh1YXdlaSBUZWNobm9s b2dpZXMgQ28uLCBMdGQxKDAmBgNVBAsTH1NlcnZpY2UgUHJvdmlkZXIgT3BlcmF0aW9uIERlcHQx HTAbBgNVBAMTFGF1dGguaHVhd2VpY2xvdWQuY29tMB4XDTE4MDYyMTEzMjUwMFoXDTI4MDYxODEz MjUwMFowgaQxCzAJBgNVBAYTAkNOMRIwEAYDVQQIEwlHdWFuZ0RvbmcxETAPBgNVBAcTCFNoZW5a aGVuMSUwIwYDVQQKExxIdWF3ZWkgVGVjaG5vbG9naWVzIENvLiwgTHRkMSgwJgYDVQQLEx9TZXJ2 aWNlIFByb3ZpZGVyIE9wZXJhdGlvbiBEZXB0MR0wGwYDVQQDExRhdXRoLmh1YXdlaWNsb3VkLmNv bTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJisHObeLOPs2mJ4eJBcruMZl9FjvRQe DmofxgOcIcmybt6qlDqAv7275JMMQfQFcEoxH3GqcCTYqvSaSnHPaJB1xljPIKAWtd7p1ymevcSy F2HdCZ8gPJKg3Q6+ZjwTipKS/nZr7xmpUQ0WvwRkgZ7zfslAW+y5PCgkDrgmEjG92rLArj8iPhmu jajXaTPQKVHuZMxBzS735uo4yjVwYE3+0mE4HTZjK+6n6Ffu+JhLzhcKGulmrT/6qHisMbIXAZyE egBDavomb+5zu/CUQwii5IAPrRTwwegYpG4+uYJ2cHfrUdHqw9lSCSbQzu1yW1AS4zB16sjoHZdV rxYyktlswNmJ1/MyRH5bO90e2kvVwV4l34Hi5HEFvFFjL8TAsbN4mGvA9fgohXp30x97UdPVV8Ji NNAKZjdZSEdG9xqrTRfe4+LQg/hzLNSwsko3nDnH8qhCgtb8qIipQ3s7niCa53AQWYR82lEViols /dbWU9qYeldVvGNAgJSqHLB7qLwcQW78+2V1446KhQqzqPeLI4ANGaLFKw8fGzgh85RKOjrIetb4 wAOZmhrrUJrRg47DYQQjNv3glDg53ijLPFunzRqUoqLphrZ1XpEA4y21OtTP6OMYAM0lSOj1gjvb ubTDo8XOQs5YGtTOyHn4CQ4GR8NNo7UrwVEmoZHA+AKbAgMBAAGjITAfMB0GA1UdDgQWBBTt05QW 9dyXi7eMekKOH0bn4xKrkDANBgkqhkiG9w0BAQsFAAOCAgEACv9zgzUgxxQ8t9ldOXmirxzSOrHx MCL8SKsu+c+Y4hoHma5LFjylv6x76NWTAFSE6GgqfuNI/gPj/2AWqObAvsHd8lsPjJ96ZoSaTmS8 NrtU6HuT1Lc+CmVfeGd3/G+KspQECjg2JeBrfyEw9B8KUAQV20DQukGfAHtKKQPOZmKm0Qm3ExWC eXz1TR2KP+Lrhny/yG43g4iVUKq65HFHs5cRzRk0iR0/NLpggl5+Op0rxMxBn+bCrnJBi0n9/PIM fWNhkEBl+B++EifPUQxQaOEsnxTgFo1O4ksK9hDFcLbr+1qCDgVIMkyC1xMBBikCgLvIdzy3SXBN ndIEUq+QgOORxLlq1WqrfLFO22TxZm8XaB5g36UMk5PGoVHGnjALReAHjC0C5sIiKMJSgQOPd71X mQSsw3G9NsMKf/H3xJXkq/b672ls/l1JBslm52DAk2k5UlLkf/1p4I7WHOfm5ZNpDjj1rTP6SiAc tWLtqXIU28fLa2sA+zHXA5acDGOm6eIrMme5HpsV/KoUOW1MXGugK59zofeueCFDGRfbyoS2lj0S W+CbJVa72CLf3xPh2nWH0cK9de+wyCx8uI0KGPyV4I9/XBLHhvkb3XPaUfnkzYkcrG/39cOaxuPF z+haXwI1lvI964zvmTgwdDjdf/0asA09S7EEK2KyzXUREM4=
+
+
+
+
+
+
+
+
+
+MIIF6TCCA9GgAwIBAgIEPHSCijANBgkqhkiG9w0BAQsFADCBpDELMAkGA1UEBhMCQ04xEjAQBgNV BAgTCUd1YW5nRG9uZzERMA8GA1UEBxMIU2hlblpoZW4xJTAjBgNVBAoTHEh1YXdlaSBUZWNobm9s b2dpZXMgQ28uLCBMdGQxKDAmBgNVBAsTH1NlcnZpY2UgUHJvdmlkZXIgT3BlcmF0aW9uIERlcHQx HTAbBgNVBAMTFGF1dGguaHVhd2VpY2xvdWQuY29tMB4XDTE4MDYyMTEzMjUwMFoXDTI4MDYxODEz MjUwMFowgaQxCzAJBgNVBAYTAkNOMRIwEAYDVQQIEwlHdWFuZ0RvbmcxETAPBgNVBAcTCFNoZW5a aGVuMSUwIwYDVQQKExxIdWF3ZWkgVGVjaG5vbG9naWVzIENvLiwgTHRkMSgwJgYDVQQLEx9TZXJ2 aWNlIFByb3ZpZGVyIE9wZXJhdGlvbiBEZXB0MR0wGwYDVQQDExRhdXRoLmh1YXdlaWNsb3VkLmNv bTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJisHObeLOPs2mJ4eJBcruMZl9FjvRQe DmofxgOcIcmybt6qlDqAv7275JMMQfQFcEoxH3GqcCTYqvSaSnHPaJB1xljPIKAWtd7p1ymevcSy F2HdCZ8gPJKg3Q6+ZjwTipKS/nZr7xmpUQ0WvwRkgZ7zfslAW+y5PCgkDrgmEjG92rLArj8iPhmu jajXaTPQKVHuZMxBzS735uo4yjVwYE3+0mE4HTZjK+6n6Ffu+JhLzhcKGulmrT/6qHisMbIXAZyE egBDavomb+5zu/CUQwii5IAPrRTwwegYpG4+uYJ2cHfrUdHqw9lSCSbQzu1yW1AS4zB16sjoHZdV rxYyktlswNmJ1/MyRH5bO90e2kvVwV4l34Hi5HEFvFFjL8TAsbN4mGvA9fgohXp30x97UdPVV8Ji NNAKZjdZSEdG9xqrTRfe4+LQg/hzLNSwsko3nDnH8qhCgtb8qIipQ3s7niCa53AQWYR82lEViols /dbWU9qYeldVvGNAgJSqHLB7qLwcQW78+2V1446KhQqzqPeLI4ANGaLFKw8fGzgh85RKOjrIetb4 wAOZmhrrUJrRg47DYQQjNv3glDg53ijLPFunzRqUoqLphrZ1XpEA4y21OtTP6OMYAM0lSOj1gjvb ubTDo8XOQs5YGtTOyHn4CQ4GR8NNo7UrwVEmoZHA+AKbAgMBAAGjITAfMB0GA1UdDgQWBBTt05QW 9dyXi7eMekKOH0bn4xKrkDANBgkqhkiG9w0BAQsFAAOCAgEACv9zgzUgxxQ8t9ldOXmirxzSOrHx MCL8SKsu+c+Y4hoHma5LFjylv6x76NWTAFSE6GgqfuNI/gPj/2AWqObAvsHd8lsPjJ96ZoSaTmS8 NrtU6HuT1Lc+CmVfeGd3/G+KspQECjg2JeBrfyEw9B8KUAQV20DQukGfAHtKKQPOZmKm0Qm3ExWC eXz1TR2KP+Lrhny/yG43g4iVUKq65HFHs5cRzRk0iR0/NLpggl5+Op0rxMxBn+bCrnJBi0n9/PIM fWNhkEBl+B++EifPUQxQaOEsnxTgFo1O4ksK9hDFcLbr+1qCDgVIMkyC1xMBBikCgLvIdzy3SXBN ndIEUq+QgOORxLlq1WqrfLFO22TxZm8XaB5g36UMk5PGoVHGnjALReAHjC0C5sIiKMJSgQOPd71X mQSsw3G9NsMKf/H3xJXkq/b672ls/l1JBslm52DAk2k5UlLkf/1p4I7WHOfm5ZNpDjj1rTP6SiAc tWLtqXIU28fLa2sA+zHXA5acDGOm6eIrMme5HpsV/KoUOW1MXGugK59zofeueCFDGRfbyoS2lj0S W+CbJVa72CLf3xPh2nWH0cK9de+wyCx8uI0KGPyV4I9/XBLHhvkb3XPaUfnkzYkcrG/39cOaxuPF z+haXwI1lvI964zvmTgwdDjdf/0asA09S7EEK2KyzXUREM4=
+
+
+
+
+
+
+
+
+MIIF6TCCA9GgAwIBAgIEPHSCijANBgkqhkiG9w0BAQsFADCBpDELMAkGA1UEBhMCQ04xEjAQBgNV BAgTCUd1YW5nRG9uZzERMA8GA1UEBxMIU2hlblpoZW4xJTAjBgNVBAoTHEh1YXdlaSBUZWNobm9s b2dpZXMgQ28uLCBMdGQxKDAmBgNVBAsTH1NlcnZpY2UgUHJvdmlkZXIgT3BlcmF0aW9uIERlcHQx HTAbBgNVBAMTFGF1dGguaHVhd2VpY2xvdWQuY29tMB4XDTE4MDYyMTEzMjUwMFoXDTI4MDYxODEz MjUwMFowgaQxCzAJBgNVBAYTAkNOMRIwEAYDVQQIEwlHdWFuZ0RvbmcxETAPBgNVBAcTCFNoZW5a aGVuMSUwIwYDVQQKExxIdWF3ZWkgVGVjaG5vbG9naWVzIENvLiwgTHRkMSgwJgYDVQQLEx9TZXJ2 aWNlIFByb3ZpZGVyIE9wZXJhdGlvbiBEZXB0MR0wGwYDVQQDExRhdXRoLmh1YXdlaWNsb3VkLmNv bTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJisHObeLOPs2mJ4eJBcruMZl9FjvRQe DmofxgOcIcmybt6qlDqAv7275JMMQfQFcEoxH3GqcCTYqvSaSnHPaJB1xljPIKAWtd7p1ymevcSy F2HdCZ8gPJKg3Q6+ZjwTipKS/nZr7xmpUQ0WvwRkgZ7zfslAW+y5PCgkDrgmEjG92rLArj8iPhmu jajXaTPQKVHuZMxBzS735uo4yjVwYE3+0mE4HTZjK+6n6Ffu+JhLzhcKGulmrT/6qHisMbIXAZyE egBDavomb+5zu/CUQwii5IAPrRTwwegYpG4+uYJ2cHfrUdHqw9lSCSbQzu1yW1AS4zB16sjoHZdV rxYyktlswNmJ1/MyRH5bO90e2kvVwV4l34Hi5HEFvFFjL8TAsbN4mGvA9fgohXp30x97UdPVV8Ji NNAKZjdZSEdG9xqrTRfe4+LQg/hzLNSwsko3nDnH8qhCgtb8qIipQ3s7niCa53AQWYR82lEViols /dbWU9qYeldVvGNAgJSqHLB7qLwcQW78+2V1446KhQqzqPeLI4ANGaLFKw8fGzgh85RKOjrIetb4 wAOZmhrrUJrRg47DYQQjNv3glDg53ijLPFunzRqUoqLphrZ1XpEA4y21OtTP6OMYAM0lSOj1gjvb ubTDo8XOQs5YGtTOyHn4CQ4GR8NNo7UrwVEmoZHA+AKbAgMBAAGjITAfMB0GA1UdDgQWBBTt05QW 9dyXi7eMekKOH0bn4xKrkDANBgkqhkiG9w0BAQsFAAOCAgEACv9zgzUgxxQ8t9ldOXmirxzSOrHx MCL8SKsu+c+Y4hoHma5LFjylv6x76NWTAFSE6GgqfuNI/gPj/2AWqObAvsHd8lsPjJ96ZoSaTmS8 NrtU6HuT1Lc+CmVfeGd3/G+KspQECjg2JeBrfyEw9B8KUAQV20DQukGfAHtKKQPOZmKm0Qm3ExWC eXz1TR2KP+Lrhny/yG43g4iVUKq65HFHs5cRzRk0iR0/NLpggl5+Op0rxMxBn+bCrnJBi0n9/PIM fWNhkEBl+B++EifPUQxQaOEsnxTgFo1O4ksK9hDFcLbr+1qCDgVIMkyC1xMBBikCgLvIdzy3SXBN ndIEUq+QgOORxLlq1WqrfLFO22TxZm8XaB5g36UMk5PGoVHGnjALReAHjC0C5sIiKMJSgQOPd71X mQSsw3G9NsMKf/H3xJXkq/b672ls/l1JBslm52DAk2k5UlLkf/1p4I7WHOfm5ZNpDjj1rTP6SiAc tWLtqXIU28fLa2sA+zHXA5acDGOm6eIrMme5HpsV/KoUOW1MXGugK59zofeueCFDGRfbyoS2lj0S W+CbJVa72CLf3xPh2nWH0cK9de+wyCx8uI0KGPyV4I9/XBLHhvkb3XPaUfnkzYkcrG/39cOaxuPF z+haXwI1lvI964zvmTgwdDjdf/0asA09S7EEK2KyzXUREM4=
+
+
+
+
+
+
+
+
+
+
+
+urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+`
+ hwIdpMetadata = `
+
+
+
+
+
+MIICsDCCAhmgAwIBAgIJAKNbH+B0Vm9HMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDMwMDIxMzA4WhcNMzMxMDMxMDIxMzA4WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDIIZtsLpqLDpXB1LI8tbtwoeOyJbM2PIxJTOqRm1ZM0r7rpvt4kFCgAd68gAsl
+YAEeSqUawxV3FUgt62DLMOT2auwBcpywVW7L/ZF4IUziwuFQLWdw5NIGMP5lpt1M
+HSel8k4paokoXAwZ2B+Vtku+kDTGLc3cp1T5/ClYE/ofdQIDAQABo4GnMIGkMB0G
+A1UdDgQWBBRVZlu4B6TzuNHasJz5tHoMilKLdjB1BgNVHSMEbjBsgBRVZlu4B6Tz
+uNHasJz5tHoMilKLdqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
+U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAKNbH+B0
+Vm9HMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAhyVdBqW4r94XdwMy
+LK42mwqNnHy4WjM8eq9X5FhBckZX+TyM909iH2AsMjpkv8BDIxTiX6tpmNyYhOCp
+vCPMmQHl9450maIA7At//sEgL94FNRJbTYkme7F3xI90X0htMr23Yan31lRwdj53
+DgagnkMlzQ8QccUXrdQgzXzKb0w=
+
+
+
+
+
+
+
+MIICsDCCAhmgAwIBAgIJAKNbH+B0Vm9HMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDMwMDIxMzA4WhcNMzMxMDMxMDIxMzA4WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDIIZtsLpqLDpXB1LI8tbtwoeOyJbM2PIxJTOqRm1ZM0r7rpvt4kFCgAd68gAsl
+YAEeSqUawxV3FUgt62DLMOT2auwBcpywVW7L/ZF4IUziwuFQLWdw5NIGMP5lpt1M
+HSel8k4paokoXAwZ2B+Vtku+kDTGLc3cp1T5/ClYE/ofdQIDAQABo4GnMIGkMB0G
+A1UdDgQWBBRVZlu4B6TzuNHasJz5tHoMilKLdjB1BgNVHSMEbjBsgBRVZlu4B6Tz
+uNHasJz5tHoMilKLdqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
+U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAKNbH+B0
+Vm9HMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAhyVdBqW4r94XdwMy
+LK42mwqNnHy4WjM8eq9X5FhBckZX+TyM909iH2AsMjpkv8BDIxTiX6tpmNyYhOCp
+vCPMmQHl9450maIA7At//sEgL94FNRJbTYkme7F3xI90X0htMr23Yan31lRwdj53
+DgagnkMlzQ8QccUXrdQgzXzKb0w=
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+`
+ aliyunSpMeta = `
+
+
+
+
+
+ MIIDXjCCAkagAwIBAgIEXHToLjANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJDTjERMA8GA1UE
+ CBMISGFuZ3pob3UxKTAnBgNVBAsTIEFsaWJhYmEgQ2xvdWQgQ29tcHV0aW5nIENvLiBMdGQuMSMw
+ IQYDVQQDExp1cm46YWxpYmFiYTpjbG91ZGNvbXB1dGluZzAgFw0xOTAyMjYwNzE4MDZaGA8yMTE5
+ MDIwMjA3MTgwNlowcDELMAkGA1UEBhMCQ04xETAPBgNVBAgTCEhhbmd6aG91MSkwJwYDVQQLEyBB
+ bGliYWJhIENsb3VkIENvbXB1dGluZyBDby4gTHRkLjEjMCEGA1UEAxMadXJuOmFsaWJhYmE6Y2xv
+ dWRjb21wdXRpbmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDjD53ZENEHoYNXAOf
+ OIbVBJhj7SCWKmdjnbnxq8WAFWEeZtS6hZLPpWh1z7b0NjJkvCf5oFVqBJYbbW5kEKV+9CpV6VHZ
+ qOXmsIRlkvZB+Wnc3SduwiiiUR9JojSPxVQvSf4WLT+HDASlrBztuRV2vHj9utLbvy+6bgVBqF8g
+ emL9Pcif1robDH8HlqUcADXLAt18E4MbToldVoHjpFc6fAKUXujWH5feAL8g0CKlmf/JVlHLEtu4
+ vKPxBQ8sgkysk6EnrjXl6Q4a4t+vbPG5uczA1ouTkDupMCRlaWHIHaJL/AoDGabn8sVXdaVJUKC5
+ 54FNkRznBhRQll+Nuc2rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAE8k4S4HvOglthJwF3aMQXGi
+ LKW6Becs9SljA0/5VtZQDrDf2By/1BIMvWfZ/dFnO+MylDLVdS6XWvWat/DW0fOGxU4s1WNfshX7
+ DJDGR2G1XgtGoDZEYIDahUp5katAPypCkY57fGZlI0d3nq46/2qT/Zpne+pFE3DI/x8klZMniniw
+ YjNXbG96y/M4DYi1J7RR8mLIfVvz5o1SMGT4Ta2p/USE2M9F6O7/zc2j62dQgXiYa9OONo31RiXR
+ TmvGEUNuQoBZhVrFIvOnNjIfFT7Xd3CUowwJKP1floserrx4B5jScRAi9yK3x2a2lhfc+PksXfTs
+ aqIr5TQL4OorLEE=
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
+ urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName
+ urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos
+ urn:oasis:names:tc:SAML:2.0:nameid-format:entity
+
+
+ Alibaba Cloud Console Single Sign-On
+
+
+
+
+
+
+ Alibaba Cloud Computing Co. Ltd.
+ AlibabaCloud
+ https://www.aliyun.com
+
+`
+
+ aliyunUserMeta = `
+MIIDUTCCAjmgAwIBAgIEIv2v9DANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJDTjERMA8GA1UE
+BxMISGFuZ3pob3UxFDASBgNVBAoTC0FsaWJhYmEgSW5jMQ8wDQYDVQQLEwZBcHNhcmExEDAOBgNV
+BAMTB0FsaWJhYmEwHhcNMTcwMzE0MTc1OTE5WhcNMjcwMzEyMTc1OTE5WjBZMQswCQYDVQQGEwJD
+TjERMA8GA1UEBxMISGFuZ3pob3UxFDASBgNVBAoTC0FsaWJhYmEgSW5jMQ8wDQYDVQQLEwZBcHNh
+cmExEDAOBgNVBAMTB0FsaWJhYmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqK2HR
+tf4smv9pCQtPenFE1w6lvxsHiv0J/knvpC1BU4iAWcS8LxAElKb49QbKHuUxcwEGJfm0+zZpqS+J
+I3jmGc4aHYACyL2WxtKNx/5EK1Qs5ugCipn7g+ySOqXxc/Rv2S7muw6LTrGVTT7vo09EUDkZM34s
+TupuU7tzX0ktYhimxwskG9o7bvZuQKQf66gN8l/DUzyUl59/0wA1+x5A5B3pvaABCA6dq4mi8mtJ
+fTXcqWm06+FgVNPgKo59uP6y08rQJXjKDwLIf0owuoiRrPLR5JKC1vQ6PSz0cGv8tGUts5dr/0zG
+FHy4h3aufQiXCSi44WUB3FejQQfgEiBdAgMBAAGjITAfMB0GA1UdDgQWBBShWN61nZsWz9MYnSrV
+kCkJnSdFtDANBgkqhkiG9w0BAQsFAAOCAQEAMMAl+C3oyI6kZNmvX05Sb0q6UAM8wqjFKbPhSSiy
+srjVZwjEjiZnOSnoX8vO07fsZpcVmByHzGXWuBxxKCviCpQCS9hyOTF6bvAoXwe37h02Uhv3tKI0
+7FRkXJA7HeB0HEuHPCBxxWVWJfgtkeUETnGV06CrUlGON7Du3h37EUzfTqmKhlsqKeK8uqw3gLYq
+Bp6ULrP1PbNo2AaHMYaZhFL1dSUtNYvekZppregZKMIDqtEm6Pwpw2lj8gjTC40PQ0GuXEeTsfE5
+dhw42xc9RkyUg1Go04k9Z/UMxTX0KVMiRZ9DF2FWjWp1AAQJ3TvZ2Ao/XOhmk4GWRehUoHr7Hw==urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddressurn:oasis:names:tc:SAML:2.0:nameid-format:transienturn:oasis:names:tc:SAML:2.0:nameid-format:persistenturn:oasis:names:tc:SAML:1.1:nameid-format:unspecified`
+)
+
+func TestParseMetadata(t *testing.T) {
+ cases := []struct {
+ In string
+ }{
+ {
+ In: spMetadata1,
+ },
+ {
+ In: idpMetadata,
+ },
+ {
+ In: hwSpMetadata,
+ },
+ {
+ In: hwIdpMetadata,
+ },
+ {
+ In: aliyunSpMeta,
+ },
+ {
+ In: aliyunUserMeta,
+ },
+ }
+ for _, c := range cases {
+ ed, err := ParseMetadata([]byte(c.In))
+ if err != nil {
+ t.Errorf("ParseMetadata fail %s", err)
+ } else {
+ xmlstr, err := xml.MarshalIndent(ed, "", " ")
+ if err != nil {
+ t.Errorf("xml.Marshal fail %s", err)
+ } else {
+ t.Logf("%s", xmlstr)
+ }
+ }
+ }
+}
+
+var (
+ resp1 = `
+
+
+
+
+ https://www.test.com
+
+
+ urn:oasis:names:tc:SAML:2.0:status:Success
+
+
+
+ https://www.test.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rFxrycznfGNYOnprZIFJJou4ro0Mz65+43MIR5F0+H4=
+
+
+
+
+ YqTWQngAPfGqQmWa610PM7LeefqWdKuveUVINrqL67NoHJIDa2WxLwdVzoJIlJh64QiNPr6+ndmL DCMgIC5F/9ijuzhIICZcc6lHNIjy6EsPkKRjfo9oeoVAqLgG/kmVQYeHLBID0y11RNXXpAVY4nhJ 26KiIVGt7ywyKAmhichE+eW/UYAGiOI5vkfgD2gZUGV+yPkv64k7xK4yAH3mL2NaCPuw/90e4enm iUx0YuazDwM5FiRUSMpcJs0rcNmS6clWAUcCzbOx+y2vJGtTjHb7k3UsmpnTop5eYNp94+sDPEat 8FaV4SgafMEL5z54gpe8+//9yOWEvlBs1b0RYg==
+
+
+
+
+
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhK3L160NjP9EhBGQOC2s4r+Wc62bkRkc nUxfhiZwCwJdQCykzuLOAoATnfoEamV5W25xtSS5kFs+4OC0mYVpKcI3SWoydX+UE5Qik5UfJ8Dt G1AvSEKhSluyO9axrV5Uv089jMxBnlm/R+xND73WcZM11yIbKJEZSTCEDfh+KnFbMw108umFMden RZCrNWUJoSp/90XeG0V2Nmj7Fkq72skSifwIASLRq9KqLbmh1QwUX+AoWpHK/jRUBustMBmG1n1i
+ AqpD4EBjjBOB27k1wXZ30+IoJt8IZmfSZRFoNn5VFWXNeEmZ1aQvGSvd3Tyyw2/Wr+w/8Mags69C mpeX6QIDAQAB
+
+
+
+
+
+
+ Some NameID value
+
+
+
+
+
+
+
+
+ https://auth.huaweicloud.com/
+
+
+
+
+
+ *******
+
+
+
+ ********
+
+
+ ******
+
+
+
+
+
+ ******
+
+
+ *****
+
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
+
+
+
+`
+ resp2 = `
+ https://idp.example.org/SAML2
+
+
+
+
+ https://idp.example.org/SAML2
+
+ ...
+
+
+ 3f7b3dcf-1674-4ecd-92c8-1544f346baf8
+
+
+
+
+
+
+
+ https://sp.example.com/SAML2
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
+
+
+
+
+ `
+)
+
+func TestParseResponse(t *testing.T) {
+ cases := []struct {
+ In string
+ }{
+ {
+ In: resp1,
+ },
+ {
+ In: resp2,
+ },
+ }
+ for _, c := range cases {
+ resp := Response{}
+ err := xml.Unmarshal([]byte(c.In), &resp)
+ if err != nil {
+ t.Errorf("xml.Unmarshal %s", err)
+ } else {
+ xmlstr, err := xml.MarshalIndent(resp, "", " ")
+ if err != nil {
+ t.Errorf("xml.Marshal fail %s", err)
+ } else {
+ t.Logf("%s", xmlstr)
+ }
+ }
+ }
+}
diff --git a/pkg/util/samlutils/response.go b/pkg/util/samlutils/response.go
new file mode 100644
index 0000000000..a7305b2c3d
--- /dev/null
+++ b/pkg/util/samlutils/response.go
@@ -0,0 +1,352 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import (
+ "encoding/xml"
+ "time"
+
+ "yunion.io/x/pkg/util/timeutils"
+ "yunion.io/x/pkg/utils"
+)
+
+type SSAMLResponseAttribute struct {
+ Name string
+ NameFormat string
+ FriendlyName string
+ Values []string
+}
+
+type SSAMLSpInitiatedLoginData struct {
+ NameId string
+ NameIdFormat string
+
+ AudienceRestriction string
+
+ Attributes []SSAMLResponseAttribute
+}
+
+type SSAMLIdpInitiatedLoginData struct {
+ SSAMLSpInitiatedLoginData
+
+ RelayState string
+}
+
+type SSAMLResponseInput struct {
+ IssuerEntityId string
+
+ RequestEntityId string
+ RequestID string
+
+ AssertionConsumerServiceURL string
+
+ IssuerCertString string
+
+ SSAMLSpInitiatedLoginData
+}
+
+func NewResponse(input SSAMLResponseInput) Response {
+ // since := timeutils.IsoTime(time.Now().UTC().Add(-time.Minute * 60 * 24))
+ until := timeutils.IsoTime(time.Now().UTC().Add(time.Minute * 60 * 24))
+
+ respId := "_" + utils.GenRequestId(16)
+ assertId := "_" + utils.GenRequestId(16)
+ now := timeutils.IsoTime(time.Now().UTC())
+ issuerFormat := NAME_ID_FORMAT_ENTITY
+
+ issuer := Issuer{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "Issuer",
+ },
+ Format: &issuerFormat,
+ Issuer: input.IssuerEntityId,
+ }
+
+ var responseTo *string
+ if len(input.RequestID) > 0 {
+ responseTo = &input.RequestID
+ }
+
+ resp := Response{
+ XMLName: xml.Name{
+ Space: XMLNS_PROTO,
+ Local: "Response",
+ },
+ ID: respId,
+ InResponseTo: responseTo,
+ Version: SAML2_VERSION,
+ IssueInstant: now,
+ Destination: input.AssertionConsumerServiceURL,
+ Issuer: issuer,
+ Status: Status{
+ XMLName: xml.Name{
+ Space: XMLNS_PROTO,
+ Local: "Status",
+ },
+ StatusCode: StatusCode{
+ XMLName: xml.Name{
+ Space: XMLNS_PROTO,
+ Local: "StatusCode",
+ },
+ Value: STATUS_SUCCESS,
+ },
+ StatusMessage: &StatusMessage{
+ XMLName: xml.Name{
+ Space: XMLNS_PROTO,
+ Local: "StatusMessage",
+ },
+ Message: STATUS_SUCCESS,
+ },
+ },
+ Assertion: Assertion{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "Assertion",
+ },
+ ID: assertId,
+ Version: SAML2_VERSION,
+ IssueInstant: now,
+ Issuer: issuer,
+ Signature: &Signature{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "Signature",
+ },
+ SignedInfo: SignedInfo{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "SignedInfo",
+ },
+ CanonicalizationMethod: EncryptionMethod{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "CanonicalizationMethod",
+ },
+ Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
+ },
+ SignatureMethod: EncryptionMethod{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "SignatureMethod",
+ },
+ Algorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ },
+ Reference: Reference{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "Reference",
+ },
+ URI: "#" + assertId,
+ Transforms: Transforms{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "Transforms",
+ },
+ Transforms: []EncryptionMethod{
+ {
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "Transform",
+ },
+ Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
+ },
+ {
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "Transform",
+ },
+ Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
+ },
+ },
+ },
+ DigestMethod: EncryptionMethod{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "DigestMethod",
+ },
+ Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ },
+ DigestValue: SSAMLValue{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "DigestValue",
+ },
+ Value: "",
+ },
+ },
+ },
+ SignatureValue: SSAMLValue{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "SignatureValue",
+ },
+ Value: "",
+ },
+ KeyInfo: KeyInfo{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "KeyInfo",
+ },
+ X509Data: X509Data{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "X509Data",
+ },
+ X509Certificate: X509Certificate{
+ XMLName: xml.Name{
+ Space: XMLNS_DS,
+ Local: "X509Certificate",
+ },
+ Cert: input.IssuerCertString,
+ },
+ },
+ },
+ },
+ Subject: Subject{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "Subject",
+ },
+ NameID: NameID{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "NameID",
+ },
+ Format: input.NameIdFormat,
+ NameQualifier: &input.RequestEntityId,
+ Value: input.NameId,
+ },
+ SubjectConfirmation: SubjectConfirmation{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "SubjectConfirmation",
+ },
+ Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
+ SubjectConfirmationData: SubjectConfirmationData{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "SubjectConfirmationData",
+ },
+ InResponseTo: responseTo,
+ Recipient: input.AssertionConsumerServiceURL,
+ NotOnOrAfter: until,
+ },
+ },
+ },
+ Conditions: Conditions{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "Conditions",
+ },
+ NotOnOrAfter: until,
+ AudienceRestrictions: []AudienceRestriction{},
+ },
+ AttributeStatement: &AttributeStatement{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "AttributeStatement",
+ },
+ Attributes: []Attribute{},
+ },
+ AuthnStatement: AuthnStatement{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "AuthnStatement",
+ },
+ AuthnInstant: now,
+ SessionIndex: assertId,
+ SubjectLocality: &SubjectLocality{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "SubjectLocality",
+ },
+ Address: input.RequestEntityId,
+ },
+ AuthnContext: AuthnContext{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "AuthnContext",
+ },
+ AuthnContextClassRef: AuthnContextClassRef{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "AuthnContextClassRef",
+ },
+ // Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
+ Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
+ },
+ },
+ },
+ },
+ }
+
+ if len(input.AudienceRestriction) > 0 {
+ resp.AddAudienceRestriction(input.AudienceRestriction)
+ }
+
+ for _, attr := range input.Attributes {
+ resp.AddAttribute(attr.Name, attr.FriendlyName, attr.NameFormat, attr.Values)
+ }
+
+ return resp
+}
+
+// AddAttribute add strong attribute to the Response
+func (r *Response) AddAttribute(name string, friendlyName string, nameFormat string, values []string) {
+ attr := Attribute{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "Attribute",
+ },
+ Name: name,
+ AttributeValues: []AttributeValue{},
+ }
+ if len(friendlyName) > 0 {
+ attr.FriendlyName = &friendlyName
+ }
+ if len(nameFormat) > 0 {
+ attr.NameFormat = &nameFormat
+ }
+ for _, value := range values {
+ attrValue := AttributeValue{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "AttributeValue",
+ },
+ Type: "xs:string",
+ Value: value,
+ }
+ attr.AttributeValues = append(attr.AttributeValues, attrValue)
+ }
+ r.Assertion.AttributeStatement.Attributes = append(r.Assertion.AttributeStatement.Attributes, attr)
+}
+
+func (r *Response) AddAudienceRestriction(value string) {
+ restrict := AudienceRestriction{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "AudienceRestriction",
+ },
+ Audience: Audience{
+ XMLName: xml.Name{
+ Space: XMLNS_ASSERT,
+ Local: "Audience",
+ },
+ Value: value,
+ },
+ }
+ r.Assertion.Conditions.AudienceRestrictions = append(r.Assertion.Conditions.AudienceRestrictions, restrict)
+}
diff --git a/pkg/util/samlutils/response_test.go b/pkg/util/samlutils/response_test.go
new file mode 100644
index 0000000000..c5c0435c86
--- /dev/null
+++ b/pkg/util/samlutils/response_test.go
@@ -0,0 +1,125 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import (
+ "encoding/xml"
+ "testing"
+)
+
+func TestNewResponse(t *testing.T) {
+ privateKeyString := `-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDKpaC0XoinzuQO
+bt/VsWap0OVqN8O3cBFS41Ss9GGbjn1ZIg+D14OxywZdrRA17MKxfjhO4F+d2n8H
+Ih5ZofGURKgOH8KrEIPcii5HLk1ZVnLXXbinExwG26OPAID9rYNSdBhVeR4pqHm7
+SBnBXV+s83eIekAuPE6lIOF4irA3kOXxKhInYAygnx2fNR6p98suNda+F1VTO8HV
+WoAvjJoQ0iAA6/eHmUv1gPtJ+odjJPH7m6FrF3voJIiwmgFua01qkn6tZH53hBc2
+Dkbt5PxqI1KC0C79wq1ufehkk9MR5NTMxIwxbc3PcTbmwduGelKOsloLK/tGu5g8
+Kq5UJE4ZAgMBAAECggEBAJmRtNybe1I4Hmm1qlkl7Fgqn4DEK8Sa3/YBowzC0ilx
+bRqcDkfqjbmx0uwwl8VV3CFoNsHHlY5po7RDLd7dM9cZxIWXmg3LITKDYRi+RQ27
+zqHZO3MZrzafQi6/wgD8ejWFF1/Gvo3xR/ceZ6461aOaie5aPsMLHspSxat05p/k
+C4NkBvjHdYt4PiH873PLOI6mpt8N5mnkYmer8S5XX02GyQI4OGnHuCqQsHtlhgj7
+XeSB2biDNrcYfL1Kp1E8vEGBYqhohVWVWo+rP5zyoWNxQUXOVDE5zQanLrktjno5
+uzHkK1zKGcOlh7s40pV3qgcQFRTWt4RRP5wBasKc9wECgYEA948ikAVBiAiEPDha
+hlNboaUKYeB6HxKSUZhqaGY0+7OWSyBhQBALCZRk4ccscysj4VUur+6FwC7lleRj
+tG+S7ukyiQEeH0c0NAfnIlSd+LFQREsj4MVLn+Sl/8edNIl/UH+69pDtVU6Az/2u
+DF0FloGL5XZ0WLIVcRZdUjt8HfkCgYEA0Y54DPN5PcLKci0Plioc0qLb33kp6F+w
+JJhOLJv+tbmeNVA0EM+BwTHlHF8MRsreybwSHoUYX61vkR2AlGLo8D5TlTEA1VSD
+k4w5Yic3cjQzOFMWQczpGdBwV37inHOU2phOp2kKs8LyT/gJw5f6b34R7zfRc8ry
+uAEXaTg4OSECgYBb37ESBgFV/OMmfjuKUnFVQiziOi7YTUokIg6LhDLxnqqOYwv0
+fH+8JGh0Kjji3QXJ4JUdEcZtlnn58PLXyfib1cu9cL6/GOvUy4IKCaE+5H9HeSNt
+jYsNYgwBKxG6p7SqKV03mH2cBTBlAF6RlAw42QcUN6viJuUyPPyRQiZD8QKBgQCO
+N28X8wC8Pn9gH16tnaTz+pzXvAYJ8y66lzaupaumLvPE4MqFAh7gO3lu2L6fKL0s
+EdwGJHOXM0A9LtV9XucRbGsTHC+hl/q33vluuIizk+OS/ShkvakQ4NntN2qZnQNP
+mv/+M5aUyt/iD8aonHLUya1oOOyH9hrlb7Aws3vMoQKBgQDNH4xxS7wxnf7GcTnt
+96tHvjBVHXa0SK1XkbFjgIEDU+xeEUteYs/9jW4AMA/lqsfBXUwhF3JAVoRUWm0M
+d95jkAi6q2icuGogkftyv+LgUvhSUm6N+8eQ+YvQfgsNBqxlSudjpP0V+S2v73vp
+8egFWKkL+H0upowTbr0T0OUfog==
+-----END PRIVATE KEY-----
+`
+ certString := `MIIE/DCCA+SgAwIBAgIQQHdQar3faUJLgHcCgO4z3zANBgkqhkiG9w0BAQsFADCB
+nzELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxETAPBgNVBAcMCENoYW95
+YW5nMSMwIQYDVQQKDBpZdW5pb24gVGVjaG5vbG9neSBDby4gTHRkLjERMA8GA1UE
+CwwIT25lQ2xvdWQxFDASBgNVBAMMC0AxNTkxNzIxNDc5MR0wGwYJKoZIhvcNAQkB
+Fg5pbmZvQHl1bmlvbi5jbjAeFw0yMDA2MDkxNjUxMjBaFw0yMjA4MTgxNjUxMjBa
+MIGXMQswCQYDVQQGEwJDTjEQMA4GA1UECAwHQmVpamluZzERMA8GA1UEBwwIQ2hh
+b3lhbmcxIzAhBgNVBAoMGll1bmlvbiBUZWNobm9sb2d5IENvLiBMdGQuMREwDwYD
+VQQLDAhPbmVDbG91ZDEMMAoGA1UEAwwDaWRwMR0wGwYJKoZIhvcNAQkBFg5pbmZv
+QHl1bmlvbi5jbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqloLRe
+iKfO5A5u39WxZqnQ5Wo3w7dwEVLjVKz0YZuOfVkiD4PXg7HLBl2tEDXswrF+OE7g
+X53afwciHlmh8ZREqA4fwqsQg9yKLkcuTVlWctdduKcTHAbbo48AgP2tg1J0GFV5
+HimoebtIGcFdX6zzd4h6QC48TqUg4XiKsDeQ5fEqEidgDKCfHZ81Hqn3yy411r4X
+VVM7wdVagC+MmhDSIADr94eZS/WA+0n6h2Mk8fuboWsXe+gkiLCaAW5rTWqSfq1k
+fneEFzYORu3k/GojUoLQLv3CrW596GST0xHk1MzEjDFtzc9xNubB24Z6Uo6yWgsr
++0a7mDwqrlQkThkCAwEAAaOCATgwggE0MAkGA1UdEwQCMAAwHQYDVR0OBBYEFFie
+B9nsyKQ0A+oUWqiJkkURLJMqMIHUBgNVHSMEgcwwgcmAFISskqtTpDD4otifwvuG
+bFwyvZ0zoYGlpIGiMIGfMQswCQYDVQQGEwJDTjEQMA4GA1UECAwHQmVpamluZzER
+MA8GA1UEBwwIQ2hhb3lhbmcxIzAhBgNVBAoMGll1bmlvbiBUZWNobm9sb2d5IENv
+LiBMdGQuMREwDwYDVQQLDAhPbmVDbG91ZDEUMBIGA1UEAwwLQDE1OTE3MjE0Nzkx
+HTAbBgkqhkiG9w0BCQEWDmluZm9AeXVuaW9uLmNuggkA7uFZmhG6rz0wEwYDVR0l
+BAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMA8GA1UdEQQIMAaHBH8AAAEwDQYJ
+KoZIhvcNAQELBQADggEBAKc7gUyrAMto3O8/Qi/m23aHxYc33GVMRrDPNS7asSWO
+l+3IiWc2L61OiWhmK75t1leMRZU5hEuxAb3Rq6TwHVQWD3mAuIrss+Dsfhs0Y/wa
+miIYV+v4rXuu3CusBxQo8NRuNW+f/F5cR0WVBDDMm5NDQKPVdeQ5uszqtWN62tPq
+zvi1nRqaRKZQbzENGmqLwfMQCm0fsUzw6mdZb137S04MuSnvb1hTyhQ492uqoTlE
+KM6qMM9M9PWLW8PiZxiHxtLdO5BIXOXY//xU9xD58sOIAePuc322+TbMBtRZAyqg
+OZ+tB0Nzk2RJp4luPEqxoBcDVpcuiyfu3s3Jtnr3Lns=`
+
+ input := SSAMLResponseInput{
+ NameId: "testUser",
+ NameIdFormat: NAME_ID_FORMAT_TRANSIENT,
+ RequestID: "_dck4mm08qmdhc8k4nuir07hghetdqqg8umg5",
+ RequestEntityId: "https://auth.huaweicloud.com/",
+ IssuerEntityId: "https://saml.yunion.io/",
+ IssuerCertString: certString,
+ AssertionConsumerServiceURL: "https://auth.huaweicloud.com/authui/saml/SAMLAssertionConsumer",
+ }
+ resp := NewResponse(input)
+ resp.AddAudienceRestriction(input.RequestEntityId)
+ for k, v := range map[string]string{
+ "xUserId": "userXXXX",
+ "xAccountId": "accountXXXX",
+ "bpId": "bpXXXXX",
+ "email": "xxxx@yunion.io",
+ "name": "xxxx",
+ "mobile": "13812341234",
+ } {
+ resp.AddAttribute(k, k, "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", []string{v})
+ }
+
+ respXml, err := xml.MarshalIndent(resp, "", " ")
+ if err != nil {
+ t.Fatalf("xml.MarshalIndent fail %s", err)
+ }
+
+ privateKey, err := decodePrivateKey([]byte(privateKeyString))
+ if err != nil {
+ t.Fatalf("decodePrivateKey fail %s", err)
+ }
+
+ signed, err := SignXML(string(respXml), privateKey)
+ if err != nil {
+ t.Fatalf("SignXML fail %s", err)
+ }
+
+ t.Logf("signed XML: %s", signed)
+
+ validXMLs, err := ValidateXML(signed)
+ if err != nil {
+ t.Fatalf("ValidateXML fail %s", err)
+ }
+
+ t.Logf("validated xml: %s", validXMLs)
+}
diff --git a/pkg/util/samlutils/types.go b/pkg/util/samlutils/types.go
new file mode 100644
index 0000000000..9d05bc9eeb
--- /dev/null
+++ b/pkg/util/samlutils/types.go
@@ -0,0 +1,439 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import "encoding/xml"
+
+type DigestMethod struct {
+ XMLName xml.Name
+
+ Algorithm string `xml:"Algorithm,attr"`
+}
+
+type SigningMethod struct {
+ XMLName xml.Name
+
+ Algorithm string `xml:"Algorithm,attr"`
+}
+
+type RequestInitiator struct {
+ XMLName xml.Name
+
+ Binding string `xml:"Binding,attr"`
+ Location string `xml:"Location,attr"`
+}
+
+type SXMLText struct {
+ XMLName xml.Name
+
+ Lang string `xml:"lang,attr"`
+ Text string `xml:",innerxml"`
+}
+
+type SXMLLogo struct {
+ XMLName xml.Name
+
+ Height string `xml:"height,attr"`
+ Width string `xml:"width,attr"`
+ URL string `xml:",innerxml"`
+}
+
+type SSAMLUIInfo struct {
+ XMLName xml.Name
+
+ DisplayName SXMLText `xml:"DisplayName"`
+ Description SXMLText `xml:"Description"`
+ Logo SXMLLogo `xml:"Logo"`
+}
+
+type SSAMLScope struct {
+ XMLName xml.Name
+
+ Regexp string `xml:"regexp,attr"`
+ Scope string `xml:",innerxml"`
+}
+
+type Extensions struct {
+ XMLName xml.Name
+
+ // Alg string `xml:"alg,attr"`
+ // MDAttr string `xml:"mdattr,attr"`
+ // MDRPI string `xml:"mdrpi,attr"`
+
+ // EntityAttributes string `xml:"EntityAttributes"`
+
+ SigningMethods []SigningMethod `xml:"SigningMethod"`
+ DigestMethods []DigestMethod `xml:"DigestMethod"`
+
+ RequestInitiator *RequestInitiator `xml:"RequestInitiator"`
+
+ UIInfo *SSAMLUIInfo `xml:"UIInfo"`
+
+ Scope *SSAMLScope `xml:"Scope"`
+}
+
+type X509Certificate struct {
+ XMLName xml.Name
+
+ Cert string `xml:",innerxml"`
+}
+
+type X509Data struct {
+ XMLName xml.Name
+
+ X509Certificate X509Certificate `xml:"X509Certificate"`
+}
+
+type KeyInfo struct {
+ XMLName xml.Name
+
+ X509Data X509Data `xml:"X509Data"`
+}
+
+type EncryptionMethod struct {
+ XMLName xml.Name
+
+ Algorithm string `xml:"Algorithm,attr"`
+}
+
+type KeyDescriptor struct {
+ XMLName xml.Name
+
+ Use string `xml:"use,attr"`
+
+ KeyInfo KeyInfo `xml:"KeyInfo"`
+
+ EncryptionMethods []EncryptionMethod `xml:"EncryptionMethod"`
+}
+
+type SSAMLService struct {
+ XMLName xml.Name
+
+ Binding string `xml:"Binding,attr"`
+ Location string `xml:"Location,attr"`
+ Index *string `xml:"index,attr"`
+ IsDefault *string `xml:"isDefault,attr"`
+}
+
+type SSAMLNameIDFormat struct {
+ XMLName xml.Name
+
+ Format string `xml:",innerxml"`
+}
+
+type RequestedAttribute struct {
+ XMLName xml.Name
+
+ IsRequired string `xml:"isRequired,attr"`
+ Name string `xml:"Name,attr"`
+ FriendlyName string `xml:"FriendlyName,attr"`
+}
+
+type AttributeConsumingService struct {
+ XMLName xml.Name
+
+ Index string `xml:"index,attr"`
+
+ ServiceName SXMLText `xml:"ServiceName"`
+
+ RequestedAttribute []RequestedAttribute `xml:"RequestedAttribute"`
+}
+
+type SSODescriptor struct {
+ XMLName xml.Name
+
+ AuthnRequestsSigned *string `xml:"AuthnRequestsSigned,attr"`
+ WantAssertionsSigned *string `xml:"WantAssertionsSigned,attr"`
+ ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
+
+ Extensions *Extensions `xml:"Extensions"`
+
+ KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"`
+
+ ArtifactResolutionServices []SSAMLService `xml:"ArtifactResolutionService"`
+
+ SingleLogoutServices []SSAMLService `xml:"SingleLogoutService"`
+ ManageNameIDServices []SSAMLService `xml:"ManageNameIDService"`
+
+ NameIDFormat []SSAMLNameIDFormat `xml:"NameIDFormat"`
+ SingleSignOnServices []SSAMLService `xml:"SingleSignOnService"`
+
+ AssertionConsumerServices []SSAMLService `xml:"AssertionConsumerService"`
+
+ AttributeConsumingService []AttributeConsumingService `xml:"AttributeConsumingService"`
+}
+
+type SSAMLValue struct {
+ XMLName xml.Name
+
+ Value string `xml:",innerxml"`
+}
+
+type Transforms struct {
+ XMLName xml.Name
+
+ Transforms []EncryptionMethod `xml:"Transform"`
+}
+
+type Reference struct {
+ XMLName xml.Name
+
+ URI string `xml:"URI,attr"`
+
+ Transforms Transforms `xml:"Transforms"`
+ DigestMethod EncryptionMethod `xml:"DigestMethod"`
+ DigestValue SSAMLValue `xml:"DigestValue"`
+}
+
+type SignedInfo struct {
+ XMLName xml.Name
+
+ CanonicalizationMethod EncryptionMethod `xml:"CanonicalizationMethod"`
+ SignatureMethod EncryptionMethod `xml:"SignatureMethod"`
+
+ Reference Reference `xml:"Reference"`
+}
+
+type Signature struct {
+ XMLName xml.Name
+
+ SignedInfo SignedInfo `xml:"SignedInfo"`
+ SignatureValue SSAMLValue `xml:"SignatureValue"`
+ KeyInfo KeyInfo `xml:"KeyInfo"`
+}
+
+type Organization struct {
+ XMLName xml.Name
+
+ OrganizationName SXMLText `xml:"OrganizationName"`
+ OrganizationDisplayName SXMLText `xml:"OrganizationDisplayName"`
+ OrganizationURL SXMLText `xml:"OrganizationURL"`
+}
+
+type EntityDescriptor struct {
+ XMLName xml.Name
+
+ // Id *string `xml:"ID,attr"`
+ EntityId string `xml:"entityID,attr"`
+
+ Extensions *Extensions `xml:"Extensions"`
+ Signature *Signature `xml:"Signature"`
+
+ SPSSODescriptor *SSODescriptor `xml:"SPSSODescriptor"`
+ IDPSSODescriptor *SSODescriptor `xml:"IDPSSODescriptor"`
+
+ Organization *Organization `xml:"Organization"`
+}
+
+type SIdpRedirectLoginInput struct {
+ SAMLRequest string `json:"SAMLRequest"`
+ RelayState string `json:"RelayState"`
+ SigAlg string `json:"SigAlg"`
+ Signature string `json:"Signature"`
+}
+
+type SIdpInitiatedLoginInput struct {
+ EntityID string `json:"EntityID"`
+ State string `json:"State"`
+}
+
+type Issuer struct {
+ XMLName xml.Name
+
+ Format *string `xml:"Format,attr"`
+
+ Issuer string `xml:",innerxml"`
+}
+
+type NameIDPolicy struct {
+ XMLName xml.Name
+
+ AllowCreate string `xml:"AllowCreate,attr"`
+ Format string `xml:"Format,attr"`
+ SPNameQualifier string `xml:"SPNameQualifier,attr"`
+}
+
+type AuthnRequest struct {
+ XMLName xml.Name
+
+ AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"`
+ Destination string `xml:"Destination,attr"`
+ ForceAuthn string `xml:"ForceAuthn,attr"`
+ ID string `xml:"ID,attr"`
+ IsPassive string `xml:"IsPassive,attr"`
+ IssueInstant string `xml:"IssueInstant,attr"`
+ ProtocolBinding string `xml:"ProtocolBinding,attr"`
+ Version string `xml:"Version,attr"`
+
+ Issuer Issuer `xml:"Issuer"`
+ NameIDPolicy NameIDPolicy `xml:"NameIDPolicy"`
+}
+
+type StatusCode struct {
+ XMLName xml.Name
+
+ Value string `xml:"Value,attr"`
+}
+
+type StatusMessage struct {
+ XMLName xml.Name
+
+ Message string `xml:",innerxml"`
+}
+
+type Status struct {
+ XMLName xml.Name
+
+ StatusCode StatusCode `xml:"StatusCode"`
+ StatusMessage *StatusMessage `xml:"StatusMessage"`
+}
+
+type Response struct {
+ XMLName xml.Name
+
+ ID string `xml:"ID,attr"`
+ InResponseTo *string `xml:"InResponseTo,attr"`
+ Version string `xml:"Version,attr"`
+ IssueInstant string `xml:"IssueInstant,attr"`
+ Destination string `xml:"Destination,attr"`
+
+ Issuer Issuer `xml:"Issuer"`
+ Status Status `xml:"Status"`
+
+ Assertion Assertion `xml:"Assertion"`
+}
+
+type Assertion struct {
+ XMLName xml.Name
+
+ ID string `xml:"ID,attr"`
+ Version string `xml:"Version,attr"`
+ IssueInstant string `xml:"IssueInstant,attr"`
+
+ Issuer Issuer `xml:"Issuer"`
+ Signature *Signature `xml:"Signature"`
+ Subject Subject `xml:"Subject"`
+ Conditions Conditions `xml:"Conditions"`
+ AttributeStatement *AttributeStatement `xml:"AttributeStatement"`
+ AuthnStatement AuthnStatement `xml:"AuthnStatement"`
+}
+
+type Subject struct {
+ XMLName xml.Name
+
+ NameID NameID `xml:"NameID"`
+
+ SubjectConfirmation SubjectConfirmation `xml:"SubjectConfirmation"`
+}
+
+type NameID struct {
+ XMLName xml.Name
+
+ Format string `xml:"Format,attr"`
+ NameQualifier *string `xml:"NameQualifier,attr"`
+
+ Value string `xml:",innerxml"`
+}
+
+type SubjectConfirmation struct {
+ XMLName xml.Name
+
+ Method string `xml:"Method,attr"`
+
+ SubjectConfirmationData SubjectConfirmationData `xml:"SubjectConfirmationData"`
+}
+
+type SubjectConfirmationData struct {
+ XMLName xml.Name
+
+ InResponseTo *string `xml:"InResponseTo,attr"`
+ Recipient string `xml:"Recipient,attr"`
+ NotBefore *string `xml:"NotBefore,attr"`
+ NotOnOrAfter string `xml:"NotOnOrAfter,attr"`
+}
+
+type Conditions struct {
+ XMLName xml.Name
+
+ NotBefore *string `xml:"NotBefore,attr"`
+ NotOnOrAfter string `xml:"NotOnOrAfter,attr"`
+
+ AudienceRestrictions []AudienceRestriction `xml:"AudienceRestriction"`
+}
+
+type AudienceRestriction struct {
+ XMLName xml.Name
+
+ Audience Audience `xml:"Audience"`
+}
+
+type Audience struct {
+ XMLName xml.Name
+
+ Value string `xml:",innerxml"`
+}
+
+type AttributeStatement struct {
+ XMLName xml.Name
+
+ Attributes []Attribute `xml:"Attribute"`
+}
+
+type Attribute struct {
+ XMLName xml.Name
+
+ FriendlyName *string `xml:"FriendlyName,attr"`
+ Name string `xml:"Name,attr"`
+ NameFormat *string `xml:"NameFormat,attr"`
+
+ AttributeValues []AttributeValue `xml:"AttributeValue"`
+}
+
+type AttributeValue struct {
+ XMLName xml.Name
+
+ Type string `xml:"type,attr"`
+
+ Value string `xml:",innerxml"`
+}
+
+type AuthnStatement struct {
+ XMLName xml.Name
+
+ AuthnInstant string `xml:"AuthnInstant,attr"`
+ SessionIndex string `xml:"SessionIndex,attr"`
+
+ SubjectLocality *SubjectLocality `xml:"SubjectLocality"`
+
+ AuthnContext AuthnContext `xml:"AuthnContext"`
+}
+
+type SubjectLocality struct {
+ XMLName xml.Name
+
+ Address string `xml:"Address,attr"`
+}
+
+type AuthnContext struct {
+ XMLName xml.Name
+
+ AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"`
+}
+
+type AuthnContextClassRef struct {
+ XMLName xml.Name
+
+ Value string `xml:",innerxml"`
+}
diff --git a/pkg/util/samlutils/util.go b/pkg/util/samlutils/util.go
new file mode 100644
index 0000000000..5391d95a13
--- /dev/null
+++ b/pkg/util/samlutils/util.go
@@ -0,0 +1,128 @@
+// Copyright 2019 Yunion
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package samlutils
+
+import (
+ "bytes"
+ "compress/flate"
+ "crypto/rsa"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/ma314smith/signedxml"
+
+ "yunion.io/x/pkg/errors"
+)
+
+func compressString(in string) string {
+ buf := new(bytes.Buffer)
+ compressor, _ := flate.NewWriter(buf, 9)
+ compressor.Write([]byte(in))
+ compressor.Close()
+ return buf.String()
+}
+
+func decompressString(in string) string {
+ buf := new(bytes.Buffer)
+ decompressor := flate.NewReader(strings.NewReader(in))
+ io.Copy(buf, decompressor)
+ decompressor.Close()
+ return buf.String()
+}
+
+func compress(in []byte) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ compressor, _ := flate.NewWriter(buf, 9)
+ _, err := compressor.Write(in)
+ if err != nil {
+ return nil, errors.Wrap(err, "compressor.Write")
+ }
+ compressor.Close()
+ return buf.Bytes(), nil
+}
+
+func decompress(in []byte) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ decompressor := flate.NewReader(bytes.NewReader(in))
+ _, err := io.Copy(buf, decompressor)
+ if err != nil {
+ return nil, errors.Wrap(err, "io.Copy")
+ }
+ decompressor.Close()
+ return buf.Bytes(), nil
+}
+
+func SAMLDecode(input string) ([]byte, error) {
+ reqBytes, err := base64.StdEncoding.DecodeString(input)
+ if err != nil {
+ return nil, errors.Wrap(err, "base64.StdEncoding.DecodeString")
+ }
+ plainText, err := decompress(reqBytes)
+ if err != nil {
+ return nil, errors.Wrap(err, "decompress")
+ }
+ return plainText, nil
+}
+
+func SAMLEncode(input []byte) (string, error) {
+ comp, err := compress(input)
+ if err != nil {
+ return "", errors.Wrap(err, "compress")
+ }
+ return base64.StdEncoding.EncodeToString(comp), nil
+}
+
+func SAMLForm(action string, attrs map[string]string) string {
+ form := strings.Builder{}
+ // form.WriteString(``)
+ form.WriteString(``)
+ // form.WriteString(``)
+ return form.String()
+}
+
+func SignXML(xmlstr string, privateKey *rsa.PrivateKey) (string, error) {
+ signer, err := signedxml.NewSigner(string(xmlstr))
+ if err != nil {
+ return "", errors.Wrap(err, "signedxml.NewSigner")
+ }
+ signed, err := signer.Sign(privateKey)
+ if err != nil {
+ return "", errors.Wrap(err, "signer.Sign")
+ }
+ return signed, nil
+}
+
+func ValidateXML(signed string) ([]string, error) {
+ validator, err := signedxml.NewValidator(signed)
+ if err != nil {
+ return nil, errors.Wrap(err, "signedxml.NewValidator")
+ }
+ validXMLs, err := validator.ValidateReferences()
+ if err != nil {
+ return nil, errors.Wrap(err, "validator.ValidateReferences")
+ }
+ return validXMLs, nil
+}
diff --git a/pkg/util/seclib2/certfile.go b/pkg/util/seclib2/certfile.go
index 33537d4229..46141e4891 100644
--- a/pkg/util/seclib2/certfile.go
+++ b/pkg/util/seclib2/certfile.go
@@ -17,6 +17,7 @@ package seclib2
import (
"fmt"
"io/ioutil"
+ "regexp"
"strings"
)
@@ -55,3 +56,11 @@ func MergeCaCertFiles(cafile string, certfile string) (string, error) {
return tmpfile.Name(), nil
}
+
+func CleanCertificate(cert string) string {
+ re := regexp.MustCompile("---(.*)CERTIFICATE(.*)---")
+ cert = re.ReplaceAllString(cert, "")
+ cert = strings.Trim(cert, " \n")
+ // cert = strings.Replace(cert, "\n", "", -1)
+ return cert
+}
diff --git a/vendor/github.com/beevik/etree/.travis.yml b/vendor/github.com/beevik/etree/.travis.yml
new file mode 100644
index 0000000000..f4cb25d473
--- /dev/null
+++ b/vendor/github.com/beevik/etree/.travis.yml
@@ -0,0 +1,14 @@
+language: go
+sudo: false
+
+go:
+ - 1.11.x
+ - tip
+
+matrix:
+ allow_failures:
+ - go: tip
+
+script:
+ - go vet ./...
+ - go test -v ./...
diff --git a/vendor/github.com/beevik/etree/CONTRIBUTORS b/vendor/github.com/beevik/etree/CONTRIBUTORS
new file mode 100644
index 0000000000..03211a85eb
--- /dev/null
+++ b/vendor/github.com/beevik/etree/CONTRIBUTORS
@@ -0,0 +1,10 @@
+Brett Vickers (beevik)
+Felix Geisendörfer (felixge)
+Kamil Kisiel (kisielk)
+Graham King (grahamking)
+Matt Smith (ma314smith)
+Michal Jemala (michaljemala)
+Nicolas Piganeau (npiganeau)
+Chris Brown (ccbrown)
+Earncef Sequeira (earncef)
+Gabriel de Labachelerie (wuzuf)
diff --git a/vendor/github.com/beevik/etree/LICENSE b/vendor/github.com/beevik/etree/LICENSE
new file mode 100644
index 0000000000..26f1f7751b
--- /dev/null
+++ b/vendor/github.com/beevik/etree/LICENSE
@@ -0,0 +1,24 @@
+Copyright 2015-2019 Brett Vickers. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/beevik/etree/README.md b/vendor/github.com/beevik/etree/README.md
new file mode 100644
index 0000000000..08ec26b0ac
--- /dev/null
+++ b/vendor/github.com/beevik/etree/README.md
@@ -0,0 +1,205 @@
+[](https://travis-ci.org/beevik/etree)
+[](https://godoc.org/github.com/beevik/etree)
+
+etree
+=====
+
+The etree package is a lightweight, pure go package that expresses XML in
+the form of an element tree. Its design was inspired by the Python
+[ElementTree](http://docs.python.org/2/library/xml.etree.elementtree.html)
+module.
+
+Some of the package's capabilities and features:
+
+* Represents XML documents as trees of elements for easy traversal.
+* Imports, serializes, modifies or creates XML documents from scratch.
+* Writes and reads XML to/from files, byte slices, strings and io interfaces.
+* Performs simple or complex searches with lightweight XPath-like query APIs.
+* Auto-indents XML using spaces or tabs for better readability.
+* Implemented in pure go; depends only on standard go libraries.
+* Built on top of the go [encoding/xml](http://golang.org/pkg/encoding/xml)
+ package.
+
+### Creating an XML document
+
+The following example creates an XML document from scratch using the etree
+package and outputs its indented contents to stdout.
+```go
+doc := etree.NewDocument()
+doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
+doc.CreateProcInst("xml-stylesheet", `type="text/xsl" href="style.xsl"`)
+
+people := doc.CreateElement("People")
+people.CreateComment("These are all known people")
+
+jon := people.CreateElement("Person")
+jon.CreateAttr("name", "Jon")
+
+sally := people.CreateElement("Person")
+sally.CreateAttr("name", "Sally")
+
+doc.Indent(2)
+doc.WriteTo(os.Stdout)
+```
+
+Output:
+```xml
+
+
+
+
+
+
+
+```
+
+### Reading an XML file
+
+Suppose you have a file on disk called `bookstore.xml` containing the
+following data:
+
+```xml
+
+
+
+ Everyday Italian
+ Giada De Laurentiis
+ 2005
+ 30.00
+
+
+
+ Harry Potter
+ J K. Rowling
+ 2005
+ 29.99
+
+
+
+ XQuery Kick Start
+ James McGovern
+ Per Bothner
+ Kurt Cagle
+ James Linn
+ Vaidyanathan Nagarajan
+ 2003
+ 49.99
+
+
+
+ Learning XML
+ Erik T. Ray
+ 2003
+ 39.95
+
+
+
+```
+
+This code reads the file's contents into an etree document.
+```go
+doc := etree.NewDocument()
+if err := doc.ReadFromFile("bookstore.xml"); err != nil {
+ panic(err)
+}
+```
+
+You can also read XML from a string, a byte slice, or an `io.Reader`.
+
+### Processing elements and attributes
+
+This example illustrates several ways to access elements and attributes using
+etree selection queries.
+```go
+root := doc.SelectElement("bookstore")
+fmt.Println("ROOT element:", root.Tag)
+
+for _, book := range root.SelectElements("book") {
+ fmt.Println("CHILD element:", book.Tag)
+ if title := book.SelectElement("title"); title != nil {
+ lang := title.SelectAttrValue("lang", "unknown")
+ fmt.Printf(" TITLE: %s (%s)\n", title.Text(), lang)
+ }
+ for _, attr := range book.Attr {
+ fmt.Printf(" ATTR: %s=%s\n", attr.Key, attr.Value)
+ }
+}
+```
+Output:
+```
+ROOT element: bookstore
+CHILD element: book
+ TITLE: Everyday Italian (en)
+ ATTR: category=COOKING
+CHILD element: book
+ TITLE: Harry Potter (en)
+ ATTR: category=CHILDREN
+CHILD element: book
+ TITLE: XQuery Kick Start (en)
+ ATTR: category=WEB
+CHILD element: book
+ TITLE: Learning XML (en)
+ ATTR: category=WEB
+```
+
+### Path queries
+
+This example uses etree's path functions to select all book titles that fall
+into the category of 'WEB'. The double-slash prefix in the path causes the
+search for book elements to occur recursively; book elements may appear at any
+level of the XML hierarchy.
+```go
+for _, t := range doc.FindElements("//book[@category='WEB']/title") {
+ fmt.Println("Title:", t.Text())
+}
+```
+
+Output:
+```
+Title: XQuery Kick Start
+Title: Learning XML
+```
+
+This example finds the first book element under the root bookstore element and
+outputs the tag and text of each of its child elements.
+```go
+for _, e := range doc.FindElements("./bookstore/book[1]/*") {
+ fmt.Printf("%s: %s\n", e.Tag, e.Text())
+}
+```
+
+Output:
+```
+title: Everyday Italian
+author: Giada De Laurentiis
+year: 2005
+price: 30.00
+```
+
+This example finds all books with a price of 49.99 and outputs their titles.
+```go
+path := etree.MustCompilePath("./bookstore/book[p:price='49.99']/title")
+for _, e := range doc.FindElementsPath(path) {
+ fmt.Println(e.Text())
+}
+```
+
+Output:
+```
+XQuery Kick Start
+```
+
+Note that this example uses the FindElementsPath function, which takes as an
+argument a pre-compiled path object. Use precompiled paths when you plan to
+search with the same path more than once.
+
+### Other features
+
+These are just a few examples of the things the etree package can do. See the
+[documentation](http://godoc.org/github.com/beevik/etree) for a complete
+description of its capabilities.
+
+### Contributing
+
+This project accepts contributions. Just fork the repo and submit a pull
+request!
diff --git a/vendor/github.com/beevik/etree/RELEASE_NOTES.md b/vendor/github.com/beevik/etree/RELEASE_NOTES.md
new file mode 100644
index 0000000000..ee59d7abf4
--- /dev/null
+++ b/vendor/github.com/beevik/etree/RELEASE_NOTES.md
@@ -0,0 +1,109 @@
+Release v1.1.0
+==============
+
+**New Features**
+
+* New attribute helpers.
+ * Added the `Element.SortAttrs` method, which lexicographically sorts an
+ element's attributes by key.
+* New `ReadSettings` properties.
+ * Added `Entity` for the support of custom entity maps.
+* New `WriteSettings` properties.
+ * Added `UseCRLF` to allow the output of CR-LF newlines instead of the
+ default LF newlines. This is useful on Windows systems.
+* Additional support for text and CDATA sections.
+ * The `Element.Text` method now returns the concatenation of all consecutive
+ character data tokens immediately following an element's opening tag.
+ * Added `Element.SetCData` to replace the character data immediately
+ following an element's opening tag with a CDATA section.
+ * Added `Element.CreateCData` to create and add a CDATA section child
+ `CharData` token to an element.
+ * Added `Element.CreateText` to create and add a child text `CharData` token
+ to an element.
+ * Added `NewCData` to create a parentless CDATA section `CharData` token.
+ * Added `NewText` to create a parentless text `CharData`
+ token.
+ * Added `CharData.IsCData` to detect if the token contains a CDATA section.
+ * Added `CharData.IsWhitespace` to detect if the token contains whitespace
+ inserted by one of the document Indent functions.
+ * Modified `Element.SetText` so that it replaces a run of consecutive
+ character data tokens following the element's opening tag (instead of just
+ the first one).
+* New "tail text" support.
+ * Added the `Element.Tail` method, which returns the text immediately
+ following an element's closing tag.
+ * Added the `Element.SetTail` method, which modifies the text immediately
+ following an element's closing tag.
+* New element child insertion and removal methods.
+ * Added the `Element.InsertChildAt` method, which inserts a new child token
+ before the specified child token index.
+ * Added the `Element.RemoveChildAt` method, which removes the child token at
+ the specified child token index.
+* New element and attribute queries.
+ * Added the `Element.Index` method, which returns the element's index within
+ its parent element's child token list.
+ * Added the `Element.NamespaceURI` method to return the namespace URI
+ associated with an element.
+ * Added the `Attr.NamespaceURI` method to return the namespace URI
+ associated with an element.
+ * Added the `Attr.Element` method to return the element that an attribute
+ belongs to.
+* New Path filter functions.
+ * Added `[local-name()='val']` to keep elements whose unprefixed tag matches
+ the desired value.
+ * Added `[name()='val']` to keep elements whose full tag matches the desired
+ value.
+ * Added `[namespace-prefix()='val']` to keep elements whose namespace prefix
+ matches the desired value.
+ * Added `[namespace-uri()='val']` to keep elements whose namespace URI
+ matches the desired value.
+
+**Bug Fixes**
+
+* A default XML `CharSetReader` is now used to prevent failed parsing of XML
+ documents using certain encodings.
+ ([Issue](https://github.com/beevik/etree/issues/53)).
+* All characters are now properly escaped according to XML parsing rules.
+ ([Issue](https://github.com/beevik/etree/issues/55)).
+* The `Document.Indent` and `Document.IndentTabs` functions no longer insert
+ empty string `CharData` tokens.
+
+**Deprecated**
+
+* `Element`
+ * The `InsertChild` method is deprecated. Use `InsertChildAt` instead.
+ * The `CreateCharData` method is deprecated. Use `CreateText` instead.
+* `CharData`
+ * The `NewCharData` method is deprecated. Use `NewText` instead.
+
+
+Release v1.0.1
+==============
+
+**Changes**
+
+* Added support for absolute etree Path queries. An absolute path begins with
+ `/` or `//` and begins its search from the element's document root.
+* Added [`GetPath`](https://godoc.org/github.com/beevik/etree#Element.GetPath)
+ and [`GetRelativePath`](https://godoc.org/github.com/beevik/etree#Element.GetRelativePath)
+ functions to the [`Element`](https://godoc.org/github.com/beevik/etree#Element)
+ type.
+
+**Breaking changes**
+
+* A path starting with `//` is now interpreted as an absolute path.
+ Previously, it was interpreted as a relative path starting from the element
+ whose
+ [`FindElement`](https://godoc.org/github.com/beevik/etree#Element.FindElement)
+ method was called. To remain compatible with this release, all paths
+ prefixed with `//` should be prefixed with `.//` when called from any
+ element other than the document's root.
+* [**edit 2/1/2019**]: Minor releases should not contain breaking changes.
+ Even though this breaking change was very minor, it was a mistake to include
+ it in this minor release. In the future, all breaking changes will be
+ limited to major releases (e.g., version 2.0.0).
+
+Release v1.0.0
+==============
+
+Initial release.
diff --git a/vendor/github.com/beevik/etree/etree.go b/vendor/github.com/beevik/etree/etree.go
new file mode 100644
index 0000000000..9e24f90126
--- /dev/null
+++ b/vendor/github.com/beevik/etree/etree.go
@@ -0,0 +1,1453 @@
+// Copyright 2015-2019 Brett Vickers.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package etree provides XML services through an Element Tree
+// abstraction.
+package etree
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "io"
+ "os"
+ "sort"
+ "strings"
+)
+
+const (
+ // NoIndent is used with Indent to disable all indenting.
+ NoIndent = -1
+)
+
+// ErrXML is returned when XML parsing fails due to incorrect formatting.
+var ErrXML = errors.New("etree: invalid XML format")
+
+// ReadSettings allow for changing the default behavior of the ReadFrom*
+// methods.
+type ReadSettings struct {
+ // CharsetReader to be passed to standard xml.Decoder. Default: nil.
+ CharsetReader func(charset string, input io.Reader) (io.Reader, error)
+
+ // Permissive allows input containing common mistakes such as missing tags
+ // or attribute values. Default: false.
+ Permissive bool
+
+ // Entity to be passed to standard xml.Decoder. Default: nil.
+ Entity map[string]string
+}
+
+// newReadSettings creates a default ReadSettings record.
+func newReadSettings() ReadSettings {
+ return ReadSettings{
+ CharsetReader: func(label string, input io.Reader) (io.Reader, error) {
+ return input, nil
+ },
+ Permissive: false,
+ }
+}
+
+// WriteSettings allow for changing the serialization behavior of the WriteTo*
+// methods.
+type WriteSettings struct {
+ // CanonicalEndTags forces the production of XML end tags, even for
+ // elements that have no child elements. Default: false.
+ CanonicalEndTags bool
+
+ // CanonicalText forces the production of XML character references for
+ // text data characters &, <, and >. If false, XML character references
+ // are also produced for " and '. Default: false.
+ CanonicalText bool
+
+ // CanonicalAttrVal forces the production of XML character references for
+ // attribute value characters &, < and ". If false, XML character
+ // references are also produced for > and '. Default: false.
+ CanonicalAttrVal bool
+
+ // When outputting indented XML, use a carriage return and linefeed
+ // ("\r\n") as a new-line delimiter instead of just a linefeed ("\n").
+ // This is useful on Windows-based systems.
+ UseCRLF bool
+}
+
+// newWriteSettings creates a default WriteSettings record.
+func newWriteSettings() WriteSettings {
+ return WriteSettings{
+ CanonicalEndTags: false,
+ CanonicalText: false,
+ CanonicalAttrVal: false,
+ UseCRLF: false,
+ }
+}
+
+// A Token is an empty interface that represents an Element, CharData,
+// Comment, Directive, or ProcInst.
+type Token interface {
+ Parent() *Element
+ Index() int
+ dup(parent *Element) Token
+ setParent(parent *Element)
+ setIndex(index int)
+ writeTo(w *bufio.Writer, s *WriteSettings)
+}
+
+// A Document is a container holding a complete XML hierarchy. Its embedded
+// element contains zero or more children, one of which is usually the root
+// element. The embedded element may include other children such as
+// processing instructions or BOM CharData tokens.
+type Document struct {
+ Element
+ ReadSettings ReadSettings
+ WriteSettings WriteSettings
+}
+
+// An Element represents an XML element, its attributes, and its child tokens.
+type Element struct {
+ Space, Tag string // namespace prefix and tag
+ Attr []Attr // key-value attribute pairs
+ Child []Token // child tokens (elements, comments, etc.)
+ parent *Element // parent element
+ index int // token index in parent's children
+}
+
+// An Attr represents a key-value attribute of an XML element.
+type Attr struct {
+ Space, Key string // The attribute's namespace prefix and key
+ Value string // The attribute value string
+ element *Element // element containing the attribute
+}
+
+// charDataFlags are used with CharData tokens to store additional settings.
+type charDataFlags uint8
+
+const (
+ // The CharData was created by an indent function as whitespace.
+ whitespaceFlag charDataFlags = 1 << iota
+
+ // The CharData contains a CDATA section.
+ cdataFlag
+)
+
+// CharData can be used to represent character data or a CDATA section within
+// an XML document.
+type CharData struct {
+ Data string
+ parent *Element
+ index int
+ flags charDataFlags
+}
+
+// A Comment represents an XML comment.
+type Comment struct {
+ Data string
+ parent *Element
+ index int
+}
+
+// A Directive represents an XML directive.
+type Directive struct {
+ Data string
+ parent *Element
+ index int
+}
+
+// A ProcInst represents an XML processing instruction.
+type ProcInst struct {
+ Target string
+ Inst string
+ parent *Element
+ index int
+}
+
+// NewDocument creates an XML document without a root element.
+func NewDocument() *Document {
+ return &Document{
+ Element{Child: make([]Token, 0)},
+ newReadSettings(),
+ newWriteSettings(),
+ }
+}
+
+// Copy returns a recursive, deep copy of the document.
+func (d *Document) Copy() *Document {
+ return &Document{*(d.dup(nil).(*Element)), d.ReadSettings, d.WriteSettings}
+}
+
+// Root returns the root element of the document, or nil if there is no root
+// element.
+func (d *Document) Root() *Element {
+ for _, t := range d.Child {
+ if c, ok := t.(*Element); ok {
+ return c
+ }
+ }
+ return nil
+}
+
+// SetRoot replaces the document's root element with e. If the document
+// already has a root when this function is called, then the document's
+// original root is unbound first. If the element e is bound to another
+// document (or to another element within a document), then it is unbound
+// first.
+func (d *Document) SetRoot(e *Element) {
+ if e.parent != nil {
+ e.parent.RemoveChild(e)
+ }
+
+ p := &d.Element
+ e.setParent(p)
+
+ // If there is already a root element, replace it.
+ for i, t := range p.Child {
+ if _, ok := t.(*Element); ok {
+ t.setParent(nil)
+ t.setIndex(-1)
+ p.Child[i] = e
+ e.setIndex(i)
+ return
+ }
+ }
+
+ // No existing root element, so add it.
+ p.addChild(e)
+}
+
+// ReadFrom reads XML from the reader r into the document d. It returns the
+// number of bytes read and any error encountered.
+func (d *Document) ReadFrom(r io.Reader) (n int64, err error) {
+ return d.Element.readFrom(r, d.ReadSettings)
+}
+
+// ReadFromFile reads XML from the string s into the document d.
+func (d *Document) ReadFromFile(filename string) error {
+ f, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ _, err = d.ReadFrom(f)
+ return err
+}
+
+// ReadFromBytes reads XML from the byte slice b into the document d.
+func (d *Document) ReadFromBytes(b []byte) error {
+ _, err := d.ReadFrom(bytes.NewReader(b))
+ return err
+}
+
+// ReadFromString reads XML from the string s into the document d.
+func (d *Document) ReadFromString(s string) error {
+ _, err := d.ReadFrom(strings.NewReader(s))
+ return err
+}
+
+// WriteTo serializes an XML document into the writer w. It
+// returns the number of bytes written and any error encountered.
+func (d *Document) WriteTo(w io.Writer) (n int64, err error) {
+ cw := newCountWriter(w)
+ b := bufio.NewWriter(cw)
+ for _, c := range d.Child {
+ c.writeTo(b, &d.WriteSettings)
+ }
+ err, n = b.Flush(), cw.bytes
+ return
+}
+
+// WriteToFile serializes an XML document into the file named
+// filename.
+func (d *Document) WriteToFile(filename string) error {
+ f, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ _, err = d.WriteTo(f)
+ return err
+}
+
+// WriteToBytes serializes the XML document into a slice of
+// bytes.
+func (d *Document) WriteToBytes() (b []byte, err error) {
+ var buf bytes.Buffer
+ if _, err = d.WriteTo(&buf); err != nil {
+ return
+ }
+ return buf.Bytes(), nil
+}
+
+// WriteToString serializes the XML document into a string.
+func (d *Document) WriteToString() (s string, err error) {
+ var b []byte
+ if b, err = d.WriteToBytes(); err != nil {
+ return
+ }
+ return string(b), nil
+}
+
+type indentFunc func(depth int) string
+
+// Indent modifies the document's element tree by inserting character data
+// tokens containing newlines and indentation. The amount of indentation per
+// depth level is given as spaces. Pass etree.NoIndent for spaces if you want
+// no indentation at all.
+func (d *Document) Indent(spaces int) {
+ var indent indentFunc
+ switch {
+ case spaces < 0:
+ indent = func(depth int) string { return "" }
+ case d.WriteSettings.UseCRLF == true:
+ indent = func(depth int) string { return indentCRLF(depth*spaces, indentSpaces) }
+ default:
+ indent = func(depth int) string { return indentLF(depth*spaces, indentSpaces) }
+ }
+ d.Element.indent(0, indent)
+}
+
+// IndentTabs modifies the document's element tree by inserting CharData
+// tokens containing newlines and tabs for indentation. One tab is used per
+// indentation level.
+func (d *Document) IndentTabs() {
+ var indent indentFunc
+ switch d.WriteSettings.UseCRLF {
+ case true:
+ indent = func(depth int) string { return indentCRLF(depth, indentTabs) }
+ default:
+ indent = func(depth int) string { return indentLF(depth, indentTabs) }
+ }
+ d.Element.indent(0, indent)
+}
+
+// NewElement creates an unparented element with the specified tag. The tag
+// may be prefixed by a namespace prefix and a colon.
+func NewElement(tag string) *Element {
+ space, stag := spaceDecompose(tag)
+ return newElement(space, stag, nil)
+}
+
+// newElement is a helper function that creates an element and binds it to
+// a parent element if possible.
+func newElement(space, tag string, parent *Element) *Element {
+ e := &Element{
+ Space: space,
+ Tag: tag,
+ Attr: make([]Attr, 0),
+ Child: make([]Token, 0),
+ parent: parent,
+ index: -1,
+ }
+ if parent != nil {
+ parent.addChild(e)
+ }
+ return e
+}
+
+// Copy creates a recursive, deep copy of the element and all its attributes
+// and children. The returned element has no parent but can be parented to a
+// another element using AddElement, or to a document using SetRoot.
+func (e *Element) Copy() *Element {
+ return e.dup(nil).(*Element)
+}
+
+// FullTag returns the element e's complete tag, including namespace prefix if
+// present.
+func (e *Element) FullTag() string {
+ if e.Space == "" {
+ return e.Tag
+ }
+ return e.Space + ":" + e.Tag
+}
+
+// NamespaceURI returns the XML namespace URI associated with the element. If
+// the element is part of the XML default namespace, NamespaceURI returns the
+// empty string.
+func (e *Element) NamespaceURI() string {
+ if e.Space == "" {
+ return e.findDefaultNamespaceURI()
+ }
+ return e.findLocalNamespaceURI(e.Space)
+}
+
+// findLocalNamespaceURI finds the namespace URI corresponding to the
+// requested prefix.
+func (e *Element) findLocalNamespaceURI(prefix string) string {
+ for _, a := range e.Attr {
+ if a.Space == "xmlns" && a.Key == prefix {
+ return a.Value
+ }
+ }
+
+ if e.parent == nil {
+ return ""
+ }
+
+ return e.parent.findLocalNamespaceURI(prefix)
+}
+
+// findDefaultNamespaceURI finds the default namespace URI of the element.
+func (e *Element) findDefaultNamespaceURI() string {
+ for _, a := range e.Attr {
+ if a.Space == "" && a.Key == "xmlns" {
+ return a.Value
+ }
+ }
+
+ if e.parent == nil {
+ return ""
+ }
+
+ return e.parent.findDefaultNamespaceURI()
+}
+
+// hasText returns true if the element has character data immediately
+// folllowing the element's opening tag.
+func (e *Element) hasText() bool {
+ if len(e.Child) == 0 {
+ return false
+ }
+ _, ok := e.Child[0].(*CharData)
+ return ok
+}
+
+// namespacePrefix returns the namespace prefix associated with the element.
+func (e *Element) namespacePrefix() string {
+ return e.Space
+}
+
+// name returns the tag associated with the element.
+func (e *Element) name() string {
+ return e.Tag
+}
+
+// Text returns all character data immediately following the element's opening
+// tag.
+func (e *Element) Text() string {
+ if len(e.Child) == 0 {
+ return ""
+ }
+
+ text := ""
+ for _, ch := range e.Child {
+ if cd, ok := ch.(*CharData); ok {
+ if text == "" {
+ text = cd.Data
+ } else {
+ text = text + cd.Data
+ }
+ } else {
+ break
+ }
+ }
+ return text
+}
+
+// SetText replaces all character data immediately following an element's
+// opening tag with the requested string.
+func (e *Element) SetText(text string) {
+ e.replaceText(0, text, 0)
+}
+
+// SetCData replaces all character data immediately following an element's
+// opening tag with a CDATA section.
+func (e *Element) SetCData(text string) {
+ e.replaceText(0, text, cdataFlag)
+}
+
+// Tail returns all character data immediately following the element's end
+// tag.
+func (e *Element) Tail() string {
+ if e.Parent() == nil {
+ return ""
+ }
+
+ p := e.Parent()
+ i := e.Index()
+
+ text := ""
+ for _, ch := range p.Child[i+1:] {
+ if cd, ok := ch.(*CharData); ok {
+ if text == "" {
+ text = cd.Data
+ } else {
+ text = text + cd.Data
+ }
+ } else {
+ break
+ }
+ }
+ return text
+}
+
+// SetTail replaces all character data immediately following the element's end
+// tag with the requested string.
+func (e *Element) SetTail(text string) {
+ if e.Parent() == nil {
+ return
+ }
+
+ p := e.Parent()
+ p.replaceText(e.Index()+1, text, 0)
+}
+
+// replaceText is a helper function that replaces a series of chardata tokens
+// starting at index i with the requested text.
+func (e *Element) replaceText(i int, text string, flags charDataFlags) {
+ end := e.findTermCharDataIndex(i)
+
+ switch {
+ case end == i:
+ if text != "" {
+ // insert a new chardata token at index i
+ cd := newCharData(text, flags, nil)
+ e.InsertChildAt(i, cd)
+ }
+
+ case end == i+1:
+ if text == "" {
+ // remove the chardata token at index i
+ e.RemoveChildAt(i)
+ } else {
+ // replace the first and only character token at index i
+ cd := e.Child[i].(*CharData)
+ cd.Data, cd.flags = text, flags
+ }
+
+ default:
+ if text == "" {
+ // remove all chardata tokens starting from index i
+ copy(e.Child[i:], e.Child[end:])
+ removed := end - i
+ e.Child = e.Child[:len(e.Child)-removed]
+ for j := i; j < len(e.Child); j++ {
+ e.Child[j].setIndex(j)
+ }
+ } else {
+ // replace the first chardata token at index i and remove all
+ // subsequent chardata tokens
+ cd := e.Child[i].(*CharData)
+ cd.Data, cd.flags = text, flags
+ copy(e.Child[i+1:], e.Child[end:])
+ removed := end - (i + 1)
+ e.Child = e.Child[:len(e.Child)-removed]
+ for j := i + 1; j < len(e.Child); j++ {
+ e.Child[j].setIndex(j)
+ }
+ }
+ }
+}
+
+// findTermCharDataIndex finds the index of the first child token that isn't
+// a CharData token. It starts from the requested start index.
+func (e *Element) findTermCharDataIndex(start int) int {
+ for i := start; i < len(e.Child); i++ {
+ if _, ok := e.Child[i].(*CharData); !ok {
+ return i
+ }
+ }
+ return len(e.Child)
+}
+
+// CreateElement creates an element with the specified tag and adds it as the
+// last child element of the element e. The tag may be prefixed by a namespace
+// prefix and a colon.
+func (e *Element) CreateElement(tag string) *Element {
+ space, stag := spaceDecompose(tag)
+ return newElement(space, stag, e)
+}
+
+// AddChild adds the token t as the last child of element e. If token t was
+// already the child of another element, it is first removed from its current
+// parent element.
+func (e *Element) AddChild(t Token) {
+ if t.Parent() != nil {
+ t.Parent().RemoveChild(t)
+ }
+
+ t.setParent(e)
+ e.addChild(t)
+}
+
+// InsertChild inserts the token t before e's existing child token ex. If ex
+// is nil or ex is not a child of e, then t is added to the end of e's child
+// token list. If token t was already the child of another element, it is
+// first removed from its current parent element.
+//
+// Deprecated: InsertChild is deprecated. Use InsertChildAt instead.
+func (e *Element) InsertChild(ex Token, t Token) {
+ if ex == nil || ex.Parent() != e {
+ e.AddChild(t)
+ return
+ }
+
+ if t.Parent() != nil {
+ t.Parent().RemoveChild(t)
+ }
+
+ t.setParent(e)
+
+ i := ex.Index()
+ e.Child = append(e.Child, nil)
+ copy(e.Child[i+1:], e.Child[i:])
+ e.Child[i] = t
+
+ for j := i; j < len(e.Child); j++ {
+ e.Child[j].setIndex(j)
+ }
+}
+
+// InsertChildAt inserts the token t into the element e's list of child tokens
+// just before the requested index. If the index is greater than or equal to
+// the length of the list of child tokens, the token t is added to the end of
+// the list.
+func (e *Element) InsertChildAt(index int, t Token) {
+ if index >= len(e.Child) {
+ e.AddChild(t)
+ return
+ }
+
+ if t.Parent() != nil {
+ if t.Parent() == e && t.Index() > index {
+ index--
+ }
+ t.Parent().RemoveChild(t)
+ }
+
+ t.setParent(e)
+
+ e.Child = append(e.Child, nil)
+ copy(e.Child[index+1:], e.Child[index:])
+ e.Child[index] = t
+
+ for j := index; j < len(e.Child); j++ {
+ e.Child[j].setIndex(j)
+ }
+}
+
+// RemoveChild attempts to remove the token t from element e's list of
+// children. If the token t is a child of e, then it is returned. Otherwise,
+// nil is returned.
+func (e *Element) RemoveChild(t Token) Token {
+ if t.Parent() != e {
+ return nil
+ }
+ return e.RemoveChildAt(t.Index())
+}
+
+// RemoveChildAt removes the index-th child token from the element e. The
+// removed child token is returned. If the index is out of bounds, no child is
+// removed and nil is returned.
+func (e *Element) RemoveChildAt(index int) Token {
+ if index >= len(e.Child) {
+ return nil
+ }
+
+ t := e.Child[index]
+ for j := index + 1; j < len(e.Child); j++ {
+ e.Child[j].setIndex(j - 1)
+ }
+ e.Child = append(e.Child[:index], e.Child[index+1:]...)
+ t.setIndex(-1)
+ t.setParent(nil)
+ return t
+}
+
+// ReadFrom reads XML from the reader r and stores the result as a new child
+// of element e.
+func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err error) {
+ r := newCountReader(ri)
+ dec := xml.NewDecoder(r)
+ dec.CharsetReader = settings.CharsetReader
+ dec.Strict = !settings.Permissive
+ dec.Entity = settings.Entity
+ var stack stack
+ stack.push(e)
+ for {
+ t, err := dec.RawToken()
+ switch {
+ case err == io.EOF:
+ return r.bytes, nil
+ case err != nil:
+ return r.bytes, err
+ case stack.empty():
+ return r.bytes, ErrXML
+ }
+
+ top := stack.peek().(*Element)
+
+ switch t := t.(type) {
+ case xml.StartElement:
+ e := newElement(t.Name.Space, t.Name.Local, top)
+ for _, a := range t.Attr {
+ e.createAttr(a.Name.Space, a.Name.Local, a.Value, e)
+ }
+ stack.push(e)
+ case xml.EndElement:
+ stack.pop()
+ case xml.CharData:
+ data := string(t)
+ var flags charDataFlags
+ if isWhitespace(data) {
+ flags = whitespaceFlag
+ }
+ newCharData(data, flags, top)
+ case xml.Comment:
+ newComment(string(t), top)
+ case xml.Directive:
+ newDirective(string(t), top)
+ case xml.ProcInst:
+ newProcInst(t.Target, string(t.Inst), top)
+ }
+ }
+}
+
+// SelectAttr finds an element attribute matching the requested key and
+// returns it if found. Returns nil if no matching attribute is found. The key
+// may be prefixed by a namespace prefix and a colon.
+func (e *Element) SelectAttr(key string) *Attr {
+ space, skey := spaceDecompose(key)
+ for i, a := range e.Attr {
+ if spaceMatch(space, a.Space) && skey == a.Key {
+ return &e.Attr[i]
+ }
+ }
+ return nil
+}
+
+// SelectAttrValue finds an element attribute matching the requested key and
+// returns its value if found. The key may be prefixed by a namespace prefix
+// and a colon. If the key is not found, the dflt value is returned instead.
+func (e *Element) SelectAttrValue(key, dflt string) string {
+ space, skey := spaceDecompose(key)
+ for _, a := range e.Attr {
+ if spaceMatch(space, a.Space) && skey == a.Key {
+ return a.Value
+ }
+ }
+ return dflt
+}
+
+// ChildElements returns all elements that are children of element e.
+func (e *Element) ChildElements() []*Element {
+ var elements []*Element
+ for _, t := range e.Child {
+ if c, ok := t.(*Element); ok {
+ elements = append(elements, c)
+ }
+ }
+ return elements
+}
+
+// SelectElement returns the first child element with the given tag. The tag
+// may be prefixed by a namespace prefix and a colon. Returns nil if no
+// element with a matching tag was found.
+func (e *Element) SelectElement(tag string) *Element {
+ space, stag := spaceDecompose(tag)
+ for _, t := range e.Child {
+ if c, ok := t.(*Element); ok && spaceMatch(space, c.Space) && stag == c.Tag {
+ return c
+ }
+ }
+ return nil
+}
+
+// SelectElements returns a slice of all child elements with the given tag.
+// The tag may be prefixed by a namespace prefix and a colon.
+func (e *Element) SelectElements(tag string) []*Element {
+ space, stag := spaceDecompose(tag)
+ var elements []*Element
+ for _, t := range e.Child {
+ if c, ok := t.(*Element); ok && spaceMatch(space, c.Space) && stag == c.Tag {
+ elements = append(elements, c)
+ }
+ }
+ return elements
+}
+
+// FindElement returns the first element matched by the XPath-like path
+// string. Returns nil if no element is found using the path. Panics if an
+// invalid path string is supplied.
+func (e *Element) FindElement(path string) *Element {
+ return e.FindElementPath(MustCompilePath(path))
+}
+
+// FindElementPath returns the first element matched by the XPath-like path
+// string. Returns nil if no element is found using the path.
+func (e *Element) FindElementPath(path Path) *Element {
+ p := newPather()
+ elements := p.traverse(e, path)
+ switch {
+ case len(elements) > 0:
+ return elements[0]
+ default:
+ return nil
+ }
+}
+
+// FindElements returns a slice of elements matched by the XPath-like path
+// string. Panics if an invalid path string is supplied.
+func (e *Element) FindElements(path string) []*Element {
+ return e.FindElementsPath(MustCompilePath(path))
+}
+
+// FindElementsPath returns a slice of elements matched by the Path object.
+func (e *Element) FindElementsPath(path Path) []*Element {
+ p := newPather()
+ return p.traverse(e, path)
+}
+
+// GetPath returns the absolute path of the element.
+func (e *Element) GetPath() string {
+ path := []string{}
+ for seg := e; seg != nil; seg = seg.Parent() {
+ if seg.Tag != "" {
+ path = append(path, seg.Tag)
+ }
+ }
+
+ // Reverse the path.
+ for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
+ path[i], path[j] = path[j], path[i]
+ }
+
+ return "/" + strings.Join(path, "/")
+}
+
+// GetRelativePath returns the path of the element relative to the source
+// element. If the two elements are not part of the same element tree, then
+// GetRelativePath returns the empty string.
+func (e *Element) GetRelativePath(source *Element) string {
+ var path []*Element
+
+ if source == nil {
+ return ""
+ }
+
+ // Build a reverse path from the element toward the root. Stop if the
+ // source element is encountered.
+ var seg *Element
+ for seg = e; seg != nil && seg != source; seg = seg.Parent() {
+ path = append(path, seg)
+ }
+
+ // If we found the source element, reverse the path and compose the
+ // string.
+ if seg == source {
+ if len(path) == 0 {
+ return "."
+ }
+ parts := []string{}
+ for i := len(path) - 1; i >= 0; i-- {
+ parts = append(parts, path[i].Tag)
+ }
+ return "./" + strings.Join(parts, "/")
+ }
+
+ // The source wasn't encountered, so climb from the source element toward
+ // the root of the tree until an element in the reversed path is
+ // encountered.
+
+ findPathIndex := func(e *Element, path []*Element) int {
+ for i, ee := range path {
+ if e == ee {
+ return i
+ }
+ }
+ return -1
+ }
+
+ climb := 0
+ for seg = source; seg != nil; seg = seg.Parent() {
+ i := findPathIndex(seg, path)
+ if i >= 0 {
+ path = path[:i] // truncate at found segment
+ break
+ }
+ climb++
+ }
+
+ // No element in the reversed path was encountered, so the two elements
+ // must not be part of the same tree.
+ if seg == nil {
+ return ""
+ }
+
+ // Reverse the (possibly truncated) path and prepend ".." segments to
+ // climb.
+ parts := []string{}
+ for i := 0; i < climb; i++ {
+ parts = append(parts, "..")
+ }
+ for i := len(path) - 1; i >= 0; i-- {
+ parts = append(parts, path[i].Tag)
+ }
+ return strings.Join(parts, "/")
+}
+
+// indent recursively inserts proper indentation between an
+// XML element's child tokens.
+func (e *Element) indent(depth int, indent indentFunc) {
+ e.stripIndent()
+ n := len(e.Child)
+ if n == 0 {
+ return
+ }
+
+ oldChild := e.Child
+ e.Child = make([]Token, 0, n*2+1)
+ isCharData, firstNonCharData := false, true
+ for _, c := range oldChild {
+ // Insert NL+indent before child if it's not character data.
+ // Exceptions: when it's the first non-character-data child, or when
+ // the child is at root depth.
+ _, isCharData = c.(*CharData)
+ if !isCharData {
+ if !firstNonCharData || depth > 0 {
+ s := indent(depth)
+ if s != "" {
+ newCharData(s, whitespaceFlag, e)
+ }
+ }
+ firstNonCharData = false
+ }
+
+ e.addChild(c)
+
+ // Recursively process child elements.
+ if ce, ok := c.(*Element); ok {
+ ce.indent(depth+1, indent)
+ }
+ }
+
+ // Insert NL+indent before the last child.
+ if !isCharData {
+ if !firstNonCharData || depth > 0 {
+ s := indent(depth - 1)
+ if s != "" {
+ newCharData(s, whitespaceFlag, e)
+ }
+ }
+ }
+}
+
+// stripIndent removes any previously inserted indentation.
+func (e *Element) stripIndent() {
+ // Count the number of non-indent child tokens
+ n := len(e.Child)
+ for _, c := range e.Child {
+ if cd, ok := c.(*CharData); ok && cd.IsWhitespace() {
+ n--
+ }
+ }
+ if n == len(e.Child) {
+ return
+ }
+
+ // Strip out indent CharData
+ newChild := make([]Token, n)
+ j := 0
+ for _, c := range e.Child {
+ if cd, ok := c.(*CharData); ok && cd.IsWhitespace() {
+ continue
+ }
+ newChild[j] = c
+ newChild[j].setIndex(j)
+ j++
+ }
+ e.Child = newChild
+}
+
+// dup duplicates the element.
+func (e *Element) dup(parent *Element) Token {
+ ne := &Element{
+ Space: e.Space,
+ Tag: e.Tag,
+ Attr: make([]Attr, len(e.Attr)),
+ Child: make([]Token, len(e.Child)),
+ parent: parent,
+ index: e.index,
+ }
+ for i, t := range e.Child {
+ ne.Child[i] = t.dup(ne)
+ }
+ for i, a := range e.Attr {
+ ne.Attr[i] = a
+ }
+ return ne
+}
+
+// Parent returns the element token's parent element, or nil if it has no
+// parent.
+func (e *Element) Parent() *Element {
+ return e.parent
+}
+
+// Index returns the index of this element within its parent element's
+// list of child tokens. If this element has no parent element, the index
+// is -1.
+func (e *Element) Index() int {
+ return e.index
+}
+
+// setParent replaces the element token's parent.
+func (e *Element) setParent(parent *Element) {
+ e.parent = parent
+}
+
+// setIndex sets the element token's index within its parent's Child slice.
+func (e *Element) setIndex(index int) {
+ e.index = index
+}
+
+// writeTo serializes the element to the writer w.
+func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) {
+ w.WriteByte('<')
+ w.WriteString(e.FullTag())
+ for _, a := range e.Attr {
+ w.WriteByte(' ')
+ a.writeTo(w, s)
+ }
+ if len(e.Child) > 0 {
+ w.WriteString(">")
+ for _, c := range e.Child {
+ c.writeTo(w, s)
+ }
+ w.Write([]byte{'<', '/'})
+ w.WriteString(e.FullTag())
+ w.WriteByte('>')
+ } else {
+ if s.CanonicalEndTags {
+ w.Write([]byte{'>', '<', '/'})
+ w.WriteString(e.FullTag())
+ w.WriteByte('>')
+ } else {
+ w.Write([]byte{'/', '>'})
+ }
+ }
+}
+
+// addChild adds a child token to the element e.
+func (e *Element) addChild(t Token) {
+ t.setIndex(len(e.Child))
+ e.Child = append(e.Child, t)
+}
+
+// CreateAttr creates an attribute and adds it to element e. The key may be
+// prefixed by a namespace prefix and a colon. If an attribute with the key
+// already exists, its value is replaced.
+func (e *Element) CreateAttr(key, value string) *Attr {
+ space, skey := spaceDecompose(key)
+ return e.createAttr(space, skey, value, e)
+}
+
+// createAttr is a helper function that creates attributes.
+func (e *Element) createAttr(space, key, value string, parent *Element) *Attr {
+ for i, a := range e.Attr {
+ if space == a.Space && key == a.Key {
+ e.Attr[i].Value = value
+ return &e.Attr[i]
+ }
+ }
+ a := Attr{
+ Space: space,
+ Key: key,
+ Value: value,
+ element: parent,
+ }
+ e.Attr = append(e.Attr, a)
+ return &e.Attr[len(e.Attr)-1]
+}
+
+// RemoveAttr removes and returns a copy of the first attribute of the element
+// whose key matches the given key. The key may be prefixed by a namespace
+// prefix and a colon. If a matching attribute does not exist, nil is
+// returned.
+func (e *Element) RemoveAttr(key string) *Attr {
+ space, skey := spaceDecompose(key)
+ for i, a := range e.Attr {
+ if space == a.Space && skey == a.Key {
+ e.Attr = append(e.Attr[0:i], e.Attr[i+1:]...)
+ return &Attr{
+ Space: a.Space,
+ Key: a.Key,
+ Value: a.Value,
+ element: nil,
+ }
+ }
+ }
+ return nil
+}
+
+// SortAttrs sorts the element's attributes lexicographically by key.
+func (e *Element) SortAttrs() {
+ sort.Sort(byAttr(e.Attr))
+}
+
+type byAttr []Attr
+
+func (a byAttr) Len() int {
+ return len(a)
+}
+
+func (a byAttr) Swap(i, j int) {
+ a[i], a[j] = a[j], a[i]
+}
+
+func (a byAttr) Less(i, j int) bool {
+ sp := strings.Compare(a[i].Space, a[j].Space)
+ if sp == 0 {
+ return strings.Compare(a[i].Key, a[j].Key) < 0
+ }
+ return sp < 0
+}
+
+// FullKey returns the attribute a's complete key, including namespace prefix
+// if present.
+func (a *Attr) FullKey() string {
+ if a.Space == "" {
+ return a.Key
+ }
+ return a.Space + ":" + a.Key
+}
+
+// Element returns the element containing the attribute.
+func (a *Attr) Element() *Element {
+ return a.element
+}
+
+// NamespaceURI returns the XML namespace URI associated with the attribute.
+// If the element is part of the XML default namespace, NamespaceURI returns
+// the empty string.
+func (a *Attr) NamespaceURI() string {
+ return a.element.NamespaceURI()
+}
+
+// writeTo serializes the attribute to the writer.
+func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) {
+ w.WriteString(a.FullKey())
+ w.WriteString(`="`)
+ var m escapeMode
+ if s.CanonicalAttrVal {
+ m = escapeCanonicalAttr
+ } else {
+ m = escapeNormal
+ }
+ escapeString(w, a.Value, m)
+ w.WriteByte('"')
+}
+
+// NewText creates a parentless CharData token containing character data.
+func NewText(text string) *CharData {
+ return newCharData(text, 0, nil)
+}
+
+// NewCData creates a parentless XML character CDATA section.
+func NewCData(data string) *CharData {
+ return newCharData(data, cdataFlag, nil)
+}
+
+// NewCharData creates a parentless CharData token containing character data.
+//
+// Deprecated: NewCharData is deprecated. Instead, use NewText, which does the
+// same thing.
+func NewCharData(data string) *CharData {
+ return newCharData(data, 0, nil)
+}
+
+// newCharData creates a character data token and binds it to a parent
+// element. If parent is nil, the CharData token remains unbound.
+func newCharData(data string, flags charDataFlags, parent *Element) *CharData {
+ c := &CharData{
+ Data: data,
+ parent: parent,
+ index: -1,
+ flags: flags,
+ }
+ if parent != nil {
+ parent.addChild(c)
+ }
+ return c
+}
+
+// CreateText creates a CharData token containing character data and adds it
+// as a child of element e.
+func (e *Element) CreateText(text string) *CharData {
+ return newCharData(text, 0, e)
+}
+
+// CreateCData creates a CharData token containing a CDATA section and adds it
+// as a child of element e.
+func (e *Element) CreateCData(data string) *CharData {
+ return newCharData(data, cdataFlag, e)
+}
+
+// CreateCharData creates a CharData token containing character data and adds
+// it as a child of element e.
+//
+// Deprecated: CreateCharData is deprecated. Instead, use CreateText, which
+// does the same thing.
+func (e *Element) CreateCharData(data string) *CharData {
+ return newCharData(data, 0, e)
+}
+
+// dup duplicates the character data.
+func (c *CharData) dup(parent *Element) Token {
+ return &CharData{
+ Data: c.Data,
+ flags: c.flags,
+ parent: parent,
+ index: c.index,
+ }
+}
+
+// IsCData returns true if the character data token is to be encoded as a
+// CDATA section.
+func (c *CharData) IsCData() bool {
+ return (c.flags & cdataFlag) != 0
+}
+
+// IsWhitespace returns true if the character data token was created by one of
+// the document Indent methods to contain only whitespace.
+func (c *CharData) IsWhitespace() bool {
+ return (c.flags & whitespaceFlag) != 0
+}
+
+// Parent returns the character data token's parent element, or nil if it has
+// no parent.
+func (c *CharData) Parent() *Element {
+ return c.parent
+}
+
+// Index returns the index of this CharData token within its parent element's
+// list of child tokens. If this CharData token has no parent element, the
+// index is -1.
+func (c *CharData) Index() int {
+ return c.index
+}
+
+// setParent replaces the character data token's parent.
+func (c *CharData) setParent(parent *Element) {
+ c.parent = parent
+}
+
+// setIndex sets the CharData token's index within its parent element's Child
+// slice.
+func (c *CharData) setIndex(index int) {
+ c.index = index
+}
+
+// writeTo serializes character data to the writer.
+func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) {
+ if c.IsCData() {
+ w.WriteString(``)
+ } else {
+ var m escapeMode
+ if s.CanonicalText {
+ m = escapeCanonicalText
+ } else {
+ m = escapeNormal
+ }
+ escapeString(w, c.Data, m)
+ }
+}
+
+// NewComment creates a parentless XML comment.
+func NewComment(comment string) *Comment {
+ return newComment(comment, nil)
+}
+
+// NewComment creates an XML comment and binds it to a parent element. If
+// parent is nil, the Comment remains unbound.
+func newComment(comment string, parent *Element) *Comment {
+ c := &Comment{
+ Data: comment,
+ parent: parent,
+ index: -1,
+ }
+ if parent != nil {
+ parent.addChild(c)
+ }
+ return c
+}
+
+// CreateComment creates an XML comment and adds it as a child of element e.
+func (e *Element) CreateComment(comment string) *Comment {
+ return newComment(comment, e)
+}
+
+// dup duplicates the comment.
+func (c *Comment) dup(parent *Element) Token {
+ return &Comment{
+ Data: c.Data,
+ parent: parent,
+ index: c.index,
+ }
+}
+
+// Parent returns comment token's parent element, or nil if it has no parent.
+func (c *Comment) Parent() *Element {
+ return c.parent
+}
+
+// Index returns the index of this Comment token within its parent element's
+// list of child tokens. If this Comment token has no parent element, the
+// index is -1.
+func (c *Comment) Index() int {
+ return c.index
+}
+
+// setParent replaces the comment token's parent.
+func (c *Comment) setParent(parent *Element) {
+ c.parent = parent
+}
+
+// setIndex sets the Comment token's index within its parent element's Child
+// slice.
+func (c *Comment) setIndex(index int) {
+ c.index = index
+}
+
+// writeTo serialies the comment to the writer.
+func (c *Comment) writeTo(w *bufio.Writer, s *WriteSettings) {
+ w.WriteString("")
+}
+
+// NewDirective creates a parentless XML directive.
+func NewDirective(data string) *Directive {
+ return newDirective(data, nil)
+}
+
+// newDirective creates an XML directive and binds it to a parent element. If
+// parent is nil, the Directive remains unbound.
+func newDirective(data string, parent *Element) *Directive {
+ d := &Directive{
+ Data: data,
+ parent: parent,
+ index: -1,
+ }
+ if parent != nil {
+ parent.addChild(d)
+ }
+ return d
+}
+
+// CreateDirective creates an XML directive and adds it as the last child of
+// element e.
+func (e *Element) CreateDirective(data string) *Directive {
+ return newDirective(data, e)
+}
+
+// dup duplicates the directive.
+func (d *Directive) dup(parent *Element) Token {
+ return &Directive{
+ Data: d.Data,
+ parent: parent,
+ index: d.index,
+ }
+}
+
+// Parent returns directive token's parent element, or nil if it has no
+// parent.
+func (d *Directive) Parent() *Element {
+ return d.parent
+}
+
+// Index returns the index of this Directive token within its parent element's
+// list of child tokens. If this Directive token has no parent element, the
+// index is -1.
+func (d *Directive) Index() int {
+ return d.index
+}
+
+// setParent replaces the directive token's parent.
+func (d *Directive) setParent(parent *Element) {
+ d.parent = parent
+}
+
+// setIndex sets the Directive token's index within its parent element's Child
+// slice.
+func (d *Directive) setIndex(index int) {
+ d.index = index
+}
+
+// writeTo serializes the XML directive to the writer.
+func (d *Directive) writeTo(w *bufio.Writer, s *WriteSettings) {
+ w.WriteString("")
+}
+
+// NewProcInst creates a parentless XML processing instruction.
+func NewProcInst(target, inst string) *ProcInst {
+ return newProcInst(target, inst, nil)
+}
+
+// newProcInst creates an XML processing instruction and binds it to a parent
+// element. If parent is nil, the ProcInst remains unbound.
+func newProcInst(target, inst string, parent *Element) *ProcInst {
+ p := &ProcInst{
+ Target: target,
+ Inst: inst,
+ parent: parent,
+ index: -1,
+ }
+ if parent != nil {
+ parent.addChild(p)
+ }
+ return p
+}
+
+// CreateProcInst creates a processing instruction and adds it as a child of
+// element e.
+func (e *Element) CreateProcInst(target, inst string) *ProcInst {
+ return newProcInst(target, inst, e)
+}
+
+// dup duplicates the procinst.
+func (p *ProcInst) dup(parent *Element) Token {
+ return &ProcInst{
+ Target: p.Target,
+ Inst: p.Inst,
+ parent: parent,
+ index: p.index,
+ }
+}
+
+// Parent returns processing instruction token's parent element, or nil if it
+// has no parent.
+func (p *ProcInst) Parent() *Element {
+ return p.parent
+}
+
+// Index returns the index of this ProcInst token within its parent element's
+// list of child tokens. If this ProcInst token has no parent element, the
+// index is -1.
+func (p *ProcInst) Index() int {
+ return p.index
+}
+
+// setParent replaces the processing instruction token's parent.
+func (p *ProcInst) setParent(parent *Element) {
+ p.parent = parent
+}
+
+// setIndex sets the processing instruction token's index within its parent
+// element's Child slice.
+func (p *ProcInst) setIndex(index int) {
+ p.index = index
+}
+
+// writeTo serializes the processing instruction to the writer.
+func (p *ProcInst) writeTo(w *bufio.Writer, s *WriteSettings) {
+ w.WriteString("")
+ w.WriteString(p.Target)
+ if p.Inst != "" {
+ w.WriteByte(' ')
+ w.WriteString(p.Inst)
+ }
+ w.WriteString("?>")
+}
diff --git a/vendor/github.com/beevik/etree/helpers.go b/vendor/github.com/beevik/etree/helpers.go
new file mode 100644
index 0000000000..825e14e914
--- /dev/null
+++ b/vendor/github.com/beevik/etree/helpers.go
@@ -0,0 +1,276 @@
+// Copyright 2015-2019 Brett Vickers.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package etree
+
+import (
+ "bufio"
+ "io"
+ "strings"
+ "unicode/utf8"
+)
+
+// A simple stack
+type stack struct {
+ data []interface{}
+}
+
+func (s *stack) empty() bool {
+ return len(s.data) == 0
+}
+
+func (s *stack) push(value interface{}) {
+ s.data = append(s.data, value)
+}
+
+func (s *stack) pop() interface{} {
+ value := s.data[len(s.data)-1]
+ s.data[len(s.data)-1] = nil
+ s.data = s.data[:len(s.data)-1]
+ return value
+}
+
+func (s *stack) peek() interface{} {
+ return s.data[len(s.data)-1]
+}
+
+// A fifo is a simple first-in-first-out queue.
+type fifo struct {
+ data []interface{}
+ head, tail int
+}
+
+func (f *fifo) add(value interface{}) {
+ if f.len()+1 >= len(f.data) {
+ f.grow()
+ }
+ f.data[f.tail] = value
+ if f.tail++; f.tail == len(f.data) {
+ f.tail = 0
+ }
+}
+
+func (f *fifo) remove() interface{} {
+ value := f.data[f.head]
+ f.data[f.head] = nil
+ if f.head++; f.head == len(f.data) {
+ f.head = 0
+ }
+ return value
+}
+
+func (f *fifo) len() int {
+ if f.tail >= f.head {
+ return f.tail - f.head
+ }
+ return len(f.data) - f.head + f.tail
+}
+
+func (f *fifo) grow() {
+ c := len(f.data) * 2
+ if c == 0 {
+ c = 4
+ }
+ buf, count := make([]interface{}, c), f.len()
+ if f.tail >= f.head {
+ copy(buf[0:count], f.data[f.head:f.tail])
+ } else {
+ hindex := len(f.data) - f.head
+ copy(buf[0:hindex], f.data[f.head:])
+ copy(buf[hindex:count], f.data[:f.tail])
+ }
+ f.data, f.head, f.tail = buf, 0, count
+}
+
+// countReader implements a proxy reader that counts the number of
+// bytes read from its encapsulated reader.
+type countReader struct {
+ r io.Reader
+ bytes int64
+}
+
+func newCountReader(r io.Reader) *countReader {
+ return &countReader{r: r}
+}
+
+func (cr *countReader) Read(p []byte) (n int, err error) {
+ b, err := cr.r.Read(p)
+ cr.bytes += int64(b)
+ return b, err
+}
+
+// countWriter implements a proxy writer that counts the number of
+// bytes written by its encapsulated writer.
+type countWriter struct {
+ w io.Writer
+ bytes int64
+}
+
+func newCountWriter(w io.Writer) *countWriter {
+ return &countWriter{w: w}
+}
+
+func (cw *countWriter) Write(p []byte) (n int, err error) {
+ b, err := cw.w.Write(p)
+ cw.bytes += int64(b)
+ return b, err
+}
+
+// isWhitespace returns true if the byte slice contains only
+// whitespace characters.
+func isWhitespace(s string) bool {
+ for i := 0; i < len(s); i++ {
+ if c := s[i]; c != ' ' && c != '\t' && c != '\n' && c != '\r' {
+ return false
+ }
+ }
+ return true
+}
+
+// spaceMatch returns true if namespace a is the empty string
+// or if namespace a equals namespace b.
+func spaceMatch(a, b string) bool {
+ switch {
+ case a == "":
+ return true
+ default:
+ return a == b
+ }
+}
+
+// spaceDecompose breaks a namespace:tag identifier at the ':'
+// and returns the two parts.
+func spaceDecompose(str string) (space, key string) {
+ colon := strings.IndexByte(str, ':')
+ if colon == -1 {
+ return "", str
+ }
+ return str[:colon], str[colon+1:]
+}
+
+// Strings used by indentCRLF and indentLF
+const (
+ indentSpaces = "\r\n "
+ indentTabs = "\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
+)
+
+// indentCRLF returns a CRLF newline followed by n copies of the first
+// non-CRLF character in the source string.
+func indentCRLF(n int, source string) string {
+ switch {
+ case n < 0:
+ return source[:2]
+ case n < len(source)-1:
+ return source[:n+2]
+ default:
+ return source + strings.Repeat(source[2:3], n-len(source)+2)
+ }
+}
+
+// indentLF returns a LF newline followed by n copies of the first non-LF
+// character in the source string.
+func indentLF(n int, source string) string {
+ switch {
+ case n < 0:
+ return source[1:2]
+ case n < len(source)-1:
+ return source[1 : n+2]
+ default:
+ return source[1:] + strings.Repeat(source[2:3], n-len(source)+2)
+ }
+}
+
+// nextIndex returns the index of the next occurrence of sep in s,
+// starting from offset. It returns -1 if the sep string is not found.
+func nextIndex(s, sep string, offset int) int {
+ switch i := strings.Index(s[offset:], sep); i {
+ case -1:
+ return -1
+ default:
+ return offset + i
+ }
+}
+
+// isInteger returns true if the string s contains an integer.
+func isInteger(s string) bool {
+ for i := 0; i < len(s); i++ {
+ if (s[i] < '0' || s[i] > '9') && !(i == 0 && s[i] == '-') {
+ return false
+ }
+ }
+ return true
+}
+
+type escapeMode byte
+
+const (
+ escapeNormal escapeMode = iota
+ escapeCanonicalText
+ escapeCanonicalAttr
+)
+
+// escapeString writes an escaped version of a string to the writer.
+func escapeString(w *bufio.Writer, s string, m escapeMode) {
+ var esc []byte
+ last := 0
+ for i := 0; i < len(s); {
+ r, width := utf8.DecodeRuneInString(s[i:])
+ i += width
+ switch r {
+ case '&':
+ esc = []byte("&")
+ case '<':
+ esc = []byte("<")
+ case '>':
+ if m == escapeCanonicalAttr {
+ continue
+ }
+ esc = []byte(">")
+ case '\'':
+ if m != escapeNormal {
+ continue
+ }
+ esc = []byte("'")
+ case '"':
+ if m == escapeCanonicalText {
+ continue
+ }
+ esc = []byte(""")
+ case '\t':
+ if m != escapeCanonicalAttr {
+ continue
+ }
+ esc = []byte(" ")
+ case '\n':
+ if m != escapeCanonicalAttr {
+ continue
+ }
+ esc = []byte("
")
+ case '\r':
+ if m == escapeNormal {
+ continue
+ }
+ esc = []byte("
")
+ default:
+ if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
+ esc = []byte("\uFFFD")
+ break
+ }
+ continue
+ }
+ w.WriteString(s[last : i-width])
+ w.Write(esc)
+ last = i
+ }
+ w.WriteString(s[last:])
+}
+
+func isInCharacterRange(r rune) bool {
+ return r == 0x09 ||
+ r == 0x0A ||
+ r == 0x0D ||
+ r >= 0x20 && r <= 0xD7FF ||
+ r >= 0xE000 && r <= 0xFFFD ||
+ r >= 0x10000 && r <= 0x10FFFF
+}
diff --git a/vendor/github.com/beevik/etree/path.go b/vendor/github.com/beevik/etree/path.go
new file mode 100644
index 0000000000..82db0ac556
--- /dev/null
+++ b/vendor/github.com/beevik/etree/path.go
@@ -0,0 +1,582 @@
+// Copyright 2015-2019 Brett Vickers.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package etree
+
+import (
+ "strconv"
+ "strings"
+)
+
+/*
+A Path is a string that represents a search path through an etree starting
+from the document root or an arbitrary element. Paths are used with the
+Element object's Find* methods to locate and return desired elements.
+
+A Path consists of a series of slash-separated "selectors", each of which may
+be modified by one or more bracket-enclosed "filters". Selectors are used to
+traverse the etree from element to element, while filters are used to narrow
+the list of candidate elements at each node.
+
+Although etree Path strings are similar to XPath strings
+(https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more limited set
+of selectors and filtering options.
+
+The following selectors are supported by etree Path strings:
+
+ . Select the current element.
+ .. Select the parent of the current element.
+ * Select all child elements of the current element.
+ / Select the root element when used at the start of a path.
+ // Select all descendants of the current element.
+ tag Select all child elements with a name matching the tag.
+
+The following basic filters are supported by etree Path strings:
+
+ [@attrib] Keep elements with an attribute named attrib.
+ [@attrib='val'] Keep elements with an attribute named attrib and value matching val.
+ [tag] Keep elements with a child element named tag.
+ [tag='val'] Keep elements with a child element named tag and text matching val.
+ [n] Keep the n-th element, where n is a numeric index starting from 1.
+
+The following function filters are also supported:
+
+ [text()] Keep elements with non-empty text.
+ [text()='val'] Keep elements whose text matches val.
+ [local-name()='val'] Keep elements whose un-prefixed tag matches val.
+ [name()='val'] Keep elements whose full tag exactly matches val.
+ [namespace-prefix()='val'] Keep elements whose namespace prefix matches val.
+ [namespace-uri()='val'] Keep elements whose namespace URI matches val.
+
+Here are some examples of Path strings:
+
+- Select the bookstore child element of the root element:
+ /bookstore
+
+- Beginning from the root element, select the title elements of all
+descendant book elements having a 'category' attribute of 'WEB':
+ //book[@category='WEB']/title
+
+- Beginning from the current element, select the first descendant
+book element with a title child element containing the text 'Great
+Expectations':
+ .//book[title='Great Expectations'][1]
+
+- Beginning from the current element, select all child elements of
+book elements with an attribute 'language' set to 'english':
+ ./book/*[@language='english']
+
+- Beginning from the current element, select all child elements of
+book elements containing the text 'special':
+ ./book/*[text()='special']
+
+- Beginning from the current element, select all descendant book
+elements whose title child element has a 'language' attribute of 'french':
+ .//book/title[@language='french']/..
+
+- Beginning from the current element, select all book elements
+belonging to the http://www.w3.org/TR/html4/ namespace:
+ .//book[namespace-uri()='http://www.w3.org/TR/html4/']
+
+*/
+type Path struct {
+ segments []segment
+}
+
+// ErrPath is returned by path functions when an invalid etree path is provided.
+type ErrPath string
+
+// Error returns the string describing a path error.
+func (err ErrPath) Error() string {
+ return "etree: " + string(err)
+}
+
+// CompilePath creates an optimized version of an XPath-like string that
+// can be used to query elements in an element tree.
+func CompilePath(path string) (Path, error) {
+ var comp compiler
+ segments := comp.parsePath(path)
+ if comp.err != ErrPath("") {
+ return Path{nil}, comp.err
+ }
+ return Path{segments}, nil
+}
+
+// MustCompilePath creates an optimized version of an XPath-like string that
+// can be used to query elements in an element tree. Panics if an error
+// occurs. Use this function to create Paths when you know the path is
+// valid (i.e., if it's hard-coded).
+func MustCompilePath(path string) Path {
+ p, err := CompilePath(path)
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
+
+// A segment is a portion of a path between "/" characters.
+// It contains one selector and zero or more [filters].
+type segment struct {
+ sel selector
+ filters []filter
+}
+
+func (seg *segment) apply(e *Element, p *pather) {
+ seg.sel.apply(e, p)
+ for _, f := range seg.filters {
+ f.apply(p)
+ }
+}
+
+// A selector selects XML elements for consideration by the
+// path traversal.
+type selector interface {
+ apply(e *Element, p *pather)
+}
+
+// A filter pares down a list of candidate XML elements based
+// on a path filter in [brackets].
+type filter interface {
+ apply(p *pather)
+}
+
+// A pather is helper object that traverses an element tree using
+// a Path object. It collects and deduplicates all elements matching
+// the path query.
+type pather struct {
+ queue fifo
+ results []*Element
+ inResults map[*Element]bool
+ candidates []*Element
+ scratch []*Element // used by filters
+}
+
+// A node represents an element and the remaining path segments that
+// should be applied against it by the pather.
+type node struct {
+ e *Element
+ segments []segment
+}
+
+func newPather() *pather {
+ return &pather{
+ results: make([]*Element, 0),
+ inResults: make(map[*Element]bool),
+ candidates: make([]*Element, 0),
+ scratch: make([]*Element, 0),
+ }
+}
+
+// traverse follows the path from the element e, collecting
+// and then returning all elements that match the path's selectors
+// and filters.
+func (p *pather) traverse(e *Element, path Path) []*Element {
+ for p.queue.add(node{e, path.segments}); p.queue.len() > 0; {
+ p.eval(p.queue.remove().(node))
+ }
+ return p.results
+}
+
+// eval evalutes the current path node by applying the remaining
+// path's selector rules against the node's element.
+func (p *pather) eval(n node) {
+ p.candidates = p.candidates[0:0]
+ seg, remain := n.segments[0], n.segments[1:]
+ seg.apply(n.e, p)
+
+ if len(remain) == 0 {
+ for _, c := range p.candidates {
+ if in := p.inResults[c]; !in {
+ p.inResults[c] = true
+ p.results = append(p.results, c)
+ }
+ }
+ } else {
+ for _, c := range p.candidates {
+ p.queue.add(node{c, remain})
+ }
+ }
+}
+
+// A compiler generates a compiled path from a path string.
+type compiler struct {
+ err ErrPath
+}
+
+// parsePath parses an XPath-like string describing a path
+// through an element tree and returns a slice of segment
+// descriptors.
+func (c *compiler) parsePath(path string) []segment {
+ // If path ends with //, fix it
+ if strings.HasSuffix(path, "//") {
+ path = path + "*"
+ }
+
+ var segments []segment
+
+ // Check for an absolute path
+ if strings.HasPrefix(path, "/") {
+ segments = append(segments, segment{new(selectRoot), []filter{}})
+ path = path[1:]
+ }
+
+ // Split path into segments
+ for _, s := range splitPath(path) {
+ segments = append(segments, c.parseSegment(s))
+ if c.err != ErrPath("") {
+ break
+ }
+ }
+ return segments
+}
+
+func splitPath(path string) []string {
+ pieces := make([]string, 0)
+ start := 0
+ inquote := false
+ for i := 0; i+1 <= len(path); i++ {
+ if path[i] == '\'' {
+ inquote = !inquote
+ } else if path[i] == '/' && !inquote {
+ pieces = append(pieces, path[start:i])
+ start = i + 1
+ }
+ }
+ return append(pieces, path[start:])
+}
+
+// parseSegment parses a path segment between / characters.
+func (c *compiler) parseSegment(path string) segment {
+ pieces := strings.Split(path, "[")
+ seg := segment{
+ sel: c.parseSelector(pieces[0]),
+ filters: []filter{},
+ }
+ for i := 1; i < len(pieces); i++ {
+ fpath := pieces[i]
+ if fpath[len(fpath)-1] != ']' {
+ c.err = ErrPath("path has invalid filter [brackets].")
+ break
+ }
+ seg.filters = append(seg.filters, c.parseFilter(fpath[:len(fpath)-1]))
+ }
+ return seg
+}
+
+// parseSelector parses a selector at the start of a path segment.
+func (c *compiler) parseSelector(path string) selector {
+ switch path {
+ case ".":
+ return new(selectSelf)
+ case "..":
+ return new(selectParent)
+ case "*":
+ return new(selectChildren)
+ case "":
+ return new(selectDescendants)
+ default:
+ return newSelectChildrenByTag(path)
+ }
+}
+
+var fnTable = map[string]struct {
+ hasFn func(e *Element) bool
+ getValFn func(e *Element) string
+}{
+ "local-name": {nil, (*Element).name},
+ "name": {nil, (*Element).FullTag},
+ "namespace-prefix": {nil, (*Element).namespacePrefix},
+ "namespace-uri": {nil, (*Element).NamespaceURI},
+ "text": {(*Element).hasText, (*Element).Text},
+}
+
+// parseFilter parses a path filter contained within [brackets].
+func (c *compiler) parseFilter(path string) filter {
+ if len(path) == 0 {
+ c.err = ErrPath("path contains an empty filter expression.")
+ return nil
+ }
+
+ // Filter contains [@attr='val'], [fn()='val'], or [tag='val']?
+ eqindex := strings.Index(path, "='")
+ if eqindex >= 0 {
+ rindex := nextIndex(path, "'", eqindex+2)
+ if rindex != len(path)-1 {
+ c.err = ErrPath("path has mismatched filter quotes.")
+ return nil
+ }
+
+ key := path[:eqindex]
+ value := path[eqindex+2 : rindex]
+
+ switch {
+ case key[0] == '@':
+ return newFilterAttrVal(key[1:], value)
+ case strings.HasSuffix(key, "()"):
+ fn := key[:len(key)-2]
+ if t, ok := fnTable[fn]; ok && t.getValFn != nil {
+ return newFilterFuncVal(t.getValFn, value)
+ }
+ c.err = ErrPath("path has unknown function " + fn)
+ return nil
+ default:
+ return newFilterChildText(key, value)
+ }
+ }
+
+ // Filter contains [@attr], [N], [tag] or [fn()]
+ switch {
+ case path[0] == '@':
+ return newFilterAttr(path[1:])
+ case strings.HasSuffix(path, "()"):
+ fn := path[:len(path)-2]
+ if t, ok := fnTable[fn]; ok && t.hasFn != nil {
+ return newFilterFunc(t.hasFn)
+ }
+ c.err = ErrPath("path has unknown function " + fn)
+ return nil
+ case isInteger(path):
+ pos, _ := strconv.Atoi(path)
+ switch {
+ case pos > 0:
+ return newFilterPos(pos - 1)
+ default:
+ return newFilterPos(pos)
+ }
+ default:
+ return newFilterChild(path)
+ }
+}
+
+// selectSelf selects the current element into the candidate list.
+type selectSelf struct{}
+
+func (s *selectSelf) apply(e *Element, p *pather) {
+ p.candidates = append(p.candidates, e)
+}
+
+// selectRoot selects the element's root node.
+type selectRoot struct{}
+
+func (s *selectRoot) apply(e *Element, p *pather) {
+ root := e
+ for root.parent != nil {
+ root = root.parent
+ }
+ p.candidates = append(p.candidates, root)
+}
+
+// selectParent selects the element's parent into the candidate list.
+type selectParent struct{}
+
+func (s *selectParent) apply(e *Element, p *pather) {
+ if e.parent != nil {
+ p.candidates = append(p.candidates, e.parent)
+ }
+}
+
+// selectChildren selects the element's child elements into the
+// candidate list.
+type selectChildren struct{}
+
+func (s *selectChildren) apply(e *Element, p *pather) {
+ for _, c := range e.Child {
+ if c, ok := c.(*Element); ok {
+ p.candidates = append(p.candidates, c)
+ }
+ }
+}
+
+// selectDescendants selects all descendant child elements
+// of the element into the candidate list.
+type selectDescendants struct{}
+
+func (s *selectDescendants) apply(e *Element, p *pather) {
+ var queue fifo
+ for queue.add(e); queue.len() > 0; {
+ e := queue.remove().(*Element)
+ p.candidates = append(p.candidates, e)
+ for _, c := range e.Child {
+ if c, ok := c.(*Element); ok {
+ queue.add(c)
+ }
+ }
+ }
+}
+
+// selectChildrenByTag selects into the candidate list all child
+// elements of the element having the specified tag.
+type selectChildrenByTag struct {
+ space, tag string
+}
+
+func newSelectChildrenByTag(path string) *selectChildrenByTag {
+ s, l := spaceDecompose(path)
+ return &selectChildrenByTag{s, l}
+}
+
+func (s *selectChildrenByTag) apply(e *Element, p *pather) {
+ for _, c := range e.Child {
+ if c, ok := c.(*Element); ok && spaceMatch(s.space, c.Space) && s.tag == c.Tag {
+ p.candidates = append(p.candidates, c)
+ }
+ }
+}
+
+// filterPos filters the candidate list, keeping only the
+// candidate at the specified index.
+type filterPos struct {
+ index int
+}
+
+func newFilterPos(pos int) *filterPos {
+ return &filterPos{pos}
+}
+
+func (f *filterPos) apply(p *pather) {
+ if f.index >= 0 {
+ if f.index < len(p.candidates) {
+ p.scratch = append(p.scratch, p.candidates[f.index])
+ }
+ } else {
+ if -f.index <= len(p.candidates) {
+ p.scratch = append(p.scratch, p.candidates[len(p.candidates)+f.index])
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterAttr filters the candidate list for elements having
+// the specified attribute.
+type filterAttr struct {
+ space, key string
+}
+
+func newFilterAttr(str string) *filterAttr {
+ s, l := spaceDecompose(str)
+ return &filterAttr{s, l}
+}
+
+func (f *filterAttr) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, a := range c.Attr {
+ if spaceMatch(f.space, a.Space) && f.key == a.Key {
+ p.scratch = append(p.scratch, c)
+ break
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterAttrVal filters the candidate list for elements having
+// the specified attribute with the specified value.
+type filterAttrVal struct {
+ space, key, val string
+}
+
+func newFilterAttrVal(str, value string) *filterAttrVal {
+ s, l := spaceDecompose(str)
+ return &filterAttrVal{s, l, value}
+}
+
+func (f *filterAttrVal) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, a := range c.Attr {
+ if spaceMatch(f.space, a.Space) && f.key == a.Key && f.val == a.Value {
+ p.scratch = append(p.scratch, c)
+ break
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterFunc filters the candidate list for elements satisfying a custom
+// boolean function.
+type filterFunc struct {
+ fn func(e *Element) bool
+}
+
+func newFilterFunc(fn func(e *Element) bool) *filterFunc {
+ return &filterFunc{fn}
+}
+
+func (f *filterFunc) apply(p *pather) {
+ for _, c := range p.candidates {
+ if f.fn(c) {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterFuncVal filters the candidate list for elements containing a value
+// matching the result of a custom function.
+type filterFuncVal struct {
+ fn func(e *Element) string
+ val string
+}
+
+func newFilterFuncVal(fn func(e *Element) string, value string) *filterFuncVal {
+ return &filterFuncVal{fn, value}
+}
+
+func (f *filterFuncVal) apply(p *pather) {
+ for _, c := range p.candidates {
+ if f.fn(c) == f.val {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterChild filters the candidate list for elements having
+// a child element with the specified tag.
+type filterChild struct {
+ space, tag string
+}
+
+func newFilterChild(str string) *filterChild {
+ s, l := spaceDecompose(str)
+ return &filterChild{s, l}
+}
+
+func (f *filterChild) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, cc := range c.Child {
+ if cc, ok := cc.(*Element); ok &&
+ spaceMatch(f.space, cc.Space) &&
+ f.tag == cc.Tag {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterChildText filters the candidate list for elements having
+// a child element with the specified tag and text.
+type filterChildText struct {
+ space, tag, text string
+}
+
+func newFilterChildText(str, text string) *filterChildText {
+ s, l := spaceDecompose(str)
+ return &filterChildText{s, l, text}
+}
+
+func (f *filterChildText) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, cc := range c.Child {
+ if cc, ok := cc.(*Element); ok &&
+ spaceMatch(f.space, cc.Space) &&
+ f.tag == cc.Tag &&
+ f.text == cc.Text() {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
diff --git a/vendor/github.com/ma314smith/signedxml/.gitignore b/vendor/github.com/ma314smith/signedxml/.gitignore
new file mode 100644
index 0000000000..daf913b1b3
--- /dev/null
+++ b/vendor/github.com/ma314smith/signedxml/.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
diff --git a/vendor/github.com/ma314smith/signedxml/.travis.yml b/vendor/github.com/ma314smith/signedxml/.travis.yml
new file mode 100644
index 0000000000..17364db595
--- /dev/null
+++ b/vendor/github.com/ma314smith/signedxml/.travis.yml
@@ -0,0 +1,14 @@
+language: go
+sudo: false
+
+go:
+ - 1.x
+ - master
+
+before_install:
+ - go get -v golang.org/x/lint/golint
+
+script:
+ - go vet ./...
+ - golint ./...
+ - go test -cover -v ./...
diff --git a/vendor/github.com/ma314smith/signedxml/LICENSE.md b/vendor/github.com/ma314smith/signedxml/LICENSE.md
new file mode 100644
index 0000000000..538c3831f8
--- /dev/null
+++ b/vendor/github.com/ma314smith/signedxml/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Matt Smith
+
+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/ma314smith/signedxml/README.md b/vendor/github.com/ma314smith/signedxml/README.md
new file mode 100644
index 0000000000..9e8dd08269
--- /dev/null
+++ b/vendor/github.com/ma314smith/signedxml/README.md
@@ -0,0 +1,111 @@
+## signedxml
+
+[](https://travis-ci.org/ma314smith/signedxml)
+[](https://godoc.org/github.com/ma314smith/signedxml)
+
+The signedxml package transforms and validates signed xml documents. The main use case is to support Single Sign On protocols like SAML and WS-Federation.
+
+Other packages that provide similar functionality rely on C libraries, which makes them difficult to run across platforms without significant configuration. `signedxml` is written in pure go, and can be easily used on any platform.
+
+### Install
+
+`go get github.com/ma314smith/signedxml`
+
+### Included Algorithms
+
+- Hashes
+ - http://www.w3.org/2001/04/xmldsig-more#md5
+ - http://www.w3.org/2000/09/xmldsig#sha1
+ - http://www.w3.org/2001/04/xmldsig-more#sha224
+ - http://www.w3.org/2001/04/xmlenc#sha256
+ - http://www.w3.org/2001/04/xmldsig-more#sha384
+ - http://www.w3.org/2001/04/xmlenc#sha512
+ - http://www.w3.org/2001/04/xmlenc#ripemd160
+
+
+- Signatures
+ - http://www.w3.org/2001/04/xmldsig-more#rsa-md2
+ - http://www.w3.org/2001/04/xmldsig-more#rsa-md5
+ - http://www.w3.org/2000/09/xmldsig#rsa-sha1
+ - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
+ - http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
+ - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
+ - http://www.w3.org/2000/09/xmldsig#dsa-sha1
+ - http://www.w3.org/2000/09/xmldsig#dsa-sha256
+ - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
+ - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
+ - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
+ - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
+
+
+- Canonicalization Methods/Transforms
+ - http://www.w3.org/2000/09/xmldsig#enveloped-signature
+ - http://www.w3.org/2001/10/xml-exc-c14n#
+ - http://www.w3.org/2001/10/xml-exc-c14n#WithComments
+
+### Examples
+
+#### Validating signed XML
+If your signed xml contains the signature and certificate, then you can just pass in the xml and call `Validate()`.
+```go
+validator, err := signedxml.NewValidator(``)
+xml, err = validator.ValidateReferences()
+```
+`ValidateReferences()` verifies the DigestValue and SignatureValue in the xml document, and returns the signed payload(s). If the error value is `nil`, then the signed xml is valid.
+
+The x509.Certificate that was successfully used to validate the xml will be available by calling:
+```go
+validator.SigningCert()
+```
+You can then verify that you trust the certificate. You can optionally supply your trusted certificates ahead of time by assigning them to the `Certificates` property of the `Validator` object, which is an x509.Certificate array.
+
+#### Using an external Signature
+If you need to specify an external Signature, you can use the `SetSignature()` function to assign it:
+```go
+validator.SetSignature(<`Signature>`)
+```
+
+#### Generating signed XML
+It is expected that your XML contains the Signature element with all the parameters set (except DigestValue and SignatureValue).
+```go
+signer, err := signedxml.NewSigner(` uri lookup
+ for _, attr := range node.Attr {
+ if attr.Space == "xmlns" {
+ e.namespaces[attr.Key] = attr.Value
+ }
+ }
+
+ // handle the namespace of the node itself
+ if node.Space != "" {
+ if !contains(prefixesInScope, node.Space) {
+ nsListToRender["xmlns:"+node.Space] = e.namespaces[node.Space]
+ prefixesInScope = append(prefixesInScope, node.Space)
+ }
+ } else if defaultNS != currentNS {
+ newDefaultNS = currentNS
+ elementAttributes = append(elementAttributes,
+ etree.Attr{Key: "xmlns", Value: currentNS})
+ }
+
+ for _, attr := range node.Attr {
+ // include the namespaces if they are in the inclusiveNamespacePrefixList
+ if attr.Space == "xmlns" {
+ if !contains(prefixesInScope, attr.Key) &&
+ contains(e.inclusiveNamespacePrefixList, attr.Key) {
+
+ nsListToRender["xmlns:"+attr.Key] = attr.Value
+ prefixesInScope = append(prefixesInScope, attr.Key)
+ }
+ }
+
+ // include namespaces for qualfied attributes
+ if attr.Space != "" &&
+ attr.Space != "xmlns" &&
+ !contains(prefixesInScope, attr.Space) {
+
+ nsListToRender["xmlns:"+attr.Space] = e.namespaces[attr.Space]
+ prefixesInScope = append(prefixesInScope, attr.Space)
+ }
+
+ // inclued all non-namespace attributes
+ if attr.Space != "xmlns" && attr.Key != "xmlns" {
+ attrListToRender = append(attrListToRender,
+ attribute{
+ prefix: attr.Space,
+ uri: e.namespaces[attr.Space],
+ key: attr.Key,
+ value: attr.Value,
+ })
+ }
+ }
+
+ // sort and add the namespace attributes first
+ sortedNSList := getSortedNamespaces(nsListToRender)
+ elementAttributes = append(elementAttributes, sortedNSList...)
+ // then sort and add the non-namespace attributes
+ sortedAttributes := getSortedAttributes(attrListToRender)
+ elementAttributes = append(elementAttributes, sortedAttributes...)
+ // replace the nodes attributes with the sorted copy
+ node.Attr = elementAttributes
+ return currentNS, prefixesInScope
+}
+
+func contains(slice []string, value string) bool {
+ for _, s := range slice {
+ if s == value {
+ return true
+ }
+ }
+ return false
+}
+
+// getSortedNamespaces sorts the namespace attributes by their prefix
+func getSortedNamespaces(list map[string]string) []etree.Attr {
+ var keys []string
+ for k := range list {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ elem := etree.Element{}
+ for _, k := range keys {
+ elem.CreateAttr(k, list[k])
+ }
+
+ return elem.Attr
+}
+
+// getSortedAttributes sorts attributes by their namespace URIs
+func getSortedAttributes(list attributes) []etree.Attr {
+ sort.Sort(list)
+ attrs := make([]etree.Attr, len(list))
+ for i, a := range list {
+ attrs[i] = etree.Attr{
+ Space: a.prefix,
+ Key: a.key,
+ Value: a.value,
+ }
+ }
+ return attrs
+}
+
+func removeTokenFromElement(token etree.Token, e *etree.Element) *etree.Token {
+ for i, t := range e.Child {
+ if t == token {
+ e.Child = append(e.Child[0:i], e.Child[i+1:]...)
+ return &t
+ }
+ }
+ return nil
+}
+
+func removeTokenFromDocument(token etree.Token, d *etree.Document) *etree.Token {
+ for i, t := range d.Child {
+ if t == token {
+ d.Child = append(d.Child[0:i], d.Child[i+1:]...)
+ return &t
+ }
+ }
+ return nil
+}
+
+// isWhitespace returns true if the byte slice contains only
+// whitespace characters.
+func isWhitespace(s string) bool {
+ for i := 0; i < len(s); i++ {
+ if c := s[i]; c != ' ' && c != '\t' && c != '\n' && c != '\r' {
+ return false
+ }
+ }
+ return true
+}
diff --git a/vendor/github.com/ma314smith/signedxml/signedxml.go b/vendor/github.com/ma314smith/signedxml/signedxml.go
new file mode 100644
index 0000000000..f14578c86f
--- /dev/null
+++ b/vendor/github.com/ma314smith/signedxml/signedxml.go
@@ -0,0 +1,335 @@
+// Package signedxml transforms and validates signedxml documents
+package signedxml
+
+import (
+ "crypto"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/beevik/etree"
+)
+
+var logger = log.New(os.Stdout, "DEBUG-SIGNEDXML: ", log.Ldate|log.Ltime|log.Lshortfile)
+
+func init() {
+ hashAlgorithms = map[string]crypto.Hash{
+ "http://www.w3.org/2001/04/xmldsig-more#md5": crypto.MD5,
+ "http://www.w3.org/2000/09/xmldsig#sha1": crypto.SHA1,
+ "http://www.w3.org/2001/04/xmldsig-more#sha224": crypto.SHA224,
+ "http://www.w3.org/2001/04/xmlenc#sha256": crypto.SHA256,
+ "http://www.w3.org/2001/04/xmldsig-more#sha384": crypto.SHA384,
+ "http://www.w3.org/2001/04/xmlenc#sha512": crypto.SHA512,
+ "http://www.w3.org/2001/04/xmlenc#ripemd160": crypto.RIPEMD160,
+ }
+
+ signatureAlgorithms = map[string]x509.SignatureAlgorithm{
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-md2": x509.MD2WithRSA,
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-md5": x509.MD5WithRSA,
+ "http://www.w3.org/2000/09/xmldsig#rsa-sha1": x509.SHA1WithRSA,
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": x509.SHA256WithRSA,
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384": x509.SHA384WithRSA,
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": x509.SHA512WithRSA,
+ "http://www.w3.org/2000/09/xmldsig#dsa-sha1": x509.DSAWithSHA1,
+ "http://www.w3.org/2000/09/xmldsig#dsa-sha256": x509.DSAWithSHA256,
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1": x509.ECDSAWithSHA1,
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": x509.ECDSAWithSHA256,
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384": x509.ECDSAWithSHA384,
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512": x509.ECDSAWithSHA512,
+ }
+
+ CanonicalizationAlgorithms = map[string]CanonicalizationAlgorithm{
+ "http://www.w3.org/2000/09/xmldsig#enveloped-signature": EnvelopedSignature{},
+ "http://www.w3.org/2001/10/xml-exc-c14n#": ExclusiveCanonicalization{},
+ "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": ExclusiveCanonicalization{WithComments: true},
+ }
+}
+
+// CanonicalizationAlgorithm defines an interface for processing an XML
+// document into a standard format.
+//
+// If any child elements are in the Transform node, the entire transform node
+// will be passed to the Process method through the transformXML parameter as an
+// XML string. This is necessary for transforms that need additional processing
+// data, like XPath (http://www.w3.org/TR/xmldsig-core/#sec-XPath). If there are
+// no child elements in Transform (or CanonicalizationMethod), then an empty
+// string will be passed through.
+type CanonicalizationAlgorithm interface {
+ Process(inputXML string, transformXML string) (outputXML string, err error)
+}
+
+// CanonicalizationAlgorithms maps the CanonicalizationMethod or
+// Transform Algorithm URIs to a type that implements the
+// CanonicalizationAlgorithm interface.
+//
+// Implementations are provided for the following transforms:
+// http://www.w3.org/2001/10/xml-exc-c14n# (ExclusiveCanonicalization)
+// http://www.w3.org/2001/10/xml-exc-c14n#WithComments (ExclusiveCanonicalizationWithComments)
+// http://www.w3.org/2000/09/xmldsig#enveloped-signature (EnvelopedSignature)
+//
+// Custom implementations can be added to the map
+var CanonicalizationAlgorithms map[string]CanonicalizationAlgorithm
+var hashAlgorithms map[string]crypto.Hash
+var signatureAlgorithms map[string]x509.SignatureAlgorithm
+
+// signatureData provides options for verifying a signed XML document
+type signatureData struct {
+ xml *etree.Document
+ signature *etree.Element
+ signedInfo *etree.Element
+ sigValue string
+ sigAlgorithm x509.SignatureAlgorithm
+ canonAlgorithm CanonicalizationAlgorithm
+ refIDAttribute string
+}
+
+// SetSignature can be used to assign an external signature for the XML doc
+// that Validator will verify
+func (s *signatureData) SetSignature(sig string) error {
+ doc := etree.NewDocument()
+ err := doc.ReadFromString(sig)
+ s.signature = doc.Root()
+ return err
+}
+
+func (s *signatureData) parseEnvelopedSignature() error {
+ sig := s.xml.FindElement(".//Signature")
+ if sig != nil {
+ s.signature = sig
+ } else {
+ return errors.New("signedxml: Unable to find a unique signature element " +
+ "in the xml document. The signature must either be enveloped in the " +
+ "xml doc or externally assigned to Validator.SetSignature")
+ }
+ return nil
+}
+
+func (s *signatureData) parseSignedInfo() error {
+ s.signedInfo = nil
+ s.signedInfo = s.signature.SelectElement("SignedInfo")
+ if s.signedInfo == nil {
+ return errors.New("signedxml: unable to find SignedInfo element")
+ }
+
+ // move the Signature level namespace down to SignedInfo so that the signature
+ // value will match up
+ if s.signedInfo.Space != "" {
+ attr := s.signature.SelectAttr(s.signedInfo.Space)
+ if attr != nil {
+ s.signedInfo.Attr = []etree.Attr{*attr}
+ }
+ } else {
+ attr := s.signature.SelectAttr("xmlns")
+ if attr != nil {
+ s.signedInfo.Attr = []etree.Attr{*attr}
+ }
+ }
+
+ // Copy SignedInfo xmlns: into itself if it does not exist and is defined as a root attribute
+ root := s.xml.Root()
+
+ if root != nil {
+ sigNS := root.SelectAttr("xmlns:" + s.signedInfo.Space)
+ if sigNS != nil {
+ if s.signedInfo.SelectAttr("xmlns:"+s.signedInfo.Space) == nil {
+ s.signedInfo.CreateAttr("xmlns:"+s.signedInfo.Space, sigNS.Value)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *signatureData) parseSigValue() error {
+ s.sigValue = ""
+ sigValueElement := s.signature.SelectElement("SignatureValue")
+ if sigValueElement != nil {
+ s.sigValue = sigValueElement.Text()
+ return nil
+ }
+ return errors.New("signedxml: unable to find SignatureValue")
+}
+
+func (s *signatureData) parseSigAlgorithm() error {
+ s.sigAlgorithm = x509.UnknownSignatureAlgorithm
+ sigMethod := s.signedInfo.SelectElement("SignatureMethod")
+
+ var sigAlgoURI string
+ if sigMethod == nil {
+ return errors.New("signedxml: Unable to find SignatureMethod element")
+ }
+
+ sigAlgoURI = sigMethod.SelectAttrValue("Algorithm", "")
+ if sigAlgoURI == "" {
+ return errors.New("signedxml: Unable to find Algorithm in " +
+ "SignatureMethod element")
+ }
+
+ sigAlgo, ok := signatureAlgorithms[sigAlgoURI]
+ if ok {
+ s.sigAlgorithm = sigAlgo
+ return nil
+ }
+
+ return errors.New("signedxml: Unsupported Algorithm " + sigAlgoURI + " in " +
+ "SignatureMethod")
+}
+
+func (s *signatureData) parseCanonAlgorithm() error {
+ s.canonAlgorithm = nil
+ canonMethod := s.signedInfo.SelectElement("CanonicalizationMethod")
+
+ var canonAlgoURI string
+ if canonMethod == nil {
+ return errors.New("signedxml: Unable to find CanonicalizationMethod element")
+ }
+
+ canonAlgoURI = canonMethod.SelectAttrValue("Algorithm", "")
+ if canonAlgoURI == "" {
+ return errors.New("signedxml: Unable to find Algorithm in " +
+ "CanonicalizationMethod element")
+ }
+
+ canonAlgo, ok := CanonicalizationAlgorithms[canonAlgoURI]
+ if ok {
+ s.canonAlgorithm = canonAlgo
+ return nil
+ }
+
+ return errors.New("signedxml: Unsupported Algorithm " + canonAlgoURI + " in " +
+ "CanonicalizationMethod")
+}
+
+func (s *signatureData) getReferencedXML(reference *etree.Element, inputDoc *etree.Document) (outputDoc *etree.Document, err error) {
+ uri := reference.SelectAttrValue("URI", "")
+ uri = strings.Replace(uri, "#", "", 1)
+ // populate doc with the referenced xml from the Reference URI
+ if uri == "" {
+ outputDoc = inputDoc
+ } else {
+ refIDAttribute := "ID"
+ if s.refIDAttribute != "" {
+ refIDAttribute = s.refIDAttribute
+ }
+ path := fmt.Sprintf(".//[@%s='%s']", refIDAttribute, uri)
+ e := inputDoc.FindElement(path)
+ if e != nil {
+ outputDoc = etree.NewDocument()
+ outputDoc.SetRoot(e.Copy())
+ } else {
+ // SAML v1.1 Assertions use AssertionID
+ path := fmt.Sprintf(".//[@AssertionID='%s']", uri)
+ e := inputDoc.FindElement(path)
+ if e != nil {
+ outputDoc = etree.NewDocument()
+ outputDoc.SetRoot(e.Copy())
+ }
+ }
+ }
+
+ if outputDoc == nil {
+ return nil, errors.New("signedxml: unable to find refereced xml")
+ }
+
+ return outputDoc, nil
+}
+
+func getCertFromPEMString(pemString string) (*x509.Certificate, error) {
+ pubkey := fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----",
+ pemString)
+
+ pemBlock, _ := pem.Decode([]byte(pubkey))
+ if pemBlock == nil {
+ return &x509.Certificate{}, errors.New("Could not parse Public Key PEM")
+ }
+ if pemBlock.Type != "PUBLIC KEY" {
+ return &x509.Certificate{}, errors.New("Found wrong key type")
+ }
+
+ cert, err := x509.ParseCertificate(pemBlock.Bytes)
+ return cert, err
+}
+
+func processTransform(transform *etree.Element,
+ docIn *etree.Document) (docOut *etree.Document, err error) {
+
+ transformAlgoURI := transform.SelectAttrValue("Algorithm", "")
+ if transformAlgoURI == "" {
+ return nil, errors.New("signedxml: unable to find Algorithm in Transform")
+ }
+
+ transformAlgo, ok := CanonicalizationAlgorithms[transformAlgoURI]
+ if !ok {
+ return nil, fmt.Errorf("signedxml: unable to find matching transform"+
+ "algorithm for %s in CanonicalizationAlgorithms", transformAlgoURI)
+ }
+
+ var transformContent string
+
+ if transform.ChildElements() != nil {
+ tDoc := etree.NewDocument()
+ tDoc.SetRoot(transform.Copy())
+ transformContent, err = tDoc.WriteToString()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ docString, err := docIn.WriteToString()
+ if err != nil {
+ return nil, err
+ }
+
+ docString, err = transformAlgo.Process(docString, transformContent)
+ if err != nil {
+ return nil, err
+ }
+
+ docOut = etree.NewDocument()
+ docOut.ReadFromString(docString)
+
+ return docOut, nil
+}
+
+func calculateHash(reference *etree.Element, doc *etree.Document) (string, error) {
+ digestMethodElement := reference.SelectElement("DigestMethod")
+ if digestMethodElement == nil {
+ return "", errors.New("signedxml: unable to find DigestMethod")
+ }
+
+ digestMethodURI := digestMethodElement.SelectAttrValue("Algorithm", "")
+ if digestMethodURI == "" {
+ return "", errors.New("signedxml: unable to find Algorithm in DigestMethod")
+ }
+
+ digestAlgo, ok := hashAlgorithms[digestMethodURI]
+ if !ok {
+ return "", fmt.Errorf("signedxml: unable to find matching hash"+
+ "algorithm for %s in hashAlgorithms", digestMethodURI)
+ }
+
+ doc.WriteSettings.CanonicalEndTags = true
+ doc.WriteSettings.CanonicalText = true
+ doc.WriteSettings.CanonicalAttrVal = true
+
+ h := digestAlgo.New()
+ docBytes, err := doc.WriteToBytes()
+ if err != nil {
+ return "", err
+ }
+
+ // ioutil.WriteFile("C:/Temp/SignedXML/Suspect.xml", docBytes, 0644)
+ // s, _ := doc.WriteToString()
+ // logger.Println(s)
+
+ h.Write(docBytes)
+ d := h.Sum(nil)
+ calculatedValue := base64.StdEncoding.EncodeToString(d)
+
+ return calculatedValue, nil
+}
diff --git a/vendor/github.com/ma314smith/signedxml/signer.go b/vendor/github.com/ma314smith/signedxml/signer.go
new file mode 100644
index 0000000000..d9abe473bc
--- /dev/null
+++ b/vendor/github.com/ma314smith/signedxml/signer.go
@@ -0,0 +1,176 @@
+package signedxml
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/base64"
+ "errors"
+
+ "github.com/beevik/etree"
+)
+
+var signingAlgorithms map[x509.SignatureAlgorithm]cryptoHash
+
+func init() {
+ signingAlgorithms = map[x509.SignatureAlgorithm]cryptoHash{
+ // MD2 not supported
+ // x509.MD2WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.MD2},
+ x509.MD5WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.MD5},
+ x509.SHA1WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA1},
+ x509.SHA256WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA256},
+ x509.SHA384WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA384},
+ x509.SHA512WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA512},
+ // DSA not supported
+ // x509.DSAWithSHA1: cryptoHash{algorithm: "dsa", hash: crypto.SHA1},
+ // x509.DSAWithSHA256:cryptoHash{algorithm: "dsa", hash: crypto.SHA256},
+ // Golang ECDSA support is lacking, can't seem to load private keys
+ // x509.ECDSAWithSHA1: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA1},
+ // x509.ECDSAWithSHA256: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA256},
+ // x509.ECDSAWithSHA384: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA384},
+ // x509.ECDSAWithSHA512: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA512},
+ }
+}
+
+type cryptoHash struct {
+ algorithm string
+ hash crypto.Hash
+}
+
+// Signer provides options for signing an XML document
+type Signer struct {
+ signatureData
+ privateKey interface{}
+}
+
+// NewSigner returns a *Signer for the XML provided
+func NewSigner(xml string) (*Signer, error) {
+ doc := etree.NewDocument()
+ err := doc.ReadFromString(xml)
+ if err != nil {
+ return nil, err
+ }
+ s := &Signer{signatureData: signatureData{xml: doc}}
+ return s, nil
+}
+
+// Sign populates the XML digest and signature based on the parameters present and privateKey given
+func (s *Signer) Sign(privateKey interface{}) (string, error) {
+ s.privateKey = privateKey
+
+ if s.signature == nil {
+ if err := s.parseEnvelopedSignature(); err != nil {
+ return "", err
+ }
+ }
+ if err := s.parseSignedInfo(); err != nil {
+ return "", err
+ }
+ if err := s.parseSigAlgorithm(); err != nil {
+ return "", err
+ }
+ if err := s.parseCanonAlgorithm(); err != nil {
+ return "", err
+ }
+ if err := s.setDigest(); err != nil {
+ return "", err
+ }
+ if err := s.setSignature(); err != nil {
+ return "", err
+ }
+
+ xml, err := s.xml.WriteToString()
+ if err != nil {
+ return "", err
+ }
+ return xml, nil
+}
+
+// SetReferenceIDAttribute set the referenceIDAttribute
+func (s *Signer) SetReferenceIDAttribute(refIDAttribute string) {
+ s.signatureData.refIDAttribute = refIDAttribute
+}
+
+func (s *Signer) setDigest() (err error) {
+ references := s.signedInfo.FindElements("./Reference")
+ for _, ref := range references {
+ doc := s.xml.Copy()
+ transforms := ref.SelectElement("Transforms")
+ for _, transform := range transforms.SelectElements("Transform") {
+ doc, err = processTransform(transform, doc)
+ if err != nil {
+ return err
+ }
+ }
+
+ doc, err := s.getReferencedXML(ref, doc)
+ if err != nil {
+ return err
+ }
+
+ calculatedValue, err := calculateHash(ref, doc)
+ if err != nil {
+ return err
+ }
+
+ digestValueElement := ref.SelectElement("DigestValue")
+ if digestValueElement == nil {
+ return errors.New("signedxml: unable to find DigestValue")
+ }
+ digestValueElement.SetText(calculatedValue)
+ }
+ return nil
+}
+
+func (s *Signer) setSignature() error {
+ doc := etree.NewDocument()
+ doc.SetRoot(s.signedInfo.Copy())
+ signedInfo, err := doc.WriteToString()
+ if err != nil {
+ return err
+ }
+
+ canonSignedInfo, err := s.canonAlgorithm.Process(signedInfo, "")
+ if err != nil {
+ return err
+ }
+
+ var hashed, signature []byte
+ //var h1, h2 *big.Int
+ signingAlgorithm, ok := signingAlgorithms[s.sigAlgorithm]
+ if !ok {
+ return errors.New("signedxml: unsupported algorithm")
+ }
+
+ hasher := signingAlgorithm.hash.New()
+ hasher.Write([]byte(canonSignedInfo))
+ hashed = hasher.Sum(nil)
+
+ switch signingAlgorithm.algorithm {
+ case "rsa":
+ signature, err = rsa.SignPKCS1v15(rand.Reader, s.privateKey.(*rsa.PrivateKey), signingAlgorithm.hash, hashed)
+ /*
+ case "dsa":
+ h1, h2, err = dsa.Sign(rand.Reader, s.privateKey.(*dsa.PrivateKey), hashed)
+ case "ecdsa":
+ h1, h2, err = ecdsa.Sign(rand.Reader, s.privateKey.(*ecdsa.PrivateKey), hashed)
+ */
+ }
+ if err != nil {
+ return err
+ }
+
+ // DSA and ECDSA has not been validated
+ /*
+ if signature == nil && h1 != nil && h2 != nil {
+ signature = append(h1.Bytes(), h2.Bytes()...)
+ }
+ */
+
+ b64 := base64.StdEncoding.EncodeToString(signature)
+ sigValueElement := s.signature.SelectElement("SignatureValue")
+ sigValueElement.SetText(b64)
+
+ return nil
+}
diff --git a/vendor/github.com/ma314smith/signedxml/validator.go b/vendor/github.com/ma314smith/signedxml/validator.go
new file mode 100644
index 0000000000..eb1358096a
--- /dev/null
+++ b/vendor/github.com/ma314smith/signedxml/validator.go
@@ -0,0 +1,205 @@
+package signedxml
+
+import (
+ "crypto/x509"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log"
+
+ "github.com/beevik/etree"
+)
+
+// Validator provides options for verifying a signed XML document
+type Validator struct {
+ Certificates []x509.Certificate
+ signingCert x509.Certificate
+ signatureData
+}
+
+// NewValidator returns a *Validator for the XML provided
+func NewValidator(xml string) (*Validator, error) {
+ doc := etree.NewDocument()
+ err := doc.ReadFromString(xml)
+ if err != nil {
+ return nil, err
+ }
+ v := &Validator{signatureData: signatureData{xml: doc}}
+ return v, nil
+}
+
+// SetReferenceIDAttribute set the referenceIDAttribute
+func (v *Validator) SetReferenceIDAttribute(refIDAttribute string) {
+ v.signatureData.refIDAttribute = refIDAttribute
+}
+
+// SetXML is used to assign the XML document that the Validator will verify
+func (v *Validator) SetXML(xml string) error {
+ doc := etree.NewDocument()
+ err := doc.ReadFromString(xml)
+ v.xml = doc
+ return err
+}
+
+// SigningCert returns the certificate, if any, that was used to successfully
+// validate the signature of the XML document. This will be a zero value
+// x509.Certificate before Validator.Validate is successfully called.
+func (v *Validator) SigningCert() x509.Certificate {
+ return v.signingCert
+}
+
+// Validate validates the Reference digest values, and the signature value
+// over the SignedInfo.
+//
+// Deprecated: Use ValidateReferences instead
+func (v *Validator) Validate() error {
+ _, err := v.ValidateReferences()
+ return err
+}
+
+// ValidateReferences validates the Reference digest values, and the signature value
+// over the SignedInfo.
+//
+// If the signature is enveloped in the XML, then it will be used.
+// Otherwise, an external signature should be assigned using
+// Validator.SetSignature.
+//
+// The references returned by this method can be used to verify what was signed.
+func (v *Validator) ValidateReferences() ([]string, error) {
+ if err := v.loadValuesFromXML(); err != nil {
+ return nil, err
+ }
+
+ referenced, err := v.validateReferences()
+ if err != nil {
+ return nil, err
+ }
+
+ var ref []string
+ for _, doc := range referenced {
+ docStr, err := doc.WriteToString()
+ if err != nil {
+ return nil, err
+ }
+ ref = append(ref, docStr)
+ }
+
+ err = v.validateSignature()
+ return ref, err
+}
+
+func (v *Validator) loadValuesFromXML() error {
+ if v.signature == nil {
+ if err := v.parseEnvelopedSignature(); err != nil {
+ return err
+ }
+ }
+ if err := v.parseSignedInfo(); err != nil {
+ return err
+ }
+ if err := v.parseSigValue(); err != nil {
+ return err
+ }
+ if err := v.parseSigAlgorithm(); err != nil {
+ return err
+ }
+ if err := v.parseCanonAlgorithm(); err != nil {
+ return err
+ }
+ if err := v.loadCertificates(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (v *Validator) validateReferences() (referenced []*etree.Document, err error) {
+ references := v.signedInfo.FindElements("./Reference")
+ for _, ref := range references {
+ doc := v.xml.Copy()
+ transforms := ref.SelectElement("Transforms")
+ for _, transform := range transforms.SelectElements("Transform") {
+ doc, err = processTransform(transform, doc)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ doc, err = v.getReferencedXML(ref, doc)
+ if err != nil {
+ return nil, err
+ }
+
+ referenced = append(referenced, doc)
+
+ digestValueElement := ref.SelectElement("DigestValue")
+ if digestValueElement == nil {
+ return nil, errors.New("signedxml: unable to find DigestValue")
+ }
+ digestValue := digestValueElement.Text()
+
+ calculatedValue, err := calculateHash(ref, doc)
+ if err != nil {
+ return nil, err
+ }
+
+ if calculatedValue != digestValue {
+ return nil, fmt.Errorf("signedxml: Calculated digest does not match the"+
+ " expected digestvalue of %s", digestValue)
+ }
+ }
+ return referenced, nil
+}
+
+func (v *Validator) validateSignature() error {
+ doc := etree.NewDocument()
+ doc.SetRoot(v.signedInfo.Copy())
+ signedInfo, err := doc.WriteToString()
+ if err != nil {
+ return err
+ }
+
+ canonSignedInfo, err := v.canonAlgorithm.Process(signedInfo, "")
+ if err != nil {
+ return err
+ }
+
+ b64, err := base64.StdEncoding.DecodeString(v.sigValue)
+ if err != nil {
+ return err
+ }
+ sig := []byte(b64)
+
+ v.signingCert = x509.Certificate{}
+ for _, cert := range v.Certificates {
+ err := cert.CheckSignature(v.sigAlgorithm, []byte(canonSignedInfo), sig)
+ if err == nil {
+ v.signingCert = cert
+ return nil
+ }
+ }
+
+ return errors.New("signedxml: Calculated signature does not match the " +
+ "SignatureValue provided")
+}
+
+func (v *Validator) loadCertificates() error {
+ // If v.Certificates is already populated, then the client has already set it
+ // to the desired cert. Otherwise, let's pull the public keys from the XML
+ if len(v.Certificates) < 1 {
+ keydata := v.xml.FindElements(".//X509Certificate")
+ for _, key := range keydata {
+ cert, err := getCertFromPEMString(key.Text())
+ if err != nil {
+ log.Printf("signedxml: Unable to load certificate: (%s). "+
+ "Looking for another cert.", err)
+ } else {
+ v.Certificates = append(v.Certificates, *cert)
+ }
+ }
+ }
+
+ if len(v.Certificates) < 1 {
+ return errors.New("signedxml: a certificate is required, but was not found")
+ }
+ return nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index c7598a083c..eaf00a1055 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -158,6 +158,8 @@ github.com/aws/aws-sdk-go/service/iam
github.com/aws/aws-sdk-go/service/s3
github.com/aws/aws-sdk-go/service/sts
github.com/aws/aws-sdk-go/service/sts/stsiface
+# github.com/beevik/etree v1.1.0
+github.com/beevik/etree
# github.com/benbjohnson/clock v1.0.0
github.com/benbjohnson/clock
# github.com/beorn7/perks v1.0.0
@@ -393,6 +395,8 @@ github.com/kr/pty
github.com/leodido/go-urn
# github.com/libvirt/libvirt-go-xml v5.2.0+incompatible
github.com/libvirt/libvirt-go-xml
+# github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60
+github.com/ma314smith/signedxml
# github.com/mattn/go-colorable v0.1.2
github.com/mattn/go-colorable
# github.com/mattn/go-isatty v0.0.9