go-xmpp/xmpp.go

643 lines
16 KiB
Go
Raw Normal View History

2011-02-27 18:44:24 -08:00
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(rsc):
// More precise error handling.
// Presence functionality.
// TODO(mattn):
// Add proxy authentication.
// Package xmpp implements a simple Google Talk client
// using the XMPP protocol described in RFC 3920 and RFC 3921.
package xmpp
import (
"bufio"
"bytes"
2013-05-14 19:24:35 -07:00
"crypto/md5"
"crypto/rand"
2011-02-27 18:44:24 -08:00
"crypto/tls"
"encoding/base64"
2011-11-09 06:18:51 -08:00
"encoding/xml"
2011-11-04 06:40:10 -07:00
"errors"
2011-02-27 18:44:24 -08:00
"fmt"
"io"
"log"
2013-05-14 19:24:35 -07:00
"math/big"
2011-02-27 18:44:24 -08:00
"net"
2011-11-09 06:18:51 -08:00
"net/http"
"net/url"
2011-02-27 18:44:24 -08:00
"os"
"strings"
)
const (
nsStream = "http://etherx.jabber.org/streams"
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
nsClient = "jabber:client"
)
var DefaultConfig tls.Config
type Client struct {
conn net.Conn // connection to server
jid string // Jabber ID for our connection
2013-05-14 19:24:35 -07:00
domain string
p *xml.Decoder
2011-02-27 18:44:24 -08:00
}
2013-05-14 19:24:35 -07:00
func connect(host, user, passwd string) (net.Conn, error) {
2011-02-27 18:44:24 -08:00
addr := host
if strings.TrimSpace(host) == "" {
2011-06-27 18:53:04 -07:00
a := strings.SplitN(user, "@", 2)
2011-02-27 18:44:24 -08:00
if len(a) == 2 {
host = a[1]
}
}
2011-06-27 18:53:04 -07:00
a := strings.SplitN(host, ":", 2)
2011-02-27 18:44:24 -08:00
if len(a) == 1 {
host += ":5222"
}
proxy := os.Getenv("HTTP_PROXY")
if proxy == "" {
proxy = os.Getenv("http_proxy")
}
if proxy != "" {
2011-09-28 12:26:19 -07:00
url, err := url.Parse(proxy)
2011-02-27 18:44:24 -08:00
if err == nil {
addr = url.Host
}
}
2011-04-05 02:31:59 -07:00
c, err := net.Dial("tcp", addr)
2011-02-27 18:44:24 -08:00
if err != nil {
return nil, err
}
if proxy != "" {
fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host)
fmt.Fprintf(c, "Host: %s\r\n", host)
fmt.Fprintf(c, "\r\n")
br := bufio.NewReader(c)
2011-05-17 17:31:47 -07:00
req, _ := http.NewRequest("CONNECT", host, nil)
resp, err := http.ReadResponse(br, req)
2011-02-27 18:44:24 -08:00
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
2011-06-27 18:53:04 -07:00
f := strings.SplitN(resp.Status, " ", 2)
2011-11-04 06:40:10 -07:00
return nil, errors.New(f[1])
2011-02-27 18:44:24 -08:00
}
}
2013-05-14 19:24:35 -07:00
return c, nil
}
// Options are used to specify additional options for new clients, such as a Resource.
type Options struct {
// Host specifies what host to connect to, as either "hostname" or "hostname:port"
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
Host string
// User specifies what user to authenticate to the remote server.
User string
// Password supplies the password to use for authentication with the remote server.
Password string
// Resource specifies an XMPP client resource, like "bot", instead of accepting one
// from the server. Use "" to let the server generate one for your client.
Resource string
// NoTLS disables TLS and specifies that a plain old unencrypted TCP connection should
// be used.
NoTLS bool
2013-10-18 00:49:41 -07:00
// Debug output
Debug bool
}
// NewClient establishes a new Client connection based on a set of Options.
func (o Options) NewClient() (*Client, error) {
host := o.Host
c, err := connect(host, o.User, o.Password)
2013-05-14 19:24:35 -07:00
if err != nil {
return nil, err
}
2011-02-27 18:44:24 -08:00
client := new(Client)
if o.NoTLS {
client.conn = c
} else {
tlsconn := tls.Client(c, &DefaultConfig)
if err = tlsconn.Handshake(); err != nil {
return nil, err
}
if strings.LastIndex(o.Host, ":") > 0 {
host = host[:strings.LastIndex(o.Host, ":")]
}
if err = tlsconn.VerifyHostname(host); err != nil {
return nil, err
}
client.conn = tlsconn
2011-02-27 18:44:24 -08:00
}
if err := client.init(&o); err != nil {
2013-05-14 19:24:35 -07:00
client.Close()
return nil, err
}
2013-05-14 19:24:35 -07:00
return client, nil
}
// NewClient creates a new connection to a host given as "hostname" or "hostname:port".
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
2013-10-18 00:49:41 -07:00
func NewClient(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
2013-10-18 00:49:41 -07:00
Debug: debug,
2013-05-14 19:24:35 -07:00
}
return opts.NewClient()
}
2013-05-14 19:24:35 -07:00
2013-10-18 00:49:41 -07:00
func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
NoTLS: true,
2013-10-18 00:49:41 -07:00
Debug: debug,
2011-02-27 18:44:24 -08:00
}
return opts.NewClient()
2011-02-27 18:44:24 -08:00
}
2011-11-04 06:40:10 -07:00
func (c *Client) Close() error {
2013-05-14 19:24:35 -07:00
return c.conn.Close()
}
func saslDigestResponse(username, realm, passwd, nonce, cnonceStr,
authenticate, digestUri, nonceCountStr string) string {
h := func(text string) []byte {
h := md5.New()
h.Write([]byte(text))
return h.Sum(nil)
}
hex := func(bytes []byte) string {
return fmt.Sprintf("%x", bytes)
}
kd := func(secret, data string) []byte {
return h(secret + ":" + data)
}
a1 := string(h(username+":"+realm+":"+passwd)) + ":" +
nonce + ":" + cnonceStr
a2 := authenticate + ":" + digestUri
response := hex(kd(hex(h(a1)), nonce+":"+
nonceCountStr+":"+cnonceStr+":auth:"+
hex(h(a2))))
return response
2013-05-14 19:24:35 -07:00
}
func cnonce() string {
randSize := big.NewInt(0)
randSize.Lsh(big.NewInt(1), 64)
cn, err := rand.Int(rand.Reader, randSize)
if err != nil {
return ""
}
return fmt.Sprintf("%016x", cn)
2011-02-27 18:44:24 -08:00
}
func (c *Client) init(o *Options) error {
2013-05-14 19:24:35 -07:00
c.p = xml.NewDecoder(c.conn)
2013-10-18 00:49:41 -07:00
// For debugging: the following causes the plaintext of the connection to be duplicated to stdout.
if o.Debug {
c.p = xml.NewDecoder(tee{c.conn, os.Stdout})
}
2011-02-27 18:44:24 -08:00
a := strings.SplitN(o.User, "@", 2)
2011-02-27 18:44:24 -08:00
if len(a) != 2 {
return errors.New("xmpp: invalid username (want user@domain): " + o.User)
2011-02-27 18:44:24 -08:00
}
user := a[0]
2011-02-27 18:44:24 -08:00
domain := a[1]
// Declare intent to be a jabber client.
2013-05-14 19:24:35 -07:00
fmt.Fprintf(c.conn, "<?xml version='1.0'?>\n"+
2011-02-27 18:44:24 -08:00
"<stream:stream to='%s' xmlns='%s'\n"+
" xmlns:stream='%s' version='1.0'>\n",
xmlEscape(domain), nsClient, nsStream)
// Server should respond with a stream opening.
se, err := nextStart(c.p)
if err != nil {
return err
}
if se.Name.Space != nsStream || se.Name.Local != "stream" {
2011-11-04 06:40:10 -07:00
return errors.New("xmpp: expected <stream> but got <" + se.Name.Local + "> in " + se.Name.Space)
2011-02-27 18:44:24 -08:00
}
// Now we're in the stream and can use Unmarshal.
// Next message should be <features> to tell us authentication options.
// See section 4.6 in RFC 3920.
var f streamFeatures
2012-02-08 21:44:44 -08:00
if err = c.p.DecodeElement(&f, nil); err != nil {
2011-11-04 06:40:10 -07:00
return errors.New("unmarshal <features>: " + err.Error())
2011-02-27 18:44:24 -08:00
}
2013-05-14 19:24:35 -07:00
mechanism := ""
2011-02-27 18:44:24 -08:00
for _, m := range f.Mechanisms.Mechanism {
if m == "PLAIN" {
2013-05-14 19:24:35 -07:00
mechanism = m
// Plain authentication: send base64-encoded \x00 user \x00 password.
raw := "\x00" + user + "\x00" + o.Password
2013-05-14 19:24:35 -07:00
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n",
nsSASL, enc)
break
}
if m == "DIGEST-MD5" {
mechanism = m
// Digest-MD5 authentication
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='DIGEST-MD5'/>\n",
nsSASL)
var ch saslChallenge
if err = c.p.DecodeElement(&ch, nil); err != nil {
return errors.New("unmarshal <challenge>: " + err.Error())
}
b, err := base64.StdEncoding.DecodeString(string(ch))
if err != nil {
return err
}
tokens := map[string]string{}
for _, token := range strings.Split(string(b), ",") {
kv := strings.SplitN(strings.TrimSpace(token), "=", 2)
if len(kv) == 2 {
if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' {
kv[1] = kv[1][1 : len(kv[1])-1]
2013-05-14 19:24:35 -07:00
}
tokens[kv[0]] = kv[1]
}
}
realm, _ := tokens["realm"]
nonce, _ := tokens["nonce"]
qop, _ := tokens["qop"]
charset, _ := tokens["charset"]
cnonceStr := cnonce()
digestUri := "xmpp/" + domain
nonceCount := fmt.Sprintf("%08x", 1)
digest := saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", digestUri, nonceCount)
message := "username=" + user + ", realm=" + realm + ", nonce=" + nonce + ", cnonce=" + cnonceStr + ", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=" + digestUri + ", response=" + digest + ", charset=" + charset
2013-05-14 19:24:35 -07:00
fmt.Fprintf(c.conn, "<response xmlns='%s'>%s</response>\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message)))
var rspauth saslRspAuth
if err = c.p.DecodeElement(&rspauth, nil); err != nil {
return errors.New("unmarshal <challenge>: " + err.Error())
}
b, err = base64.StdEncoding.DecodeString(string(rspauth))
if err != nil {
return err
}
fmt.Fprintf(c.conn, "<response xmlns='%s'/>\n", nsSASL)
2011-02-27 18:44:24 -08:00
break
}
}
2013-05-14 19:24:35 -07:00
if mechanism == "" {
2011-11-04 06:40:10 -07:00
return errors.New(fmt.Sprintf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism))
2011-02-27 18:44:24 -08:00
}
// Next message should be either success or failure.
name, val, err := next(c.p)
2013-11-03 23:13:55 -08:00
if err != nil {
return err
}
2011-02-27 18:44:24 -08:00
switch v := val.(type) {
case *saslSuccess:
case *saslFailure:
// v.Any is type of sub-element in failure,
// which gives a description of what failed.
2011-11-04 06:40:10 -07:00
return errors.New("auth failure: " + v.Any.Local)
2011-02-27 18:44:24 -08:00
default:
2011-11-04 06:40:10 -07:00
return errors.New("expected <success> or <failure>, got <" + name.Local + "> in " + name.Space)
2011-02-27 18:44:24 -08:00
}
// Now that we're authenticated, we're supposed to start the stream over again.
// Declare intent to be a jabber client.
2013-05-14 19:24:35 -07:00
fmt.Fprintf(c.conn, "<stream:stream to='%s' xmlns='%s'\n"+
2011-02-27 18:44:24 -08:00
" xmlns:stream='%s' version='1.0'>\n",
xmlEscape(domain), nsClient, nsStream)
// Here comes another <stream> and <features>.
se, err = nextStart(c.p)
if err != nil {
return err
}
if se.Name.Space != nsStream || se.Name.Local != "stream" {
2011-11-04 06:40:10 -07:00
return errors.New("expected <stream>, got <" + se.Name.Local + "> in " + se.Name.Space)
2011-02-27 18:44:24 -08:00
}
2012-02-08 21:44:44 -08:00
if err = c.p.DecodeElement(&f, nil); err != nil {
// TODO: often stream stop.
2011-06-27 00:36:11 -07:00
//return os.NewError("unmarshal <features>: " + err.String())
2011-02-27 18:44:24 -08:00
}
// Send IQ message asking to bind to the local user name.
if o.Resource == "" {
fmt.Fprintf(c.conn, "<iq type='set' id='x'><bind xmlns='%s'></bind></iq>\n", nsBind)
} else {
fmt.Fprintf(c.conn, "<iq type='set' id='x'><bind xmlns='%s'><resource>%s</resource></bind></iq>\n", nsBind, o.Resource)
}
2011-02-27 18:44:24 -08:00
var iq clientIQ
2012-02-08 21:44:44 -08:00
if err = c.p.DecodeElement(&iq, nil); err != nil {
2011-11-04 06:40:10 -07:00
return errors.New("unmarshal <iq>: " + err.Error())
2011-02-27 18:44:24 -08:00
}
if &iq.Bind == nil {
2011-11-04 06:40:10 -07:00
return errors.New("<iq> result missing <bind>")
2011-02-27 18:44:24 -08:00
}
c.jid = iq.Bind.Jid // our local id
// We're connected and can now receive and send messages.
2013-05-14 19:24:35 -07:00
fmt.Fprintf(c.conn, "<presence xml:lang='en'><show>xa</show><status>I for one welcome our new codebot overlords.</status></presence>")
2011-02-27 18:44:24 -08:00
return nil
}
type Chat struct {
Remote string
Type string
Text string
Other []string
2011-02-27 18:44:24 -08:00
}
2013-01-18 16:48:50 -08:00
type Presence struct {
From string
To string
Type string
Show string
}
2011-02-27 18:44:24 -08:00
// Recv wait next token of chat.
2013-01-18 16:48:50 -08:00
func (c *Client) Recv() (event interface{}, err error) {
2011-02-27 18:44:24 -08:00
for {
_, val, err := next(c.p)
if err != nil {
return Chat{}, err
}
2013-01-18 16:48:50 -08:00
switch v := val.(type) {
case *clientMessage:
return Chat{v.From, v.Type, v.Body, v.Other}, nil
2013-01-18 16:48:50 -08:00
case *clientPresence:
return Presence{v.From, v.To, v.Type, v.Show}, nil
2011-02-27 18:44:24 -08:00
}
}
panic("unreachable")
}
// Send sends message text.
func (c *Client) Send(chat Chat) {
2013-05-14 19:24:35 -07:00
fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+
2011-02-27 18:44:24 -08:00
"<body>%s</body></message>",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
2011-02-27 18:44:24 -08:00
}
2013-10-18 00:52:01 -07:00
// Send origin
func (c *Client) SendOrg(org string) {
fmt.Fprint(c.conn, org)
}
2011-02-27 18:44:24 -08:00
// RFC 3920 C.1 Streams name space
type streamFeatures struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
StartTLS tlsStartTLS
Mechanisms saslMechanisms
Bind bindBind
2011-02-27 18:44:24 -08:00
Session bool
}
type streamError struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
2011-02-27 18:44:24 -08:00
Any xml.Name
Text string
}
// RFC 3920 C.3 TLS name space
type tlsStartTLS struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:":ietf:params:xml:ns:xmpp-tls starttls"`
2011-02-27 18:44:24 -08:00
Required bool
}
type tlsProceed struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
2011-02-27 18:44:24 -08:00
}
type tlsFailure struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
2011-02-27 18:44:24 -08:00
}
// RFC 3920 C.4 SASL name space
type saslMechanisms struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
2012-04-03 09:46:01 -07:00
Mechanism []string `xml:"mechanism"`
2011-02-27 18:44:24 -08:00
}
type saslAuth struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
2012-04-03 09:46:01 -07:00
Mechanism string `xml:",attr"`
2011-02-27 18:44:24 -08:00
}
type saslChallenge string
2013-05-14 19:24:35 -07:00
type saslRspAuth string
2011-02-27 18:44:24 -08:00
type saslResponse string
type saslAbort struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl abort"`
2011-02-27 18:44:24 -08:00
}
type saslSuccess struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
2011-02-27 18:44:24 -08:00
}
type saslFailure struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
2011-02-27 18:44:24 -08:00
Any xml.Name
}
// RFC 3920 C.5 Resource binding name space
type bindBind struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
2011-02-27 18:44:24 -08:00
Resource string
Jid string `xml:"jid"`
2011-02-27 18:44:24 -08:00
}
// RFC 3921 B.1 jabber:client
type clientMessage struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"jabber:client message"`
From string `xml:"from,attr"`
Id string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // chat, error, groupchat, headline, or normal
2011-02-27 18:44:24 -08:00
// These should technically be []clientText,
// but string is much more convenient.
Subject string `xml:"subject"`
Body string `xml:"body"`
Thread string `xml:"thread"`
// Any hasn't matched element
Other []string `xml:",any"`
2011-02-27 18:44:24 -08:00
}
type clientText struct {
2012-04-03 09:46:01 -07:00
Lang string `xml:",attr"`
2011-09-28 12:26:19 -07:00
Body string `xml:"chardata"`
2011-02-27 18:44:24 -08:00
}
type clientPresence struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"jabber:client presence"`
2012-06-16 19:47:48 -07:00
From string `xml:"from,attr"`
Id string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, probe, subscribe, subscribed, unavailable, unsubscribe, unsubscribed
Lang string `xml:"lang,attr"`
2011-02-27 18:44:24 -08:00
2013-01-18 16:48:50 -08:00
Show string `xml:"show"` // away, chat, dnd, xa
Status string `xml:"status,attr"` // sb []clientText
Priority string `xml:"priority,attr"`
2011-02-27 18:44:24 -08:00
Error *clientError
}
type clientIQ struct { // info/query
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"jabber:client iq"`
2012-04-03 09:46:01 -07:00
From string `xml:",attr"`
Id string `xml:",attr"`
To string `xml:",attr"`
Type string `xml:",attr"` // error, get, result, set
Error clientError
Bind bindBind
2011-02-27 18:44:24 -08:00
}
type clientError struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"jabber:client error"`
2012-04-03 09:46:01 -07:00
Code string `xml:",attr"`
Type string `xml:",attr"`
2011-02-27 18:44:24 -08:00
Any xml.Name
Text string
}
// Scan XML token stream to find next StartElement.
2012-02-08 21:44:44 -08:00
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
2011-02-27 18:44:24 -08:00
for {
t, err := p.Token()
2013-11-03 23:13:55 -08:00
if err != nil && err != io.EOF {
return nil, err
2011-02-27 18:44:24 -08:00
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
panic("unreachable")
}
// Scan XML token stream for next element and save into val.
// If val == nil, allocate new element based on proto map.
// Either way, return val.
2012-02-08 21:44:44 -08:00
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
2011-02-27 18:44:24 -08:00
// Read start element to find out what type we want.
se, err := nextStart(p)
if err != nil {
return xml.Name{}, nil, err
}
// Put it in an interface and allocate one.
var nv interface{}
2011-11-04 06:40:10 -07:00
switch se.Name.Space + " " + se.Name.Local {
case nsStream + " features":
nv = &streamFeatures{}
case nsStream + " error":
nv = &streamError{}
case nsTLS + " starttls":
nv = &tlsStartTLS{}
case nsTLS + " proceed":
nv = &tlsProceed{}
case nsTLS + " failure":
nv = &tlsFailure{}
case nsSASL + " mechanisms":
nv = &saslMechanisms{}
case nsSASL + " challenge":
nv = ""
case nsSASL + " response":
nv = ""
case nsSASL + " abort":
nv = &saslAbort{}
case nsSASL + " success":
nv = &saslSuccess{}
case nsSASL + " failure":
nv = &saslFailure{}
case nsBind + " bind":
nv = &bindBind{}
case nsClient + " message":
nv = &clientMessage{}
case nsClient + " presence":
nv = &clientPresence{}
case nsClient + " iq":
nv = &clientIQ{}
case nsClient + " error":
nv = &clientError{}
default:
2011-11-04 06:40:10 -07:00
return xml.Name{}, nil, errors.New("unexpected XMPP message " +
2011-02-27 18:44:24 -08:00
se.Name.Space + " <" + se.Name.Local + "/>")
}
// Unmarshal into that storage.
2012-02-08 21:44:44 -08:00
if err = p.DecodeElement(nv, &se); err != nil {
2011-02-27 18:44:24 -08:00
return xml.Name{}, nil, err
}
return se.Name, nv, err
2011-02-27 18:44:24 -08:00
}
var xmlSpecial = map[byte]string{
'<': "&lt;",
'>': "&gt;",
'"': "&quot;",
'\'': "&apos;",
'&': "&amp;",
}
func xmlEscape(s string) string {
var b bytes.Buffer
for i := 0; i < len(s); i++ {
c := s[i]
if s, ok := xmlSpecial[c]; ok {
b.WriteString(s)
} else {
b.WriteByte(c)
}
}
return b.String()
}
type tee struct {
r io.Reader
w io.Writer
}
2011-11-04 06:40:10 -07:00
func (t tee) Read(p []byte) (n int, err error) {
2011-02-27 18:44:24 -08:00
n, err = t.r.Read(p)
if n > 0 {
t.w.Write(p[0:n])
2013-10-18 00:49:41 -07:00
t.w.Write([]byte("\n"))
2011-02-27 18:44:24 -08:00
}
return
}