~samwhited/xmpp

1be5398bc8b98ec8fff34abefc55de85ac9228be — Sam Whited 5 years ago 710a0f9
Add experimental (internal) SASL errors package
2 files changed, 276 insertions(+), 0 deletions(-)

A internal/saslerr/errors.go
A internal/saslerr/errors_test.go
A internal/saslerr/errors.go => internal/saslerr/errors.go +143 -0
@@ 0,0 1,143 @@
// Copyright 2016 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

// Package saslerr provides error conditions for the XMPP profile of SASL as
// defined by RFC 6120 §6.5.
package saslerr

// TODO(ssw): I think these errors should really be created via code generation
//            in case more are added in the future and so that we can store them
//            in a more efficient way that doesn't require a giant switch.

import (
	"encoding/xml"

	"golang.org/x/text/language"
)

// condition represents a SASL error condition that can be encapsulated by a
// <failure/> element.
type condition string

const (
	Aborted              condition = "aborted"
	AccountDisabled      condition = "account-disabled"
	CredentialsExpired   condition = "credentials-expired"
	EncryptionRequired   condition = "encryption-required"
	IncorrectEncoding    condition = "incorrect-encoding"
	InvalidAuthzID       condition = "invalid-authzid"
	InvalidMechanism     condition = "invalid-mechanism"
	MalformedRequest     condition = "malformed-request"
	MechanismTooWeak     condition = "mechanism-too-weak"
	NotAuthorized        condition = "not-authorized"
	TemporaryAuthFailure condition = "temporary-auth-failure"
)

// Failure represents a SASL error that is marshalable to XML.
type Failure struct {
	Condition condition
	Lang      language.Tag
	Text      string
}

// Error satisfies the error interface for a Failure. It returns the text string
// if set, or the condition otherwise.
func (f Failure) Error() string {
	if f.Text != "" {
		return f.Text
	}
	return string(f.Condition)
}

// MarshalXML satisfies the xml.Marshaler interface for a Failure.
func (f Failure) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	failure := xml.StartElement{
		Name: xml.Name{Space: `urn:ietf:params:xml:ns:xmpp-sasl`, Local: "failure"},
	}
	e.EncodeToken(failure)
	condition := xml.StartElement{
		Name: xml.Name{Space: "", Local: string(f.Condition)},
	}
	e.EncodeToken(condition)
	e.EncodeToken(condition.End())
	if f.Text != "" {
		text := xml.StartElement{
			Name: xml.Name{Space: "", Local: "text"},
			Attr: []xml.Attr{
				xml.Attr{
					Name:  xml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"},
					Value: f.Lang.String(),
				},
			},
		}
		e.EncodeToken(text)
		e.EncodeToken(xml.CharData(f.Text))
		e.EncodeToken(text.End())
	}
	e.EncodeToken(failure.End())
	return nil
}

// UnmarshalXML satisfies the xml.Unmarshaler interface for a Failure. If
// multiple text elements are present in the XML and the Failure struct already
// has a language tag set, UnmarshalXML selects the text element with an
// xml:lang attribute that most closely matches the features language tag. If no
// language tag is present, UnmarshalXML selects a text element with an xml:lang
// attribute of "und" if present, behavior is undefined otherwise (it will pick
// the tag that most closely matches "und", whatever that means).
func (f *Failure) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	decoded := struct {
		Condition struct {
			XMLName xml.Name
		} `xml:",any"`
		Text []struct {
			Lang string `xml:"http://www.w3.org/XML/1998/namespace lang,attr"`
			Data string `xml:",chardata"`
		} `xml:"text"`
	}{}
	if err := d.DecodeElement(&decoded, &start); err != nil {
		return err
	}
	switch decoded.Condition.XMLName.Local {
	case "not-authorized":
		f.Condition = NotAuthorized
	case "aborted":
		f.Condition = Aborted
	case "account-disabled":
		f.Condition = AccountDisabled
	case "credentials-expired":
		f.Condition = CredentialsExpired
	case "encryption-required":
		f.Condition = EncryptionRequired
	case "incorrect-encoding":
		f.Condition = IncorrectEncoding
	case "invalid-authzid":
		f.Condition = InvalidAuthzID
	case "invalid-mechanism":
		f.Condition = InvalidMechanism
	case "malformed-request":
		f.Condition = MalformedRequest
	case "mechanism-too-weak":
		f.Condition = MechanismTooWeak
	case "temporary-auth-failure":
		f.Condition = TemporaryAuthFailure
	default:
		f.Condition = condition(decoded.Condition.XMLName.Local)
	}
	tags := make([]language.Tag, 0, len(decoded.Text))
	data := make(map[language.Tag]string)
	for _, text := range decoded.Text {
		// Parse the language tag, skipping any that cannot be parsed.
		tag, err := language.Parse(text.Lang)
		if err != nil {
			continue
		}
		tags = append(tags, tag)
		data[tag] = text.Data
	}
	tag, _, _ := language.NewMatcher(tags).Match(f.Lang)
	f.Lang = tag
	f.Text, _ = data[tag]
	return nil
}

