/
component.go
128 lines (112 loc) · 3.62 KB
/
component.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 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 component is used to establish XEP-0114: Jabber Component Protocol
// connections.
//
// Be advised: This API is still unstable and is subject to change.
package component // import "mellium.im/xmpp/component"
import (
"context"
"crypto/sha1"
"encoding/xml"
"errors"
"fmt"
"io"
"mellium.im/xmpp"
"mellium.im/xmpp/internal/ns"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/stream"
)
// A list of namespaces used by this package, provided as a convenience.
const (
NSAccept = `jabber:component:accept`
)
// NewSession initiates an XMPP session on the given io.ReadWriter using the
// component protocol.
func NewSession(ctx context.Context, addr *jid.JID, secret []byte, rw io.ReadWriter) (*xmpp.Session, error) {
return xmpp.NegotiateSession(ctx, nil, rw, Negotiator(addr, secret))
}
// Negotiator returns a new function that can be used to negotiate a component
// protocol connection on the provided io.ReadWriter.
//
// It currently only supports the client side of the component protocol.
func Negotiator(addr *jid.JID, secret []byte) xmpp.Negotiator {
return func(ctx context.Context, s *xmpp.Session, _ interface{}) (mask xmpp.SessionState, _ io.ReadWriter, _ interface{}, err error) {
d := xml.NewDecoder(s.Conn())
if (s.State() & xmpp.Received) == xmpp.Received {
// If we're the receiving entity wait for a new stream, then send one in
// response.
panic("component: receiving connections not yet implemented")
} else {
// If we're the initiating entity, send a new stream and then wait for one
// in response.
_, err = fmt.Fprintf(s.Conn(), `<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='%s'>`, addr)
if err != nil {
return mask, nil, nil, err
}
}
foundProc := false
var start xml.StartElement
// TODO: This loop is stupid and probably broken. Find a way to reuse existing
// logic from the xmpp package?
procloop:
for {
tok, err := d.Token()
if err != nil {
return mask, nil, nil, err
}
switch t := tok.(type) {
case xml.ProcInst:
if !foundProc {
foundProc = true
continue
}
return mask, nil, nil, errors.New("Received unexpected proc inst from server")
case xml.StartElement:
start = t
break procloop
default:
return mask, nil, nil, errors.New("Received unexpected token from server")
}
}
if start.Name.Local != "stream" || start.Name.Space != ns.Stream {
return mask, nil, nil, errors.New("Expected stream:stream from server")
}
var id string
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
id = attr.Value
break
}
}
if id == "" {
return mask, nil, nil, errors.New("Expected server stream to contain stream ID")
}
// hash.Write never returns an error per the documentation.
h := sha1.New()
_, _ = h.Write([]byte(id))
_, _ = h.Write(secret)
_, err = fmt.Fprintf(s.Conn(), `<handshake>%x</handshake>`, h.Sum(nil))
if err != nil {
return mask, nil, nil, err
}
tok, err := d.Token()
if err != nil {
return mask, nil, nil, err
}
start, ok := tok.(xml.StartElement)
if !ok {
return mask, nil, nil, errors.New("Expected acknowledgement or error start token from server")
}
// TODO: Actually unmarshal the stream error.
switch start.Name.Local {
case "error":
return mask, nil, nil, stream.NotAuthorized
case "handshake":
err = d.Skip()
return xmpp.Ready | xmpp.Authn, nil, nil, err
}
return mask, nil, nil, fmt.Errorf("Unknown start element: %v", start)
}
}