Use reflection instead of constructor functions to create extended stanza structures.
// 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.// This file contains the three layers of processing for the// communication with the server: transport (where TLS happens), XML// (where strings are converted to go structures), and Stream (where// we respond to XMPP events on behalf of the library client), or send// those events to the client.packagexmppimport("crypto/md5""crypto/rand""crypto/tls""encoding/base64""encoding/xml""fmt""io""math/big""net""reflect""regexp""strings""time")// Callback to handle a stanza with a particular id.typestanzaHandlerstruct{idstring// Return true means pass this to the applicationffunc(Stanza)bool}func(cl*Client)readTransport(wio.WriteCloser){deferw.Close()p:=make([]byte,1024)for{ifcl.socket==nil{cl.waitForSocket()}cl.socket.SetReadDeadline(time.Now().Add(time.Second))nr,err:=cl.socket.Read(p)ifnr==0{iferrno,ok:=err.(*net.OpError);ok{iferrno.Timeout(){continue}}Warn.Logf("read: %s",err)break}nw,err:=w.Write(p[:nr])ifnw<nr{Warn.Logf("read: %s",err)break}}}func(cl*Client)writeTransport(rio.Reader){defercl.socket.Close()p:=make([]byte,1024)for{nr,err:=r.Read(p)ifnr==0{Warn.Logf("write: %s",err)break}nw,err:=cl.socket.Write(p[:nr])ifnw<nr{Warn.Logf("write: %s",err)break}}}funcreadXml(rio.Reader,chchan<-interface{},extStanzamap[xml.Name]reflect.Type){if_,ok:=Debug.(*noLog);!ok{pr,pw:=io.Pipe()gotee(r,pw,"S: ")r=pr}deferclose(ch)// This trick loads our namespaces into the parser.nsstr:=fmt.Sprintf(`<a xmlns="%s" xmlns:stream="%s">`,NsClient,NsStream)nsrdr:=strings.NewReader(nsstr)p:=xml.NewDecoder(io.MultiReader(nsrdr,r))p.Token()Loop:for{// Sniff the next token on the stream.t,err:=p.Token()ift==nil{iferr!=io.EOF{Warn.Logf("read: %s",err)}break}varsexml.StartElementvarokboolifse,ok=t.(xml.StartElement);!ok{continue}// Allocate the appropriate structure for this token.varobjinterface{}switchse.Name.Space+" "+se.Name.Local{caseNsStream+" stream":st,err:=parseStream(se)iferr!=nil{Warn.Logf("unmarshal stream: %s",err)breakLoop}ch<-stcontinuecase"stream error",NsStream+" error":obj=&streamError{}caseNsStream+" features":obj=&Features{}caseNsTLS+" proceed",NsTLS+" failure":obj=&starttls{}caseNsSASL+" challenge",NsSASL+" failure",NsSASL+" success":obj=&auth{}caseNsClient+" iq":obj=&Iq{}caseNsClient+" message":obj=&Message{}caseNsClient+" presence":obj=&Presence{}default:obj=&Generic{}Info.Logf("Ignoring unrecognized: %s %s",se.Name.Space,se.Name.Local)}// Read the complete XML stanza.err=p.DecodeElement(obj,&se)iferr!=nil{Warn.Logf("unmarshal: %s",err)breakLoop}// If it's a Stanza, we try to unmarshal its innerxml// into objects of the appropriate respective// types. This is specified by our extensions.ifst,ok:=obj.(Stanza);ok{err=parseExtended(st.GetHeader(),extStanza)iferr!=nil{Warn.Logf("ext unmarshal: %s",err)breakLoop}}// Put it on the channel.ch<-obj}}funcparseExtended(st*Header,extStanzamap[xml.Name]reflect.Type)error{// Now parse the stanza's innerxml to find the string that we// can unmarshal this nested element from.reader:=strings.NewReader(st.Innerxml)p:=xml.NewDecoder(reader)for{t,err:=p.Token()iferr==io.EOF{break}iferr!=nil{returnerr}ifse,ok:=t.(xml.StartElement);ok{iftyp,ok:=extStanza[se.Name];ok{nested:=reflect.New(typ).Interface()// Unmarshal the nested element and// stuff it back into the stanza.err:=p.DecodeElement(nested,&se)iferr!=nil{returnerr}st.Nested=append(st.Nested,nested)}}}returnnil}funcwriteXml(wio.Writer,ch<-chaninterface{}){if_,ok:=Debug.(*noLog);!ok{pr,pw:=io.Pipe()gotee(pr,w,"C: ")w=pw}deferfunc(wio.Writer){ifc,ok:=w.(io.Closer);ok{c.Close()}}(w)enc:=xml.NewEncoder(w)forobj:=rangech{ifst,ok:=obj.(*stream);ok{_,err:=w.Write([]byte(st.String()))iferr!=nil{Warn.Logf("write: %s",err)}}else{err:=enc.Encode(obj)iferr!=nil{Warn.Logf("marshal: %s",err)break}}}}func(cl*Client)readStream(srvIn<-chaninterface{},cliOutchan<-Stanza){deferclose(cliOut)handlers:=make(map[string]func(Stanza)bool)Loop:for{select{caseh:=<-cl.handlers:handlers[h.id]=h.fcasex,ok:=<-srvIn:if!ok{breakLoop}switchobj:=x.(type){case*stream:handleStream(obj)case*streamError:cl.handleStreamError(obj)case*Features:cl.handleFeatures(obj)case*starttls:cl.handleTls(obj)case*auth:cl.handleSasl(obj)caseStanza:send:=trueid:=obj.GetHeader().Idifhandlers[id]!=nil{f:=handlers[id]delete(handlers,id)send=f(obj)}ifsend{cliOut<-obj}default:Warn.Logf("Unhandled non-stanza: %T %#v",x,x)}}}}// This loop is paused until resource binding is complete. Otherwise// the app might inject something inappropriate into our negotiations// with the server. The control channel controls this loop's// activity.funcwriteStream(srvOutchan<-interface{},cliIn<-chanStanza,control<-chanint){deferclose(srvOut)varinput<-chanStanzaLoop:for{select{casestatus:=<-control:switchstatus{case0:input=nilcase1:input=cliIncase-1:breakLoop}casex,ok:=<-input:if!ok{breakLoop}ifx==nil{Info.Log("Refusing to send nil stanza")continue}srvOut<-x}}}funchandleStream(ss*stream){}func(cl*Client)handleStreamError(se*streamError){Info.Logf("Received stream error: %v",se)close(cl.Send)}func(cl*Client)handleFeatures(fe*Features){cl.Features=feiffe.Starttls!=nil{start:=&starttls{XMLName:xml.Name{Space:NsTLS,Local:"starttls"}}cl.sendXml<-startreturn}iflen(fe.Mechanisms.Mechanism)>0{cl.chooseSasl(fe)return}iffe.Bind!=nil{cl.bind(fe.Bind)return}}// readTransport() is running concurrently. We need to stop it,// negotiate TLS, then start it again. It calls waitForSocket() in// its inner loop; see below.func(cl*Client)handleTls(t*starttls){tcp:=cl.socket// Set the socket to nil, and wait for the reader routine to// signal that it's paused.cl.socket=nilcl.socketSync.Add(1)cl.socketSync.Wait()// Negotiate TLS with the server.tls:=tls.Client(tcp,&TlsConfig)// Make the TLS connection available to the reader, and wait// for it to signal that it's working again.cl.socketSync.Add(1)cl.socket=tlscl.socketSync.Wait()Info.Log("TLS negotiation succeeded.")cl.Features=nil// Now re-send the initial handshake message to start the new// session.hsOut:=&stream{To:cl.Jid.Domain,Version:XMPPVersion}cl.sendXml<-hsOut}// Synchronize with handleTls(). Called from readTransport() when// cl.socket is nil.func(cl*Client)waitForSocket(){// Signal that we've stopped reading from the socket.cl.socketSync.Done()// Wait until the socket is available again.forcl.socket==nil{time.Sleep(1e8)}// Signal that we're going back to the read loop.cl.socketSync.Done()}// BUG(cjyar): Doesn't implement TLS/SASL EXTERNAL.func(cl*Client)chooseSasl(fe*Features){vardigestMd5boolfor_,m:=rangefe.Mechanisms.Mechanism{switchstrings.ToLower(m){case"digest-md5":digestMd5=true}}ifdigestMd5{auth:=&auth{XMLName:xml.Name{Space:NsSASL,Local:"auth"},Mechanism:"DIGEST-MD5"}cl.sendXml<-auth}}func(cl*Client)handleSasl(srv*auth){switchstrings.ToLower(srv.XMLName.Local){case"challenge":b64:=base64.StdEncodingstr,err:=b64.DecodeString(srv.Chardata)iferr!=nil{Warn.Logf("SASL challenge decode: %s",err)return}srvMap:=parseSasl(string(str))ifcl.saslExpected==""{cl.saslDigest1(srvMap)}else{cl.saslDigest2(srvMap)}case"failure":Info.Log("SASL authentication failed")case"success":Info.Log("Sasl authentication succeeded")cl.Features=nilss:=&stream{To:cl.Jid.Domain,Version:XMPPVersion}cl.sendXml<-ss}}func(cl*Client)saslDigest1(srvMapmap[string]string){// Make sure it supports qop=authvarhasAuthboolfor_,qop:=rangestrings.Fields(srvMap["qop"]){ifqop=="auth"{hasAuth=true}}if!hasAuth{Warn.Log("Server doesn't support SASL auth")return}// Pick a realm.varrealmstringifsrvMap["realm"]!=""{realm=strings.Fields(srvMap["realm"])[0]}passwd:=cl.passwordnonce:=srvMap["nonce"]digestUri:="xmpp/"+cl.Jid.DomainnonceCount:=int32(1)nonceCountStr:=fmt.Sprintf("%08x",nonceCount)// Begin building the response. Username is// user@domain or just domain.varusernamestringifcl.Jid.Node==""{username=cl.Jid.Domain}else{username=cl.Jid.Node}// Generate our own nonce from random data.randSize:=big.NewInt(0)randSize.Lsh(big.NewInt(1),64)cnonce,err:=rand.Int(rand.Reader,randSize)iferr!=nil{Warn.Logf("SASL rand: %s",err)return}cnonceStr:=fmt.Sprintf("%016x",cnonce)/* Now encode the actual password response, as well as the * expected next challenge from the server. */response:=saslDigestResponse(username,realm,passwd,nonce,cnonceStr,"AUTHENTICATE",digestUri,nonceCountStr)next:=saslDigestResponse(username,realm,passwd,nonce,cnonceStr,"",digestUri,nonceCountStr)cl.saslExpected=next// Build the map which will be encoded.clMap:=make(map[string]string)clMap["realm"]=`"`+realm+`"`clMap["username"]=`"`+username+`"`clMap["nonce"]=`"`+nonce+`"`clMap["cnonce"]=`"`+cnonceStr+`"`clMap["nc"]=nonceCountStrclMap["qop"]="auth"clMap["digest-uri"]=`"`+digestUri+`"`clMap["response"]=responseifsrvMap["charset"]=="utf-8"{clMap["charset"]="utf-8"}// Encode the map and send it.clStr:=packSasl(clMap)b64:=base64.StdEncodingclObj:=&auth{XMLName:xml.Name{Space:NsSASL,Local:"response"},Chardata:b64.EncodeToString([]byte(clStr))}cl.sendXml<-clObj}func(cl*Client)saslDigest2(srvMapmap[string]string){ifcl.saslExpected==srvMap["rspauth"]{clObj:=&auth{XMLName:xml.Name{Space:NsSASL,Local:"response"}}cl.sendXml<-clObj}else{clObj:=&auth{XMLName:xml.Name{Space:NsSASL,Local:"failure"},Any:&Generic{XMLName:xml.Name{Space:NsSASL,Local:"abort"}}}cl.sendXml<-clObj}}// Takes a string like `key1=value1,key2="value2"...` and returns a// key/value map.funcparseSasl(instring)map[string]string{re:=regexp.MustCompile(`([^=]+)="?([^",]+)"?,?`)strs:=re.FindAllStringSubmatch(in,-1)m:=make(map[string]string)for_,pair:=rangestrs{key:=strings.ToLower(string(pair[1]))value:=string(pair[2])m[key]=value}returnm}// Inverse of parseSasl().funcpackSasl(mmap[string]string)string{varterms[]stringforkey,value:=rangem{ifkey==""||value==""||value==`""`{continue}terms=append(terms,key+"="+value)}returnstrings.Join(terms,",")}// Computes the response string for digest authentication.funcsaslDigestResponse(username,realm,passwd,nonce,cnonceStr,authenticate,digestUri,nonceCountStrstring)string{h:=func(textstring)[]byte{h:=md5.New()h.Write([]byte(text))returnh.Sum(nil)}hex:=func(bytes[]byte)string{returnfmt.Sprintf("%x",bytes)}kd:=func(secret,datastring)[]byte{returnh(secret+":"+data)}a1:=string(h(username+":"+realm+":"+passwd))+":"+nonce+":"+cnonceStra2:=authenticate+":"+digestUriresponse:=hex(kd(hex(h(a1)),nonce+":"+nonceCountStr+":"+cnonceStr+":auth:"+hex(h(a2))))returnresponse}// Send a request to bind a resource. RFC 3920, section 7.func(cl*Client)bind(bindAdv*bindIq){res:=cl.Jid.ResourcebindReq:=&bindIq{}ifres!=""{bindReq.Resource=&res}msg:=&Iq{Header:Header{Type:"set",Id:NextId(),Nested:[]interface{}{bindReq}}}f:=func(stStanza)bool{iq,ok:=st.(*Iq)if!ok{Warn.Log("non-iq response")}ifiq.Type=="error"{Warn.Log("Resource binding failed")returnfalse}varbindRepl*bindIqfor_,ele:=rangeiq.Nested{ifb,ok:=ele.(*bindIq);ok{bindRepl=bbreak}}ifbindRepl==nil{Warn.Logf("Bad bind reply: %#v",iq)returnfalse}jidStr:=bindRepl.JidifjidStr==nil||*jidStr==""{Warn.Log("Can't bind empty resource")returnfalse}jid:=new(JID)iferr:=jid.Set(*jidStr);err!=nil{Warn.Logf("Can't parse JID %s: %s",*jidStr,err)returnfalse}cl.Jid=*jidInfo.Logf("Bound resource: %s",cl.Jid.String())cl.bindDone()returnfalse}cl.HandleStanza(msg.Id,f)cl.sendXml<-msg}// Register a callback to handle the next XMPP stanza (iq, message, or// presence) with a given id. The provided function will not be called// more than once. If it returns false, the stanza will not be made// available on the normal Client.In channel. The stanza handler// must not read from that channel, as deliveries on it cannot proceed// until the handler returns true or false.func(cl*Client)HandleStanza(idstring,ffunc(Stanza)bool){h:=&stanzaHandler{id:id,f:f}cl.handlers<-h}