A internal/saslerr/errors_test.go => internal/saslerr/errors_test.go +133 -0
@@ 0,0 1,133 @@
// Copyright 2016 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package saslerr

import (
	"encoding/xml"
	"testing"

	"golang.org/x/text/language"
)

func TestErrorTextOrCondition(t *testing.T) {
	f := Failure{
		Condition: MechanismTooWeak,
		Text:      "Test",
		Lang:      language.CanadianFrench,
	}
	if f.Error() != f.Text {
		t.Error("Expected Error() to return the value of Text")
	}
	f = Failure{
		Condition: MechanismTooWeak,
	}
	if f.Error() != string(f.Condition) {
		t.Error("Expected Error() to return the value of Condition if no text")
	}
}

func TestMarshalCondition(t *testing.T) {
	for _, test := range []struct {
		Failure   Failure
		Marshaled string
	}{
		{
			Failure{
				Condition: MechanismTooWeak,
				Text:      "Test",
				Lang:      language.BrazilianPortuguese,
			},
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism-too-weak></mechanism-too-weak><text xml:lang="pt-BR">Test</text></failure>`,
		},
		{Failure{Condition: IncorrectEncoding}, `<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><incorrect-encoding></incorrect-encoding></failure>`},
		{Failure{Condition: Aborted, Lang: language.Polish}, `<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><aborted></aborted></failure>`},
	} {
		b, err := xml.Marshal(test.Failure)
		if err != nil {
			t.Fatal(err)
		}
		if string(b) != test.Marshaled {
			t.Errorf("Expected %s but got %s", test.Marshaled, b)
		}
	}
}

func TestUnmarshalCondition(t *testing.T) {
	for _, test := range []struct {
		XML         string
		IntoFailure Failure
		Failure     Failure
		Err         bool
	}{
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><temporary-auth-failure></temporary-auth-failure></failure>`,
			Failure{}, Failure{Condition: TemporaryAuthFailure}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism-too-weak></mechanism-too-weak><text xml:lang="pt-BR">Test</text></failure>`,
			Failure{}, Failure{Lang: language.BrazilianPortuguese, Text: "Test", Condition: MechanismTooWeak}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><malformed-request></malformed-request><text xml:lang="pt-BR">pt-BR</text><text xml:lang="en-US">en-US</text></failure>`,
			Failure{Lang: language.English}, Failure{Lang: language.AmericanEnglish, Text: "en-US", Condition: MalformedRequest}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><invalid-mechanism></invalid-mechanism><text xml:lang="NOPE">NO</text></failure>`,
			Failure{}, Failure{Condition: InvalidMechanism}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><invalid-authzid></invalid-authzid><text xml:lang="pt-BR">TEXT</text><text xml:lang="NOPE">NO</text></failure>`,
			Failure{Lang: language.English}, Failure{Lang: language.BrazilianPortuguese, Text: "TEXT", Condition: InvalidAuthzID}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><wat></wat></failure>`,
			Failure{}, Failure{Condition: condition("wat")}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><nope></wat></failure>`,
			Failure{}, Failure{}, true,
		},
		// The following test cases are really just for branch coverage in the big
		// switch; it should be simplified eventually so that they are not
		// necessary:
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><incorrect-encoding></incorrect-encoding></failure>`,
			Failure{}, Failure{Condition: IncorrectEncoding}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><encryption-required></encryption-required></failure>`,
			Failure{}, Failure{Condition: EncryptionRequired}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><credentials-expired></credentials-expired></failure>`,
			Failure{}, Failure{Condition: CredentialsExpired}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><account-disabled></account-disabled></failure>`,
			Failure{}, Failure{Condition: AccountDisabled}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><aborted></aborted></failure>`,
			Failure{}, Failure{Condition: Aborted}, false,
		},
		{
			`<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized></not-authorized></failure>`,
			Failure{}, Failure{Condition: NotAuthorized}, false,
		},
	} {
		err := xml.Unmarshal([]byte(test.XML), &test.IntoFailure)
		switch {
		case test.Err && err == nil:
			t.Fatal("Expected unmarshal to error")
		case !test.Err && err != nil:
			t.Fatal(err)
		case err != nil:
			continue
		}
		if test.IntoFailure != test.Failure {
			t.Errorf("Expected failure %#v but got %#v", test.Failure, test.IntoFailure)
		}
	}
}