Skip to content

Commit c996b76

Browse files
committedDec 6, 2014
Initial commit.
0 parents  commit c996b76

37 files changed

+6446
-0
lines changed
 

‎AUTHORS

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# This is the official list of zephyr-go authors for copyright purposes.
2+
3+
# Names should be added to this file as:
4+
# Name or Organization <email address>
5+
# The email address is not required for organizations.
6+
7+
David Benjamin <davidben@davidben.net>

‎LICENSE

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
2+
Apache License
3+
Version 2.0, January 2004
4+
http://www.apache.org/licenses/
5+
6+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7+
8+
1. Definitions.
9+
10+
"License" shall mean the terms and conditions for use, reproduction,
11+
and distribution as defined by Sections 1 through 9 of this document.
12+
13+
"Licensor" shall mean the copyright owner or entity authorized by
14+
the copyright owner that is granting the License.
15+
16+
"Legal Entity" shall mean the union of the acting entity and all
17+
other entities that control, are controlled by, or are under common
18+
control with that entity. For the purposes of this definition,
19+
"control" means (i) the power, direct or indirect, to cause the
20+
direction or management of such entity, whether by contract or
21+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
22+
outstanding shares, or (iii) beneficial ownership of such entity.
23+
24+
"You" (or "Your") shall mean an individual or Legal Entity
25+
exercising permissions granted by this License.
26+
27+
"Source" form shall mean the preferred form for making modifications,
28+
including but not limited to software source code, documentation
29+
source, and configuration files.
30+
31+
"Object" form shall mean any form resulting from mechanical
32+
transformation or translation of a Source form, including but
33+
not limited to compiled object code, generated documentation,
34+
and conversions to other media types.
35+
36+
"Work" shall mean the work of authorship, whether in Source or
37+
Object form, made available under the License, as indicated by a
38+
copyright notice that is included in or attached to the work
39+
(an example is provided in the Appendix below).
40+
41+
"Derivative Works" shall mean any work, whether in Source or Object
42+
form, that is based on (or derived from) the Work and for which the
43+
editorial revisions, annotations, elaborations, or other modifications
44+
represent, as a whole, an original work of authorship. For the purposes
45+
of this License, Derivative Works shall not include works that remain
46+
separable from, or merely link (or bind by name) to the interfaces of,
47+
the Work and Derivative Works thereof.
48+
49+
"Contribution" shall mean any work of authorship, including
50+
the original version of the Work and any modifications or additions
51+
to that Work or Derivative Works thereof, that is intentionally
52+
submitted to Licensor for inclusion in the Work by the copyright owner
53+
or by an individual or Legal Entity authorized to submit on behalf of
54+
the copyright owner. For the purposes of this definition, "submitted"
55+
means any form of electronic, verbal, or written communication sent
56+
to the Licensor or its representatives, including but not limited to
57+
communication on electronic mailing lists, source code control systems,
58+
and issue tracking systems that are managed by, or on behalf of, the
59+
Licensor for the purpose of discussing and improving the Work, but
60+
excluding communication that is conspicuously marked or otherwise
61+
designated in writing by the copyright owner as "Not a Contribution."
62+
63+
"Contributor" shall mean Licensor and any individual or Legal Entity
64+
on behalf of whom a Contribution has been received by Licensor and
65+
subsequently incorporated within the Work.
66+
67+
2. Grant of Copyright License. Subject to the terms and conditions of
68+
this License, each Contributor hereby grants to You a perpetual,
69+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70+
copyright license to reproduce, prepare Derivative Works of,
71+
publicly display, publicly perform, sublicense, and distribute the
72+
Work and such Derivative Works in Source or Object form.
73+
74+
3. Grant of Patent License. Subject to the terms and conditions of
75+
this License, each Contributor hereby grants to You a perpetual,
76+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77+
(except as stated in this section) patent license to make, have made,
78+
use, offer to sell, sell, import, and otherwise transfer the Work,
79+
where such license applies only to those patent claims licensable
80+
by such Contributor that are necessarily infringed by their
81+
Contribution(s) alone or by combination of their Contribution(s)
82+
with the Work to which such Contribution(s) was submitted. If You
83+
institute patent litigation against any entity (including a
84+
cross-claim or counterclaim in a lawsuit) alleging that the Work
85+
or a Contribution incorporated within the Work constitutes direct
86+
or contributory patent infringement, then any patent licenses
87+
granted to You under this License for that Work shall terminate
88+
as of the date such litigation is filed.
89+
90+
4. Redistribution. You may reproduce and distribute copies of the
91+
Work or Derivative Works thereof in any medium, with or without
92+
modifications, and in Source or Object form, provided that You
93+
meet the following conditions:
94+
95+
(a) You must give any other recipients of the Work or
96+
Derivative Works a copy of this License; and
97+
98+
(b) You must cause any modified files to carry prominent notices
99+
stating that You changed the files; and
100+
101+
(c) You must retain, in the Source form of any Derivative Works
102+
that You distribute, all copyright, patent, trademark, and
103+
attribution notices from the Source form of the Work,
104+
excluding those notices that do not pertain to any part of
105+
the Derivative Works; and
106+
107+
(d) If the Work includes a "NOTICE" text file as part of its
108+
distribution, then any Derivative Works that You distribute must
109+
include a readable copy of the attribution notices contained
110+
within such NOTICE file, excluding those notices that do not
111+
pertain to any part of the Derivative Works, in at least one
112+
of the following places: within a NOTICE text file distributed
113+
as part of the Derivative Works; within the Source form or
114+
documentation, if provided along with the Derivative Works; or,
115+
within a display generated by the Derivative Works, if and
116+
wherever such third-party notices normally appear. The contents
117+
of the NOTICE file are for informational purposes only and
118+
do not modify the License. You may add Your own attribution
119+
notices within Derivative Works that You distribute, alongside
120+
or as an addendum to the NOTICE text from the Work, provided
121+
that such additional attribution notices cannot be construed
122+
as modifying the License.
123+
124+
You may add Your own copyright statement to Your modifications and
125+
may provide additional or different license terms and conditions
126+
for use, reproduction, or distribution of Your modifications, or
127+
for any such Derivative Works as a whole, provided Your use,
128+
reproduction, and distribution of the Work otherwise complies with
129+
the conditions stated in this License.
130+
131+
5. Submission of Contributions. Unless You explicitly state otherwise,
132+
any Contribution intentionally submitted for inclusion in the Work
133+
by You to the Licensor shall be under the terms and conditions of
134+
this License, without any additional terms or conditions.
135+
Notwithstanding the above, nothing herein shall supersede or modify
136+
the terms of any separate license agreement you may have executed
137+
with Licensor regarding such Contributions.
138+
139+
6. Trademarks. This License does not grant permission to use the trade
140+
names, trademarks, service marks, or product names of the Licensor,
141+
except as required for reasonable and customary use in describing the
142+
origin of the Work and reproducing the content of the NOTICE file.
143+
144+
7. Disclaimer of Warranty. Unless required by applicable law or
145+
agreed to in writing, Licensor provides the Work (and each
146+
Contributor provides its Contributions) on an "AS IS" BASIS,
147+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148+
implied, including, without limitation, any warranties or conditions
149+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150+
PARTICULAR PURPOSE. You are solely responsible for determining the
151+
appropriateness of using or redistributing the Work and assume any
152+
risks associated with Your exercise of permissions under this License.
153+
154+
8. Limitation of Liability. In no event and under no legal theory,
155+
whether in tort (including negligence), contract, or otherwise,
156+
unless required by applicable law (such as deliberate and grossly
157+
negligent acts) or agreed to in writing, shall any Contributor be
158+
liable to You for damages, including any direct, indirect, special,
159+
incidental, or consequential damages of any character arising as a
160+
result of this License or out of the use or inability to use the
161+
Work (including but not limited to damages for loss of goodwill,
162+
work stoppage, computer failure or malfunction, or any and all
163+
other commercial damages or losses), even if such Contributor
164+
has been advised of the possibility of such damages.
165+
166+
9. Accepting Warranty or Additional Liability. While redistributing
167+
the Work or Derivative Works thereof, You may choose to offer,
168+
and charge a fee for, acceptance of support, warranty, indemnity,
169+
or other liability obligations and/or rights consistent with this
170+
License. However, in accepting such obligations, You may act only
171+
on Your own behalf and on Your sole responsibility, not on behalf
172+
of any other Contributor, and only if You agree to indemnify,
173+
defend, and hold each Contributor harmless for any liability
174+
incurred by, or claims asserted against, such Contributor by reason
175+
of your accepting any such warranty or additional liability.
176+
177+
END OF TERMS AND CONDITIONS
178+
179+
APPENDIX: How to apply the Apache License to your work.
180+
181+
To apply the Apache License to your work, attach the following
182+
boilerplate notice, with the fields enclosed by brackets "[]"
183+
replaced with your own identifying information. (Don't include
184+
the brackets!) The text should be enclosed in the appropriate
185+
comment syntax for the file format. We also recommend that a
186+
file or class name and description of purpose be included on the
187+
same "printed page" as the copyright notice for easier
188+
identification within third-party archives.
189+
190+
Copyright [yyyy] [name of copyright owner]
191+
192+
Licensed under the Apache License, Version 2.0 (the "License");
193+
you may not use this file except in compliance with the License.
194+
You may obtain a copy of the License at
195+
196+
http://www.apache.org/licenses/LICENSE-2.0
197+
198+
Unless required by applicable law or agreed to in writing, software
199+
distributed under the License is distributed on an "AS IS" BASIS,
200+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201+
See the License for the specific language governing permissions and
202+
limitations under the License.

‎clock.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"time"
19+
)
20+
21+
// Clock is a mockable interface for components which involve time.
22+
type Clock interface {
23+
Now() time.Time
24+
After(d time.Duration) <-chan time.Time
25+
}
26+
27+
type systemClock struct{}
28+
29+
func (systemClock) Now() time.Time {
30+
return time.Now()
31+
}
32+
33+
func (systemClock) After(d time.Duration) <-chan time.Time {
34+
return time.After(d)
35+
}
36+
37+
// SystemClock is the real implementation of the Clock interface.
38+
var SystemClock systemClock

‎cmd/subscriber/subscriber.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"flag"
19+
"log"
20+
"os"
21+
"os/signal"
22+
"syscall"
23+
24+
"github.com/zephyr-im/krb5-go"
25+
"github.com/zephyr-im/zephyr-go"
26+
)
27+
28+
func main() {
29+
flag.Parse()
30+
if flag.NArg() != 2 {
31+
log.Fatal("Need 2 arguments")
32+
}
33+
subs := []zephyr.Subscription{
34+
{"", flag.Arg(0), flag.Arg(1)},
35+
}
36+
37+
// Open a session.
38+
session, err := zephyr.DialSystemDefault()
39+
if err != nil {
40+
log.Fatal(err)
41+
}
42+
defer session.Close()
43+
go func() {
44+
for r := range session.Messages() {
45+
log.Printf("Received message %v %v", r.AuthStatus, r.Message)
46+
}
47+
}()
48+
49+
log.Printf("Subscribing to %v", subs)
50+
ctx, err := krb5.NewContext()
51+
if err != nil {
52+
log.Fatal(err)
53+
}
54+
defer ctx.Free()
55+
ack, err := session.SendSubscribeNoDefaults(ctx, subs)
56+
log.Printf(" -> %v %v", ack, err)
57+
defer func() {
58+
log.Printf("Canceling subscriptions")
59+
ack, err := session.SendCancelSubscriptions(ctx)
60+
log.Printf(" -> %v %v", ack, err)
61+
}()
62+
63+
// Keep listening until a SIGINT or SIGTERM.
64+
c := make(chan os.Signal, 1)
65+
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
66+
<-c
67+
}

‎cmd/zwrite/zwrite.go

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"bufio"
19+
"fmt"
20+
"io/ioutil"
21+
"log"
22+
"os"
23+
"strings"
24+
"time"
25+
26+
"github.com/zephyr-im/krb5-go"
27+
"github.com/zephyr-im/zephyr-go"
28+
)
29+
30+
var class = "message"
31+
var instance = "personal"
32+
var opcode = ""
33+
var signature = ""
34+
var message = ""
35+
var sender = ""
36+
var haveSender = false
37+
var auth = true
38+
var expandTabs = true
39+
var eofOnly = false
40+
var realm = ""
41+
var haveRealm = false
42+
var recipients = []string{}
43+
44+
func printUsage() {
45+
fmt.Fprintln(os.Stderr, "Usage: zwrite [-a] [-d] [-t] [-l] [-u]")
46+
fmt.Fprintln(os.Stderr, "\t[-c class] [-i instance] [-O opcode] [-s signature] [-S sender]")
47+
fmt.Fprintln(os.Stderr, "\t[user...] [-r realm] [-m message]")
48+
}
49+
50+
func parseFlagArg(flag, value string) {
51+
switch flag {
52+
case "-s":
53+
signature = value
54+
case "-c":
55+
class = value
56+
case "-i":
57+
instance = value
58+
case "-r":
59+
realm = value
60+
haveRealm = true
61+
case "-S":
62+
sender = value
63+
haveSender = true
64+
case "-O":
65+
opcode = value
66+
default:
67+
panic(flag)
68+
}
69+
}
70+
71+
func readMessage(eofOnly bool) (string, error) {
72+
if eofOnly {
73+
fmt.Fprintln(os.Stderr, "Type your message now. "+
74+
"End with the end-of-file character.")
75+
message, err := ioutil.ReadAll(os.Stdin)
76+
if err != nil {
77+
return "", err
78+
}
79+
return string(message), nil
80+
}
81+
82+
fmt.Fprintln(os.Stderr, "Type your message now. "+
83+
"End with control-D or a dot on a line by itself.")
84+
scanner := bufio.NewScanner(os.Stdin)
85+
message := ""
86+
for scanner.Scan() {
87+
line := scanner.Text()
88+
if line == "." {
89+
break
90+
}
91+
message = message + line + "\n"
92+
}
93+
if err := scanner.Err(); err != nil {
94+
return "", err
95+
}
96+
return message, nil
97+
}
98+
99+
func parseFlags() {
100+
haveMessage := false
101+
// TODO(davidben): To really be true to zwrite, check isatty
102+
// and, if not, set eofOnly to true.
103+
var i int
104+
argLoop:
105+
for i = 1; i < len(os.Args); i++ {
106+
switch arg := os.Args[i]; arg {
107+
case "-a":
108+
auth = true
109+
case "-d":
110+
auth = false
111+
case "-t":
112+
expandTabs = false
113+
case "-l":
114+
eofOnly = true
115+
case "-u":
116+
instance = "URGENT"
117+
case "-m":
118+
haveMessage = true
119+
message = strings.Join(os.Args[i+1:], " ")
120+
break argLoop
121+
case "-s", "-c", "-i", "-r", "-S", "-O":
122+
if i+1 >= len(os.Args) {
123+
printUsage()
124+
os.Exit(1)
125+
}
126+
i++
127+
parseFlagArg(arg, os.Args[i])
128+
default:
129+
if len(arg) >= 1 && arg[0] == '-' {
130+
printUsage()
131+
os.Exit(1)
132+
}
133+
recipients = append(recipients, arg)
134+
}
135+
}
136+
137+
// Normalize receipients.
138+
if len(recipients) == 0 {
139+
if class == "message" &&
140+
(instance == "personal" || instance == "URGENT") {
141+
fmt.Fprintln(os.Stderr, "No recipients specified.")
142+
printUsage()
143+
os.Exit(1)
144+
}
145+
recipients = []string{""}
146+
}
147+
if haveRealm {
148+
for i := range recipients {
149+
recipients[i] = recipients[i] + "@" + realm
150+
}
151+
}
152+
153+
if !haveMessage {
154+
// Read message from stdin.
155+
var err error
156+
message, err = readMessage(eofOnly)
157+
if err != nil {
158+
fmt.Fprintf(os.Stderr, "Error reading stdin: %s\n", err)
159+
os.Exit(1)
160+
}
161+
}
162+
163+
if expandTabs {
164+
newMsg := []byte{}
165+
spaces := [8]byte{
166+
' ', ' ', ' ', ' ',
167+
' ', ' ', ' ', ' ',
168+
}
169+
off := 0
170+
for _, b := range []byte(message) {
171+
if b == '\t' {
172+
newMsg = append(newMsg, spaces[:8-off]...)
173+
off = 0
174+
} else {
175+
newMsg = append(newMsg, b)
176+
if b == '\n' {
177+
off = 0
178+
} else {
179+
off = (off + 1) % 8
180+
}
181+
}
182+
}
183+
message = string(newMsg)
184+
}
185+
}
186+
187+
func main() {
188+
parseFlags()
189+
190+
// Open a session.
191+
session, err := zephyr.DialSystemDefault()
192+
if err != nil {
193+
log.Fatal(err)
194+
}
195+
defer session.Close()
196+
// Make sure the notice sink doesn't get stuck.
197+
// TODO(davidben): This is silly.
198+
go func() {
199+
for _ = range session.Messages() {
200+
}
201+
}()
202+
203+
// Further normalize receipients.
204+
if !haveSender {
205+
sender = session.Sender()
206+
}
207+
for i := range recipients {
208+
if len(recipients[i]) != 0 && strings.Index(recipients[i], "@") < 0 {
209+
recipients[i] = recipients[i] + "@" + session.Realm()
210+
}
211+
}
212+
213+
// Get tickets.
214+
ctx, err := krb5.NewContext()
215+
if err != nil {
216+
log.Fatal(err)
217+
}
218+
defer ctx.Free()
219+
for _, recipient := range recipients {
220+
// Construct the message.
221+
uid := session.MakeUID(time.Now())
222+
msg := &zephyr.Message{
223+
Header: zephyr.Header{
224+
Kind: zephyr.ACKED,
225+
UID: uid,
226+
Port: session.Port(),
227+
Class: class, Instance: instance, OpCode: opcode,
228+
Sender: sender,
229+
Recipient: recipient,
230+
DefaultFormat: "http://mit.edu/df/",
231+
SenderAddress: session.LocalAddr().IP,
232+
Charset: zephyr.CharsetUTF8,
233+
OtherFields: nil,
234+
},
235+
Body: []string{signature, message},
236+
}
237+
sendTime := time.Now()
238+
var ack *zephyr.Notice
239+
var err error
240+
if auth {
241+
ack, err = session.SendMessage(ctx, msg)
242+
} else {
243+
ack, err = session.SendMessageUnauth(msg)
244+
}
245+
if err != nil {
246+
log.Printf("Send error: %v", err)
247+
} else {
248+
log.Printf("Received ack in %v: %v",
249+
time.Now().Sub(sendTime), ack)
250+
}
251+
}
252+
}

‎connection.go

+389
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"errors"
19+
"log"
20+
"net"
21+
"sync"
22+
"time"
23+
24+
"github.com/zephyr-im/krb5-go"
25+
)
26+
27+
func localIPForUDPAddr(addr *net.UDPAddr) (net.IP, error) {
28+
bogus, err := net.DialUDP("udp", nil, addr)
29+
if err != nil {
30+
return nil, err
31+
}
32+
defer bogus.Close()
33+
return bogus.LocalAddr().(*net.UDPAddr).IP, nil
34+
}
35+
36+
func udpAddrsEqual(a, b *net.UDPAddr) bool {
37+
return a.IP.Equal(b.IP) && a.Port == b.Port && a.Zone == b.Zone
38+
}
39+
40+
// How frequently we query for new servers.
41+
const serverRefreshInterval = 10 * time.Minute
42+
43+
// A Connection represents a low-level connection to the Zephyr
44+
// servers. It handles server discovery and sending and receiving
45+
// Notices. It does not provide high-level constructs like subscribing
46+
// or message sharding. It also does not automatically send
47+
// CLIENTACKs.
48+
type Connection struct {
49+
// Properties of the connection.
50+
conn net.PacketConn
51+
server ServerConfig
52+
cred *krb5.Credential
53+
clock Clock
54+
localIP net.IP
55+
56+
// Incoming notices from the connection.
57+
allNotices <-chan NoticeReaderResult
58+
59+
// Where non-ACK notices get dumped.
60+
notices chan NoticeReaderResult
61+
62+
// Table of pending ACKs.
63+
ackTable map[UID]chan NoticeReaderResult
64+
ackTableLock sync.Mutex
65+
66+
// Current server send schedule.
67+
sched []*net.UDPAddr
68+
schedIdx int
69+
schedLock sync.Mutex
70+
71+
stopRefreshing chan int
72+
}
73+
74+
// NewConnection creates a new Connection wrapping a given
75+
// net.PacketConn. The ServerConfig argument instructs the connection
76+
// on how to locate the remote servers. The Credential is used to
77+
// authenticate incoming and outgoing packets. The connection takes
78+
// ownership of the PacketConn and will close it when Close is
79+
// called.
80+
func NewConnection(
81+
conn net.PacketConn,
82+
server ServerConfig,
83+
cred *krb5.Credential,
84+
logger *log.Logger,
85+
) (*Connection, error) {
86+
return NewConnectionFull(conn, server, cred, logger, SystemClock)
87+
}
88+
89+
// NewConnectionFull does the same as NewConnection but takes an
90+
// additional Clock argument for testing.
91+
func NewConnectionFull(
92+
conn net.PacketConn,
93+
server ServerConfig,
94+
cred *krb5.Credential,
95+
logger *log.Logger,
96+
clock Clock,
97+
) (*Connection, error) {
98+
c := new(Connection)
99+
c.conn = conn
100+
c.server = server
101+
c.cred = cred
102+
c.clock = clock
103+
var key *krb5.KeyBlock
104+
if c.cred != nil {
105+
key = c.cred.KeyBlock
106+
}
107+
c.allNotices = ReadNoticesFromServer(conn, key, logger)
108+
c.notices = make(chan NoticeReaderResult)
109+
c.ackTable = make(map[UID]chan NoticeReaderResult)
110+
111+
c.stopRefreshing = make(chan int, 1)
112+
113+
if _, err := c.RefreshServer(); err != nil {
114+
return nil, err
115+
}
116+
localIP, err := localIPForUDPAddr(c.sched[0])
117+
if err != nil {
118+
return nil, err
119+
}
120+
c.localIP = localIP
121+
122+
go c.readLoop()
123+
// This is kinda screwy. Purely for testing purposes, ensure
124+
// the first query on the clock happens by the time
125+
// NewConnectionFull returns. MockClock is a little messy.
126+
go c.refreshLoop(c.clock.After(serverRefreshInterval))
127+
return c, nil
128+
}
129+
130+
// Notices returns the incoming notices from the connection.
131+
func (c *Connection) Notices() <-chan NoticeReaderResult {
132+
return c.notices
133+
}
134+
135+
// LocalAddr returns the local UDP address for the client when
136+
// communicating with the Zephyr servers.
137+
func (c *Connection) LocalAddr() *net.UDPAddr {
138+
addr := c.conn.LocalAddr().(*net.UDPAddr)
139+
addr.IP = c.localIP
140+
return addr
141+
}
142+
143+
// Credential returns the credential for this connection.
144+
func (c *Connection) Credential() *krb5.Credential {
145+
return c.cred
146+
}
147+
148+
// Close closes the underlying connection.
149+
func (c *Connection) Close() error {
150+
c.stopRefreshing <- 0
151+
return c.conn.Close()
152+
}
153+
154+
func (c *Connection) readLoop() {
155+
for r := range c.allNotices {
156+
if r.Notice.Kind.IsServerACK() {
157+
c.processServAck(r)
158+
} else {
159+
c.notices <- r
160+
}
161+
}
162+
close(c.notices)
163+
}
164+
165+
func (c *Connection) refreshLoop(after <-chan time.Time) {
166+
for {
167+
select {
168+
case <-after:
169+
c.RefreshServer()
170+
after = c.clock.After(serverRefreshInterval)
171+
case <-c.stopRefreshing:
172+
return
173+
}
174+
}
175+
}
176+
177+
func (c *Connection) findPendingSend(uid UID) chan NoticeReaderResult {
178+
c.ackTableLock.Lock()
179+
defer c.ackTableLock.Unlock()
180+
if ps, ok := c.ackTable[uid]; ok {
181+
delete(c.ackTable, uid)
182+
return ps
183+
}
184+
return nil
185+
}
186+
187+
func (c *Connection) addPendingSend(uid UID) <-chan NoticeReaderResult {
188+
// Buffer one entry; if the ACK and timeout race, the
189+
// sending thread should not lock up.
190+
ackChan := make(chan NoticeReaderResult, 1)
191+
c.ackTableLock.Lock()
192+
defer c.ackTableLock.Unlock()
193+
c.ackTable[uid] = ackChan
194+
return ackChan
195+
}
196+
197+
func (c *Connection) clearPendingSend(uid UID) {
198+
c.ackTableLock.Lock()
199+
defer c.ackTableLock.Unlock()
200+
delete(c.ackTable, uid)
201+
}
202+
203+
func (c *Connection) processServAck(r NoticeReaderResult) {
204+
ps := c.findPendingSend(r.Notice.UID)
205+
if ps != nil {
206+
ps <- r
207+
}
208+
}
209+
210+
func (c *Connection) schedule() ([]*net.UDPAddr, int) {
211+
c.schedLock.Lock()
212+
defer c.schedLock.Unlock()
213+
return c.sched, c.schedIdx
214+
}
215+
216+
func (c *Connection) setSchedule(sched []*net.UDPAddr, schedIdx int) {
217+
c.schedLock.Lock()
218+
defer c.schedLock.Unlock()
219+
c.sched = sched
220+
c.schedIdx = schedIdx
221+
}
222+
223+
func (c *Connection) goodServer(good *net.UDPAddr) {
224+
c.schedLock.Lock()
225+
defer c.schedLock.Unlock()
226+
227+
// Find the good server in the schedule and use it
228+
// preferentially next time.
229+
for i, addr := range c.sched {
230+
if udpAddrsEqual(addr, good) {
231+
c.schedIdx = i
232+
return
233+
}
234+
}
235+
}
236+
237+
// RefreshServer forces a manual refresh of the server schedule from
238+
// the ServerConfig. This will be called periodically and when
239+
// outgoing messages time out, so there should be little need to call
240+
// this manually.
241+
func (c *Connection) RefreshServer() ([]*net.UDPAddr, error) {
242+
sched, err := c.server.ResolveServer()
243+
if err != nil {
244+
return nil, err
245+
}
246+
if len(sched) == 0 {
247+
panic(sched)
248+
}
249+
c.setSchedule(sched, 0)
250+
return sched, nil
251+
}
252+
253+
// SendNotice sends an authenticated notice to the servers. If the
254+
// notice expects an acknowledgement, it returns the SERVACK or
255+
// SERVNAK notice from the server on success.
256+
func (c *Connection) SendNotice(ctx *krb5.Context, n *Notice) (*Notice, error) {
257+
pkt, err := n.EncodePacketForServer(ctx, c.cred)
258+
if err != nil {
259+
return nil, err
260+
}
261+
return c.SendPacket(pkt, n.Kind, n.UID)
262+
}
263+
264+
// SendNoticeUnauth sends an unauthenticated notice to the servers. If
265+
// the notice expects an acknowledgement, it returns the SERVACK or
266+
// SERVNAK notice from the server on success.
267+
func (c *Connection) SendNoticeUnauth(n *Notice) (*Notice, error) {
268+
pkt := n.EncodePacketUnauth()
269+
return c.SendPacket(pkt, n.Kind, n.UID)
270+
}
271+
272+
// SendNoticeUnackedTo sends an unauthenticated and unacked notice to
273+
// a given destination. This is used to send a CLIENTACK to a received
274+
// notice.
275+
func (c *Connection) SendNoticeUnackedTo(n *Notice, addr net.Addr) error {
276+
pkt := n.EncodePacketUnauth()
277+
return c.SendPacketUnackedTo(pkt, addr)
278+
}
279+
280+
// ErrPacketTooLong is returned when a notice or packet exceeds the
281+
// maximum Zephyr packet size.
282+
var ErrPacketTooLong = errors.New("packet too long")
283+
284+
// ErrSendTimeout is returned if a send times out without
285+
// acknowledgement from the server.
286+
var ErrSendTimeout = errors.New("send timeout")
287+
288+
// SendPacketUnackedTo sends a raw packet to a given destination.
289+
func (c *Connection) SendPacketUnackedTo(pkt []byte, addr net.Addr) error {
290+
if len(pkt) > MaxPacketLength {
291+
return ErrPacketTooLong
292+
}
293+
_, err := c.conn.WriteTo(pkt, addr)
294+
return err
295+
}
296+
297+
// TODO(davidben): We probably want to be more cleverer later. For
298+
// now, follow a similar strategy to the real zhm, but use a much more
299+
// aggressive rexmit schedule.
300+
//
301+
// Empirically, it seems to take 15-20ms for the zephyrds to ACK a
302+
// notice.
303+
var retrySchedule = []time.Duration{
304+
100 * time.Millisecond,
305+
100 * time.Millisecond,
306+
250 * time.Millisecond,
307+
500 * time.Millisecond,
308+
1 * time.Second,
309+
2 * time.Second,
310+
4 * time.Second,
311+
}
312+
313+
// If we've timed out 4 times, get a new server schedule.
314+
const timeoutsBeforeRefresh = 4
315+
316+
// SendPacket sends a raw packet to the Zephyr servers. Based on kind
317+
// and uid, it may wait for an acknowledgement. In that case, the
318+
// SERVACK or SERVNAK notice will be returned. SendPacket rotates
319+
// between the server instances and refreshes server list as necessary.
320+
func (c *Connection) SendPacket(pkt []byte, kind Kind, uid UID) (*Notice, error) {
321+
// TODO(davidben): Should we limit the number of packets
322+
// in-flight as an ad-hoc congestion control?
323+
if len(pkt) > MaxPacketLength {
324+
return nil, ErrPacketTooLong
325+
}
326+
retryIdx := -1
327+
timeout := c.clock.After(0)
328+
329+
// Listen for ACKs.
330+
var ackChan <-chan NoticeReaderResult
331+
var shouldClear bool
332+
if kind.ExpectsServerACK() {
333+
ackChan = c.addPendingSend(uid)
334+
shouldClear = true
335+
defer func() {
336+
if shouldClear {
337+
c.clearPendingSend(uid)
338+
}
339+
}()
340+
}
341+
342+
// Get the remote server schedule.
343+
sched, schedIdx := c.schedule()
344+
if len(sched) == 0 {
345+
panic(sched)
346+
}
347+
348+
for {
349+
select {
350+
case ack := <-ackChan:
351+
shouldClear = false // Already taken care of.
352+
// Record the good server so next time we
353+
// start at that one.
354+
c.goodServer(ack.Addr.(*net.UDPAddr))
355+
return ack.Notice, nil
356+
case <-timeout:
357+
retryIdx++
358+
if retryIdx >= len(retrySchedule) {
359+
return nil, ErrSendTimeout
360+
}
361+
362+
// Partway through the re-xmit schedule, if we
363+
// still haven't heard back from any server,
364+
// get a fresh set of remote addresses.
365+
if retryIdx == timeoutsBeforeRefresh {
366+
var err error
367+
sched, err = c.RefreshServer()
368+
if err != nil {
369+
return nil, err
370+
}
371+
schedIdx = 0
372+
}
373+
374+
addr := sched[schedIdx]
375+
if err := c.SendPacketUnackedTo(pkt, addr); err != nil {
376+
// TODO(davidben): Keep going on
377+
// temporary errors?
378+
return nil, err
379+
}
380+
if !kind.ExpectsServerACK() {
381+
return nil, nil
382+
}
383+
// Schedule the next timeout and move on to
384+
// the next server.
385+
timeout = c.clock.After(retrySchedule[retryIdx])
386+
schedIdx = (schedIdx + 1) % len(sched)
387+
}
388+
}
389+
}

‎connection_test.go

+541
Large diffs are not rendered by default.

‎data_for_test.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"bytes"
19+
"encoding/base64"
20+
"net"
21+
"strings"
22+
23+
"github.com/zephyr-im/krb5-go"
24+
)
25+
26+
func stringsToByteSlices(ss []string) [][]byte {
27+
bs := make([][]byte, len(ss))
28+
for i := range ss {
29+
bs[i] = []byte(ss[i])
30+
}
31+
return bs
32+
}
33+
34+
// Authenticated packets taken from a libzephyr session. (Session was
35+
// since canceled and the ticket associate with the key has expired.)
36+
37+
func sampleKeyBlock() *krb5.KeyBlock {
38+
data, err := base64.StdEncoding.DecodeString(
39+
"2PgONWKpPuAyFwRRIe1Ex5bR4kLNkI9beX4NGl7mkIA=")
40+
if err != nil {
41+
panic(err)
42+
}
43+
return &krb5.KeyBlock{krb5.ENCTYPE_AES256_CTS_HMAC_SHA1_96, data}
44+
}
45+
46+
func stringToUID(s string) UID {
47+
var uid UID
48+
if len(s) != 12 {
49+
panic(s)
50+
}
51+
copy(uid[:], []byte(s))
52+
return uid
53+
}
54+
55+
func sampleChecksum() []byte {
56+
return []byte("\x39\x04\x48\x83\x3f\xa5\x59\xf2\x0f\x39\x88\x00")
57+
}
58+
59+
func sampleChecksumZcode() []byte {
60+
return []byte("Z\x39\x04\x48\x83\x3f\xa5\x59\xf2\x0f\x39\x88\xff\xf0")
61+
}
62+
63+
func samplePacket() []byte {
64+
return []byte("ZEPH0.2\x00" +
65+
"0x00000013\x00" +
66+
"0x00000002\x00" +
67+
"0x1265189F 0x532DE3FC 0x0003AC0E\x00" +
68+
"0xC0CA\x00" +
69+
"0x00000001\x00" +
70+
"0x00000000\x00" +
71+
"\x00" +
72+
"davidben-test-class\x00" +
73+
"test\x00" +
74+
"\x00" +
75+
"davidben@ATHENA.MIT.EDU\x00" +
76+
"\x00" +
77+
"http://zephyr.1ts.org/wiki/df\x00" +
78+
string(sampleChecksumZcode()) + "\x00" +
79+
"0/23\x00" +
80+
"0x1265189F 0x532DE3FC 0x0003AC0E\x00" +
81+
"Z\x12\x65\x18\x9f\x00" +
82+
"0x6A00\x00" +
83+
"David Benjamin\x00" +
84+
"Message\n")
85+
86+
}
87+
88+
func sampleFailPacket() []byte {
89+
return makeTestPacket(14,
90+
"Z\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c")
91+
}
92+
93+
func sampleMalformedChecksumPacket() []byte {
94+
return makeTestPacket(14, "invalid checksum")
95+
}
96+
97+
func sampleMalformedPortPacket() []byte {
98+
return makeTestPacket(4, "invalid port")
99+
}
100+
101+
func sampleRawNotice() *RawNotice {
102+
return &RawNotice{
103+
stringsToByteSlices([]string{
104+
"ZEPH0.2",
105+
"0x00000013",
106+
"0x00000002",
107+
"0x1265189F 0x532DE3FC 0x0003AC0E",
108+
"0xC0CA",
109+
"0x00000001",
110+
"0x00000000",
111+
"",
112+
"davidben-test-class",
113+
"test",
114+
"",
115+
"davidben@ATHENA.MIT.EDU",
116+
"",
117+
"http://zephyr.1ts.org/wiki/df",
118+
string(sampleChecksumZcode()),
119+
"0/23",
120+
"0x1265189F 0x532DE3FC 0x0003AC0E",
121+
"Z\x12\x65\x18\x9f",
122+
"0x6A00",
123+
}),
124+
[]byte("David Benjamin\x00Message\n")}
125+
126+
}
127+
128+
func sampleNotice() *Notice {
129+
uid := stringToUID("\x12\x65\x18\x9F\x53\x2D\xE3\xFC\x00\x03\xAC\x0E")
130+
return &Notice{
131+
Header: Header{
132+
Kind: ACKED,
133+
UID: uid,
134+
Port: 49354,
135+
Class: "davidben-test-class",
136+
Instance: "test",
137+
OpCode: "",
138+
Sender: "davidben@ATHENA.MIT.EDU",
139+
Recipient: "",
140+
DefaultFormat: "http://zephyr.1ts.org/wiki/df",
141+
SenderAddress: net.ParseIP("18.101.24.159").To4(),
142+
Charset: CharsetUTF8,
143+
OtherFields: [][]byte{},
144+
},
145+
Multipart: "0/23",
146+
MultiUID: uid,
147+
RawBody: []byte("David Benjamin\x00Message\n")}
148+
}
149+
150+
func sampleNoticeWithUID(uid UID) *Notice {
151+
notice := sampleNotice()
152+
notice.UID = uid
153+
return notice
154+
}
155+
156+
func sampleMessage(uid UID, rawBody []byte) *Message {
157+
return &Message{
158+
sampleNoticeWithUID(uid).Header,
159+
strings.Split(string(rawBody), "\x00")}
160+
}
161+
162+
func makeTestPacket(index int, replace string) []byte {
163+
raw := sampleRawNotice()
164+
fields := make([][]byte, len(raw.HeaderFields)+1)
165+
copy(fields, raw.HeaderFields)
166+
fields[len(fields)-1] = raw.Body
167+
fields[index] = []byte(replace)
168+
return bytes.Join(fields, []byte{0})
169+
}

‎krb_util.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"errors"
19+
20+
"github.com/zephyr-im/krb5-go"
21+
)
22+
23+
const (
24+
keyUsageClientCksum = 1027
25+
keyUsageServerCksum = 1029
26+
)
27+
28+
// ErrUnknownEncType is returned when attempting to use a key with an
29+
// unknown enctype.
30+
var ErrUnknownEncType = errors.New("unknown enctype")
31+
32+
// Why is this not exported from MIT Kerberos?
33+
func defaultSumTypeForEncType(enctype krb5.EncType) (krb5.SumType, error) {
34+
switch enctype {
35+
case krb5.ENCTYPE_DES_CBC_CRC:
36+
return krb5.SUMTYPE_RSA_MD5_DES, nil
37+
case krb5.ENCTYPE_DES_CBC_MD4:
38+
return krb5.SUMTYPE_RSA_MD4_DES, nil
39+
case krb5.ENCTYPE_DES_CBC_MD5:
40+
return krb5.SUMTYPE_RSA_MD5_DES, nil
41+
case krb5.ENCTYPE_DES3_CBC_SHA1:
42+
return krb5.SUMTYPE_HMAC_SHA1_DES3, nil
43+
case krb5.ENCTYPE_AES128_CTS_HMAC_SHA1_96:
44+
return krb5.SUMTYPE_HMAC_SHA1_96_AES128, nil
45+
case krb5.ENCTYPE_AES256_CTS_HMAC_SHA1_96:
46+
return krb5.SUMTYPE_HMAC_SHA1_96_AES256, nil
47+
case krb5.ENCTYPE_ARCFOUR_HMAC:
48+
return krb5.SUMTYPE_HMAC_MD5_ARCFOUR, nil
49+
case krb5.ENCTYPE_ARCFOUR_HMAC_EXP:
50+
return krb5.SUMTYPE_HMAC_MD5_ARCFOUR, nil
51+
default:
52+
return krb5.SUMTYPE_DEFAULT, ErrUnknownEncType
53+
}
54+
}

‎krb_util_test.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"testing"
19+
20+
"github.com/zephyr-im/krb5-go"
21+
)
22+
23+
func TestDefaultSumTypeForEncType(t *testing.T) {
24+
ctx, err := krb5.NewContext()
25+
if err != nil {
26+
t.Fatalf("Could not create context: %v", err)
27+
}
28+
defer ctx.Free()
29+
30+
enctypes := []krb5.EncType{
31+
krb5.ENCTYPE_DES_CBC_CRC,
32+
krb5.ENCTYPE_DES_CBC_MD4,
33+
krb5.ENCTYPE_DES_CBC_MD5,
34+
krb5.ENCTYPE_DES3_CBC_SHA1,
35+
krb5.ENCTYPE_AES128_CTS_HMAC_SHA1_96,
36+
krb5.ENCTYPE_AES256_CTS_HMAC_SHA1_96,
37+
krb5.ENCTYPE_ARCFOUR_HMAC,
38+
krb5.ENCTYPE_ARCFOUR_HMAC_EXP,
39+
}
40+
usage := int32(0)
41+
data := []byte("Hello")
42+
for _, enctype := range enctypes {
43+
key, err := ctx.MakeRandomKey(enctype)
44+
if err != nil {
45+
t.Errorf("ctx.MakeRandomKey(%v) failed: %v", enctype, err)
46+
continue
47+
}
48+
49+
cksum, err := ctx.MakeChecksum(krb5.SUMTYPE_DEFAULT, key, usage, data)
50+
if err != nil {
51+
t.Errorf("ctx.MakeCheckum(%v, %v) failed: %v", key, data, err)
52+
continue
53+
}
54+
55+
if sumtype, err := defaultSumTypeForEncType(enctype); err != nil {
56+
t.Errorf("defaultSumTypeForEncType(%v) failed: %v", enctype, err)
57+
} else if sumtype != cksum.SumType {
58+
t.Errorf("defaultSumTypeForEncType(%v) = %v; want %v",
59+
enctype, sumtype, cksum.SumType)
60+
}
61+
}
62+
63+
// Error-handling for some random unknown checksum.
64+
if _, err := defaultSumTypeForEncType(krb5.ENCTYPE_DES_CBC_RAW); err == nil {
65+
t.Errorf("defaultSumTypeForEncType(krb5.ENCTYPE_DES_CBC_RAW) did not fail")
66+
}
67+
}

‎log_util.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"log"
19+
)
20+
21+
// This is silly. Why doesn't the log package expose the standard
22+
// logger?
23+
24+
func logPrint(l *log.Logger, v ...interface{}) {
25+
if l != nil {
26+
l.Print(v...)
27+
} else {
28+
log.Print(v...)
29+
}
30+
}
31+
32+
func logPrintf(l *log.Logger, format string, v ...interface{}) {
33+
if l != nil {
34+
l.Printf(format, v...)
35+
} else {
36+
log.Printf(format, v...)
37+
}
38+
}
39+
40+
func logPrintln(l *log.Logger, v ...interface{}) {
41+
if l != nil {
42+
l.Println(v...)
43+
} else {
44+
log.Println(v...)
45+
}
46+
}

‎message.go

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"strings"
19+
"time"
20+
21+
"github.com/zephyr-im/krb5-go"
22+
)
23+
24+
// A Message is a high-level reassembled zepyr message. This is the
25+
// final stage of the messaging pipeline.
26+
type Message struct {
27+
Header
28+
Body []string
29+
}
30+
31+
func sendMessage(conn *Connection, msg *Message, slop int,
32+
encodeFn func(*Notice) ([]byte, error)) (*Notice, error) {
33+
// Determine the body to send.
34+
rawBody := []byte(strings.Join(msg.Body, "\x00"))
35+
rawBodyLen := len(rawBody)
36+
37+
// Special-case: if the body is empty, send one packet.
38+
if rawBodyLen == 0 {
39+
notice := &Notice{
40+
Header: msg.Header,
41+
MultiUID: msg.UID,
42+
Multipart: "0/0",
43+
}
44+
pkt, err := encodeFn(notice)
45+
if err != nil {
46+
return nil, err
47+
}
48+
return conn.SendPacket(pkt, notice.Kind, notice.UID)
49+
}
50+
51+
// First, compute how much space we have for the body.
52+
notice := &Notice{Header: msg.Header, MultiUID: msg.UID}
53+
var headerLen int
54+
pkt, err := encodeFn(notice)
55+
if err != nil {
56+
return nil, err
57+
}
58+
headerLen = len(pkt)
59+
60+
var ack *Notice
61+
uid := msg.UID
62+
offset := 0
63+
for len(rawBody) != 0 {
64+
// Compute multipart field.
65+
multipart := EncodeMultipart(offset, rawBodyLen)
66+
// Put as much of the body in as we can.
67+
remaining := MaxPacketLength - headerLen - len(multipart) - slop
68+
if len(rawBody) < remaining {
69+
remaining = len(rawBody)
70+
}
71+
// The header was too long to include the body.
72+
if remaining <= 0 {
73+
return nil, ErrPacketTooLong
74+
}
75+
76+
// Prepare the next notice.
77+
notice.UID = uid
78+
notice.Multipart = multipart
79+
notice.RawBody = rawBody[:remaining]
80+
pkt, err := encodeFn(notice)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
// Send the notice. Stop on error or SERVNAK. (The
86+
// notice might not be ACKED, so it's possible for ack
87+
// to be nil.)
88+
ack, err = conn.SendPacket(pkt, notice.Kind, notice.UID)
89+
if err != nil {
90+
return nil, err
91+
} else if ack != nil && ack.Kind != SERVACK {
92+
return ack, nil
93+
}
94+
95+
// Next packet gets a new uid.
96+
uid = MakeUID(conn.LocalAddr().IP, time.Now())
97+
rawBody = rawBody[remaining:]
98+
offset += remaining
99+
}
100+
101+
// Return the last ACK we saw.
102+
return ack, nil
103+
}
104+
105+
// SendMessage sends an authenticated message across a connection,
106+
// sharding into multiple notices as needed. It returns the ACK from
107+
// the server if the message is ACKED.
108+
func SendMessage(ctx *krb5.Context, conn *Connection, msg *Message) (*Notice, error) {
109+
// Leave some 13 bytes of slop because, if we're unlucky,
110+
// zcode may blow up the input. 13 was chosen because it's
111+
// what libzephyr uses and is above 1024 / 128.
112+
return sendMessage(conn, msg, 13, func(n *Notice) ([]byte, error) {
113+
return n.EncodePacketForServer(ctx, conn.Credential())
114+
})
115+
}
116+
117+
// SendMessageUnauth sends an unauthenticated message across a
118+
// connection, sharding into multiple notices as needed. It returns
119+
// the ACK from the server if the message is ACKED.
120+
func SendMessageUnauth(conn *Connection, msg *Message) (*Notice, error) {
121+
// No slop needed because there isn't a checksum in this notice.
122+
return sendMessage(conn, msg, 0, func(n *Notice) ([]byte, error) {
123+
return n.EncodePacketUnauth(), nil
124+
})
125+
}

‎message_test.go

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"reflect"
21+
"strings"
22+
"testing"
23+
24+
"github.com/zephyr-im/krb5-go"
25+
"github.com/zephyr-im/krb5-go/krb5test"
26+
"github.com/zephyr-im/zephyr-go/zephyrtest"
27+
)
28+
29+
func testSendMessage(t *testing.T, l int, auth AuthStatus) {
30+
hdr := fmt.Sprintf("(%d, %v)", l, auth)
31+
32+
b := []byte{}
33+
for i := 0; i < l; i++ {
34+
b = append(b, byte(i))
35+
}
36+
body := strings.Split(string(b), "\x00")
37+
38+
msg := &Message{sampleNotice().Header, body}
39+
40+
logger, lc := expectNoLogs(t)
41+
defer lc.Close()
42+
ctx, err := krb5.NewContext()
43+
if err != nil {
44+
t.Error(err)
45+
return
46+
}
47+
defer ctx.Free()
48+
49+
clock := zephyrtest.NewMockClock()
50+
client, server := mockNetwork1()
51+
conn, err := NewConnectionFull(client, serverConfig,
52+
krb5test.Credential(), logger, clock)
53+
if err != nil {
54+
t.Error(err)
55+
return
56+
}
57+
defer conn.Close()
58+
59+
// Set up a "server" to SERVACK notices as they come in.
60+
notices := make(chan *Notice, l+1)
61+
go ackAndDumpNotices(t, server, auth, notices)
62+
63+
// Send the message.
64+
var ack *Notice
65+
if auth == AuthYes {
66+
ack, err = SendMessage(ctx, conn, msg)
67+
} else {
68+
ack, err = SendMessageUnauth(conn, msg)
69+
}
70+
server.Close()
71+
72+
// Check the ACK and whatnot.
73+
if err != nil {
74+
t.Errorf("%s Error sending message: %v", hdr, err)
75+
return
76+
}
77+
if ack.Kind != SERVACK {
78+
t.Errorf("%s Received %v; want SERVACK", hdr, ack)
79+
}
80+
81+
r := NewReassembler(l)
82+
for n := range notices {
83+
if !n.MultiUID.Equal(msg.UID) {
84+
t.Errorf("%s n.MultiUID = %v; want %v",
85+
hdr, n.MultiUID, msg.UID)
86+
}
87+
if r.Done() {
88+
t.Errorf("%s r.Done() = true; want false", hdr)
89+
}
90+
if err := r.AddNotice(n, AuthYes); err != nil {
91+
t.Errorf("%s r.AddNotice(n) failed: %v", hdr, err)
92+
}
93+
}
94+
if !r.Done() {
95+
t.Errorf("%s r.Done() = false; want true", hdr)
96+
return
97+
}
98+
m, _ := r.Message()
99+
expectHeadersEqual(t, &m.Header, &msg.Header)
100+
if !reflect.DeepEqual(m.Body, msg.Body) {
101+
t.Errorf("%s m.Body = %v; want %v", hdr, m.Body, msg.Body)
102+
}
103+
}
104+
105+
func TestSendMessage(t *testing.T) {
106+
// Test 0 and all powers of 2.
107+
ls := []int{
108+
0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024,
109+
2048, 4096, 8192, 16384,
110+
}
111+
as := []AuthStatus{AuthYes, AuthNo}
112+
for _, l := range ls {
113+
for _, a := range as {
114+
testSendMessage(t, l, a)
115+
}
116+
}
117+
}
118+
119+
func TestSendMessageLongHeader(t *testing.T) {
120+
l, lc := expectNoLogs(t)
121+
defer lc.Close()
122+
clock := zephyrtest.NewMockClock()
123+
client, server := mockNetwork1()
124+
defer server.Close()
125+
conn, err := NewConnectionFull(client, serverConfig,
126+
krb5test.Credential(), l, clock)
127+
if err != nil {
128+
t.Fatal(err)
129+
}
130+
defer conn.Close()
131+
132+
msg := &Message{sampleNotice().Header, []string{"moo"}}
133+
msg.Class = "a"
134+
for i := 0; i < 1000; i++ {
135+
msg.Class += "-really"
136+
}
137+
msg.Class += "-long-class"
138+
139+
_, err = SendMessageUnauth(conn, msg)
140+
if err != ErrPacketTooLong {
141+
t.Errorf("SendMessageUnauth(conn, msg) did not fail as expected: %v", err)
142+
}
143+
}
144+
145+
func TestSendMessageNack(t *testing.T) {
146+
l, lc := expectNoLogs(t)
147+
defer lc.Close()
148+
clock := zephyrtest.NewMockClock()
149+
client, server := mockNetwork1()
150+
defer server.Close()
151+
conn, err := NewConnectionFull(client, serverConfig,
152+
krb5test.Credential(), l, clock)
153+
if err != nil {
154+
t.Fatal(err)
155+
}
156+
defer conn.Close()
157+
158+
// Set up a "server" to SERVNAK notices as they come in.
159+
go nackNotices(t, server)
160+
161+
// Send the message.
162+
msg := &Message{sampleNotice().Header, []string{"moo"}}
163+
ack, err := SendMessageUnauth(conn, msg)
164+
165+
// Check the ACK and whatnot.
166+
if err != nil {
167+
t.Fatalf("Error sending message: %v", err)
168+
}
169+
if ack.Kind != SERVNAK {
170+
t.Errorf("ack.Kind = %v; want SERVNAK", ack.Kind)
171+
}
172+
if string(ack.RawBody) != "LOST" {
173+
t.Errorf("ack.RawBody = %v; want 'LOST'", string(ack.RawBody))
174+
}
175+
}
176+
177+
func TestSendMessageSendError(t *testing.T) {
178+
l, lc := expectNoLogs(t)
179+
defer lc.Close()
180+
clock := zephyrtest.NewMockClock()
181+
readChan := make(chan zephyrtest.PacketRead)
182+
close(readChan)
183+
mock := zephyrtest.NewMockPacketConn(clientAddr, readChan)
184+
conn, err := NewConnectionFull(mock, serverConfig,
185+
krb5test.Credential(), l, clock)
186+
if err != nil {
187+
t.Fatal(err)
188+
}
189+
defer conn.Close()
190+
191+
// Set up a "server" to fail all writes.
192+
expectedErr := errors.New("failed")
193+
go func() {
194+
for write := range mock.Writes() {
195+
write.Result <- expectedErr
196+
}
197+
}()
198+
199+
// Send the message.
200+
msg := &Message{sampleNotice().Header, []string{"moo"}}
201+
if _, err := SendMessageUnauth(conn, msg); err != expectedErr {
202+
t.Errorf("SendMessageUnauth didn't fail as expected: %v", err)
203+
}
204+
}

‎notice.go

+441
Large diffs are not rendered by default.

‎notice_test.go

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"bytes"
19+
"net"
20+
"reflect"
21+
"testing"
22+
"time"
23+
)
24+
25+
func expectHeadersEqual(t *testing.T, a *Header, b *Header) {
26+
if a.Kind != b.Kind {
27+
t.Errorf("Kind = %v; want %v", a.Kind, b.Kind)
28+
}
29+
if !a.UID.Equal(b.UID) {
30+
t.Errorf("UID = %v; want %v", a.UID, b.UID)
31+
}
32+
if a.Port != b.Port {
33+
t.Errorf("Port = %v; want %v", a.Port, b.Port)
34+
}
35+
if a.Class != b.Class {
36+
t.Errorf("Class = %v; want %v", a.Class, b.Class)
37+
}
38+
if a.Instance != b.Instance {
39+
t.Errorf("Instance = %v; want %v", a.Instance, b.Instance)
40+
}
41+
if a.OpCode != b.OpCode {
42+
t.Errorf("OpCode = %v; want %v", a.OpCode, b.OpCode)
43+
}
44+
if a.Sender != b.Sender {
45+
t.Errorf("Sender = %v; want %v", a.Sender, b.Sender)
46+
}
47+
if a.Recipient != b.Recipient {
48+
t.Errorf("Recipient = %v; want %v", a.Recipient, b.Recipient)
49+
}
50+
if a.DefaultFormat != b.DefaultFormat {
51+
t.Errorf("DefaultFormat = %v; want %v", a.DefaultFormat, b.DefaultFormat)
52+
}
53+
if !a.SenderAddress.Equal(b.SenderAddress) {
54+
t.Errorf("SenderAddress = %v; want %v", a.SenderAddress, b.SenderAddress)
55+
}
56+
if a.Charset != b.Charset {
57+
t.Errorf("Charset = %v; want %v", a.Charset, b.Charset)
58+
}
59+
if !reflect.DeepEqual(a.OtherFields, b.OtherFields) {
60+
t.Errorf("OtherFields = %v; want %v", a.OtherFields, b.OtherFields)
61+
}
62+
}
63+
64+
func expectNoticesEqual(t *testing.T, a *Notice, b *Notice) {
65+
expectHeadersEqual(t, &a.Header, &b.Header)
66+
if a.Multipart != b.Multipart {
67+
t.Errorf("Multipart = %v; want %v", a.Multipart, b.Multipart)
68+
}
69+
if !a.MultiUID.Equal(b.MultiUID) {
70+
t.Errorf("MultiUID = %v; want %v", a.MultiUID, b.MultiUID)
71+
}
72+
if !bytes.Equal(a.RawBody, b.RawBody) {
73+
t.Errorf("RawBody = %q; want %q", a.RawBody, b.RawBody)
74+
}
75+
}
76+
77+
func TestDecodeUID(t *testing.T) {
78+
field := "0x1209400D 0x532A6AC4 0x0005385B"
79+
uid := stringToUID("\x12\x09\x40\x0D\x53\x2A\x6A\xC4\x00\x05\x38\x5B")
80+
if out, err := decodeUID([]byte(field)); err != nil {
81+
t.Errorf("decodeUID(%q) failed: %v", field, err)
82+
} else if string(out[:]) != string(uid[:]) {
83+
t.Errorf("decodeUID(%q) = %v; want %v", field, &uid, out)
84+
}
85+
86+
if _, err := decodeUID([]byte("0x1209400D")); err != ErrBadField {
87+
t.Errorf("decodeUID(%q) gave bad error: %v", "0x1209400D", err)
88+
}
89+
90+
if _, err := decodeUID([]byte("?" + field[1:])); err == nil {
91+
t.Errorf("decodeUID(%q) unexpected succeeded", "bogus")
92+
}
93+
}
94+
95+
func TestUID(t *testing.T) {
96+
uid := stringToUID("\x12\x09\x40\x0D\x53\x2A\x6A\xC4\x00\x05\x38\x5B")
97+
98+
if ip := uid.IP(); !ip.Equal(net.ParseIP("18.9.64.13")) {
99+
t.Errorf("uid.IP() = %v; want 18.9.64.13", ip)
100+
}
101+
102+
expectedTime := time.Unix(0x532A6AC4, 0x0005385B*1000)
103+
if time := uid.Time(); !time.Equal(expectedTime) {
104+
t.Errorf("uid.Time() = %v; want %v", time, expectedTime)
105+
}
106+
107+
if uid2 := MakeUID(uid.IP(), uid.Time()); uid2 != uid {
108+
t.Errorf("MakeUID() = %v; want %v", uid2, uid)
109+
}
110+
}
111+
112+
func TestDecodeNotice(t *testing.T) {
113+
// Test that the raw notice decodes as expected.
114+
raw := sampleRawNotice()
115+
expected := sampleNotice()
116+
if notice, err := DecodeRawNotice(raw); err != nil {
117+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
118+
} else {
119+
expectNoticesEqual(t, notice, expected)
120+
}
121+
122+
// Value in sender address takes precedence over UID value.
123+
raw.HeaderFields[17] = []byte("Z\x08\x08\x08\x08")
124+
expected.SenderAddress = net.ParseIP("8.8.8.8").To4()
125+
if notice, err := DecodeRawNotice(raw); err != nil {
126+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
127+
} else {
128+
expectNoticesEqual(t, notice, expected)
129+
}
130+
131+
raw = sampleRawNotice()
132+
expected = sampleNotice()
133+
134+
// No charset.
135+
raw.HeaderFields = raw.HeaderFields[0:18]
136+
expected.Charset = CharsetUnknown
137+
if notice, err := DecodeRawNotice(raw); err != nil {
138+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
139+
} else {
140+
expectNoticesEqual(t, notice, expected)
141+
}
142+
143+
// No sender address specified. Still get an IP address from the UID.
144+
raw.HeaderFields = raw.HeaderFields[0:17]
145+
if notice, err := DecodeRawNotice(raw); err != nil {
146+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
147+
} else {
148+
expectNoticesEqual(t, notice, expected)
149+
}
150+
151+
// No multiuid.
152+
raw.HeaderFields = raw.HeaderFields[0:16]
153+
expected.MultiUID = expected.UID
154+
expected.Multipart = ""
155+
if notice, err := DecodeRawNotice(raw); err != nil {
156+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
157+
} else {
158+
expectNoticesEqual(t, notice, expected)
159+
}
160+
161+
// No multipart.
162+
raw.HeaderFields = raw.HeaderFields[0:15]
163+
if notice, err := DecodeRawNotice(raw); err != nil {
164+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
165+
} else {
166+
expectNoticesEqual(t, notice, expected)
167+
}
168+
169+
// Extra fields.
170+
raw = sampleRawNotice()
171+
expected = sampleNotice()
172+
raw.HeaderFields = append(raw.HeaderFields, []byte("extra"))
173+
expected.OtherFields = [][]byte{[]byte("extra")}
174+
if notice, err := DecodeRawNotice(raw); err != nil {
175+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
176+
} else {
177+
expectNoticesEqual(t, notice, expected)
178+
}
179+
180+
// Test some bad packets.
181+
indices := []int{2, 3, 4, 16, 17, 18}
182+
for _, idx := range indices {
183+
raw := sampleRawNotice()
184+
raw.HeaderFields[idx] = []byte("bogus")
185+
if _, err := DecodeRawNotice(raw); err == nil {
186+
t.Errorf("Bad header %d unexpectedly succeeded", idx)
187+
}
188+
}
189+
190+
// IP parses but has a bad length.
191+
raw = sampleRawNotice()
192+
raw.HeaderFields[17] = []byte("Zabc")
193+
if _, err := DecodeRawNotice(raw); err == nil {
194+
t.Errorf("Short IP unexpectedly succeeded")
195+
}
196+
}
197+
198+
func zeroByteSlice(b []byte) {
199+
for i := range b {
200+
b[i] = 0
201+
}
202+
}
203+
204+
func TestDecodeNoticeAliasing(t *testing.T) {
205+
// Test that DecodeRawNotice's result doesn't alias the input.
206+
raw := sampleRawNotice()
207+
expected := sampleNotice()
208+
notice, err := DecodeRawNotice(raw)
209+
if err != nil {
210+
t.Errorf("DecodeRawNotice(%v) failed: %v", raw, err)
211+
} else {
212+
expectNoticesEqual(t, notice, expected)
213+
}
214+
215+
for _, h := range raw.HeaderFields {
216+
zeroByteSlice(h)
217+
}
218+
zeroByteSlice(raw.Body)
219+
220+
expectNoticesEqual(t, notice, expected)
221+
}
222+
223+
func TestEncRawNoticeUnauth(t *testing.T) {
224+
raw := sampleRawNotice()
225+
// AuthNo = 0
226+
raw.HeaderFields[5] = []byte("0x00000000")
227+
// No authenticator.
228+
raw.HeaderFields[6] = []byte("0x00000000")
229+
raw.HeaderFields[7] = []byte("")
230+
// No checksum.
231+
raw.HeaderFields[14] = []byte("")
232+
233+
if enc := sampleNotice().EncodeRawNoticeUnauth(); !reflect.DeepEqual(enc, raw) {
234+
t.Errorf("EncodeRawNoticeUnauth()\n = %v\nwant %v", enc, raw)
235+
}
236+
}

‎raw_notice.go

+386
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"bytes"
19+
"errors"
20+
"strconv"
21+
"strings"
22+
23+
"github.com/zephyr-im/krb5-go"
24+
)
25+
26+
// AuthStatus is the result of authenticating a notice.
27+
type AuthStatus uint32
28+
29+
const (
30+
// AuthFailed describes a notice which failed authentication
31+
// for some reason.
32+
AuthFailed AuthStatus = 0xffffffff
33+
// AuthYes describes an authenticated notice.
34+
AuthYes AuthStatus = 1
35+
// AuthNo describes a notice which did not claim to be
36+
// authenticated.
37+
AuthNo AuthStatus = 0
38+
)
39+
40+
func (as AuthStatus) String() string {
41+
switch as {
42+
case AuthFailed:
43+
return "AuthFailed"
44+
case AuthYes:
45+
return "AuthYes"
46+
case AuthNo:
47+
return "AuthNo"
48+
default:
49+
return strconv.FormatUint(uint64(as), 10)
50+
}
51+
}
52+
53+
// ErrBadVersionFormat is returned when a zephyr version field cannot
54+
// be parsed.
55+
var ErrBadVersionFormat = errors.New("bad version format")
56+
57+
const zephyrVersionHeader = "ZEPH"
58+
59+
// ProtocolVersionMajor and ProtocolVersionMinor are the version of
60+
// the zephyr protocol implemented by this library.
61+
const (
62+
ProtocolVersionMajor = 0
63+
ProtocolVersionMinor = 2
64+
)
65+
66+
func parseZephyrVersion(version string) (uint, uint, error) {
67+
if !strings.HasPrefix(version, zephyrVersionHeader) {
68+
return 0, 0, ErrBadVersionFormat
69+
}
70+
split := strings.SplitN(version[len(zephyrVersionHeader):], ".", 2)
71+
if len(split) != 2 {
72+
return 0, 0, ErrBadVersionFormat
73+
}
74+
major, err := strconv.ParseUint(split[0], 10, 0)
75+
if err != nil {
76+
return 0, 0, err
77+
}
78+
minor, err := strconv.ParseUint(split[1], 10, 0)
79+
if err != nil {
80+
return 0, 0, err
81+
}
82+
return uint(major), uint(minor), nil
83+
}
84+
85+
func formatZephyrVersion(major, minor uint) string {
86+
return zephyrVersionHeader +
87+
strconv.FormatUint(uint64(major), 10) +
88+
"." +
89+
strconv.FormatUint(uint64(minor), 10)
90+
}
91+
92+
// Processing a notice is done in three stages:
93+
//
94+
// - First, we split it up into raw fields and do only basic validation. Just
95+
// enough to extract the checksum, authenticator, and validate things. This stage
96+
// gives the RawNotice type.
97+
//
98+
// - Second, we decode the various fields and give back a logical notice. That
99+
// gives a Notice.
100+
//
101+
// - Third, we process MultiUID and reassemble sharded notices. Tentatively, this
102+
// will reuse the Notice struct as the only real difference is MultiUID, but
103+
// we'll see.
104+
//
105+
// Serializing a notice goes in reverse.
106+
//
107+
// TODO(davidben): When serializing, who does UID allocation, the
108+
// library or the user? If the library, it's awkward that the type is
109+
// in there. Perhaps we want a couple more types. The reassembly logic
110+
// could return a tuple of uid, message or so.
111+
112+
// Field layout
113+
const (
114+
versionIndex = iota // string
115+
numfieldsIndex // zascii32
116+
kindIndex // zascii32
117+
uidIndex // 12-byte zascii
118+
portIndex // zascii16
119+
authstatusIndex // zascii32
120+
authlenIndex // zascii32
121+
authenticatorIndex // zcode
122+
classIndex // string
123+
instanceIndex // string
124+
opcodeIndex // string
125+
senderIndex // string
126+
recipientIndex // string
127+
defaultformatIndex // string
128+
checksumIndex // zcode
129+
// Added in 1988; ZEPHYR0.2
130+
multipartIndex // string
131+
multiuidIndex // 12-byte zascii
132+
// Added in 2009; no version bump
133+
senderSockaddrIndex // zcode
134+
charsetIndex // zascii16 little-endian
135+
// Other fields
136+
numKnownFields
137+
)
138+
139+
const numRequiredFields = checksumIndex + 1
140+
141+
// libzephyr does this awkward thing where it, for purposes of
142+
// authentication checking, it assumes that everything is pointers
143+
// into the z_packet field and does C-style pointer dancing. I'd kinda
144+
// like the intermediate formats to not make assumptions like that, so
145+
// instead we'll use Split/Join being reversible. Zephyr isn't
146+
// terribly well-layered.
147+
//
148+
// Note that this does have one subtlety: we do NOT allow a missing
149+
// body. libzephyr never produces this, but when parsing, it doesn't
150+
// distinguish between
151+
//
152+
// ZEPH0.2 NUL 0x0000003 NUL blahotherfield
153+
// ZEPH0.2 NUL 0x0000003 NUL blahotherfield NUL
154+
//
155+
// (Ignore that this notice doesn't pass our minimum field count
156+
// rules.) In the former, we have three header fields and a missing
157+
// body. In the latter, we have no body. This is relevant because we
158+
// need to be able to reconstruct the concatenation of 0-13 and 15-end
159+
// for checksumming. If this becomes an issue, do something inane like
160+
// treat a nil Body as different.
161+
//
162+
// Delimiter-based serializations. They're the worst.
163+
164+
// A RawNotice is the first stage of processing a packet. The
165+
// individual header fields are parsed out to extract a checksum and
166+
// authenticator. The other fields are uninterpreted.
167+
type RawNotice struct {
168+
HeaderFields [][]byte
169+
Body []byte
170+
}
171+
172+
// ErrBadPacketFormat is returned when parsing a malformed packet.
173+
var ErrBadPacketFormat = errors.New("bad packet format")
174+
175+
// ErrBadPacketFieldCount is returned when parsing a packet with a
176+
// field count that does not match the content.
177+
var ErrBadPacketFieldCount = errors.New("bad field count")
178+
179+
// ErrBadPacketVersion is returned when parsing a packet with an
180+
// incompatible version field.
181+
var ErrBadPacketVersion = errors.New("incompatible packet version")
182+
183+
// DecodePacket records a packet into a RawNotice.
184+
func DecodePacket(packet []byte) (*RawNotice, error) {
185+
// First, split out the version and field count.
186+
fs := bytes.SplitN(packet, []byte{0}, 3)
187+
188+
// We better have at least those fields...
189+
if len(fs) < 3 {
190+
return nil, ErrBadPacketFormat
191+
}
192+
vers, numFieldsRaw, rest := fs[0], fs[1], fs[2]
193+
194+
// Like libzephyr, the minor version is ignored in parsing.
195+
if major, _, err := parseZephyrVersion(string(vers)); err != nil {
196+
return nil, err
197+
} else if major != ProtocolVersionMajor {
198+
return nil, ErrBadPacketVersion
199+
}
200+
201+
// Decode the field count.
202+
numFields, err := DecodeZAscii32(numFieldsRaw)
203+
if err != nil {
204+
return nil, err
205+
}
206+
// Pfft.
207+
numFieldsInt := int(numFields)
208+
209+
// Sanity check; just so we can't be made to allocate giant things or
210+
// something? Meh. Also require at least 15 fields (ZEPH0.1) so there's
211+
// a checksum.
212+
if numFieldsInt > len(packet) || numFieldsInt < numRequiredFields {
213+
return nil, ErrBadPacketFieldCount
214+
}
215+
216+
fields := make([][]byte, 0, numFields)
217+
fields = append(fields, vers)
218+
fields = append(fields, numFieldsRaw)
219+
220+
// Parse the remaining fields. Subtract 2 for version and numfields. Add
221+
// 1 for the remainder (the body).
222+
rs := bytes.SplitN(rest, []byte{0}, numFieldsInt-2+1)
223+
if len(rs) != numFieldsInt-2+1 {
224+
return nil, ErrBadPacketFieldCount
225+
}
226+
227+
// And assemble the RawNotice.
228+
fields = append(fields, rs[0:len(rs)-1]...)
229+
if len(fields) != numFieldsInt {
230+
panic(len(fields))
231+
}
232+
body := rs[len(rs)-1]
233+
return &RawNotice{fields, body}, nil
234+
}
235+
236+
// ErrAuthenticatorLengthMismatch is returned when processing the
237+
// authenticator on a RawNotice where the authlen field does not match
238+
// the length of the decoded authenticator.
239+
var ErrAuthenticatorLengthMismatch = errors.New("authenticator length mismatch")
240+
241+
// DecodeAuthenticator decodes the authenticator field of a RawNotice.
242+
func (r *RawNotice) DecodeAuthenticator() ([]byte, error) {
243+
// There's this length field. It's completely bogus, but may
244+
// as well assert that it's right? Be lenient if this causes
245+
// trouble.
246+
authlen, err := DecodeZAscii32(r.HeaderFields[authlenIndex])
247+
if err != nil {
248+
return nil, err
249+
}
250+
251+
// This used to be zephyrascii, but krb4 zephyr stopped
252+
// working ages ago.
253+
auth, err := DecodeZcode(r.HeaderFields[authenticatorIndex])
254+
if err != nil {
255+
return nil, err
256+
}
257+
if len(auth) != int(authlen) {
258+
return nil, ErrAuthenticatorLengthMismatch
259+
}
260+
return auth, nil
261+
}
262+
263+
// DecodeChecksum decodes the checksum field of a RawNotice.
264+
func (r *RawNotice) DecodeChecksum() ([]byte, error) {
265+
return DecodeZcode(r.HeaderFields[checksumIndex])
266+
}
267+
268+
// DecodeAuthStatus decodes the authstate field of a RawNotice.
269+
func (r *RawNotice) DecodeAuthStatus() (AuthStatus, error) {
270+
authStatus, err := DecodeZAscii32(r.HeaderFields[authstatusIndex])
271+
if err != nil {
272+
return AuthFailed, err
273+
}
274+
return AuthStatus(authStatus), nil
275+
}
276+
277+
// ChecksumPayload returns the portion of the packet that is
278+
// checksumed. (The checksum itself is removed and the remainder is
279+
// concatenated.)
280+
func (r *RawNotice) ChecksumPayload() []byte {
281+
// The part of the packet that's checksummed is really quite
282+
// absurd, but here we go.
283+
parts := make([][]byte, 0, len(r.HeaderFields))
284+
// Fields before the checkum.
285+
parts = append(parts, r.HeaderFields[0:checksumIndex]...)
286+
// Fields after the checksum.
287+
parts = append(parts, r.HeaderFields[checksumIndex+1:]...)
288+
// Body.
289+
parts = append(parts, r.Body)
290+
return bytes.Join(parts, []byte{0})
291+
}
292+
293+
func (r *RawNotice) checkAuth(
294+
ctx *krb5.Context,
295+
key *krb5.KeyBlock,
296+
usage int32,
297+
) (AuthStatus, error) {
298+
sumtype, err := defaultSumTypeForEncType(key.EncType)
299+
if err != nil {
300+
return AuthFailed, err
301+
}
302+
303+
checksumData, err := r.DecodeChecksum()
304+
if err != nil {
305+
return AuthFailed, err
306+
}
307+
308+
checksum := &krb5.Checksum{sumtype, checksumData}
309+
310+
result, err := ctx.VerifyChecksum(key, usage, r.ChecksumPayload(), checksum)
311+
if err != nil {
312+
return AuthFailed, err
313+
} else if !result {
314+
return AuthFailed, nil
315+
} else {
316+
return AuthYes, nil
317+
}
318+
}
319+
320+
// CheckAuthFromServer is called by a client to check a packet from
321+
// the server using a previously negotiated key.
322+
func (r *RawNotice) CheckAuthFromServer(
323+
ctx *krb5.Context,
324+
key *krb5.KeyBlock,
325+
) (AuthStatus, error) {
326+
if authStatus, err := r.DecodeAuthStatus(); err != nil {
327+
return AuthFailed, err
328+
} else if authStatus != AuthYes {
329+
return authStatus, nil
330+
}
331+
332+
return r.checkAuth(ctx, key, keyUsageServerCksum)
333+
}
334+
335+
// CheckAuthFromClient is called by a server to check a packet from a
336+
// client using a server KeyTab. If successful, the session key from
337+
// the client's authenticator is returned.
338+
func (r *RawNotice) CheckAuthFromClient(
339+
ctx *krb5.Context,
340+
service *krb5.Principal,
341+
keytab *krb5.KeyTab,
342+
) (AuthStatus, *krb5.KeyBlock, error) {
343+
if authStatus, err := r.DecodeAuthStatus(); err != nil {
344+
return AuthFailed, nil, err
345+
} else if authStatus != AuthYes {
346+
return authStatus, nil, nil
347+
}
348+
349+
authcon, err := ctx.NewAuthContext()
350+
if err != nil {
351+
return AuthFailed, nil, err
352+
}
353+
defer authcon.Free()
354+
authcon.SetUseTimestamps(false)
355+
356+
authent, err := r.DecodeAuthenticator()
357+
if err != nil {
358+
return AuthFailed, nil, err
359+
}
360+
361+
if err := authcon.ReadRequest(authent, service, keytab); err != nil {
362+
return AuthFailed, nil, err
363+
}
364+
key, err := authcon.SessionKey()
365+
if err != nil {
366+
return AuthFailed, nil, err
367+
}
368+
369+
auth, err := r.checkAuth(ctx, key, keyUsageClientCksum)
370+
if err != nil {
371+
return AuthFailed, nil, err
372+
}
373+
return auth, key, nil
374+
}
375+
376+
// EncodePacket encodes a RawNotice as a packet. If it is to be
377+
// authenticated, the checksum and (if a client) authenticator fields
378+
// must be already populated.
379+
func (r *RawNotice) EncodePacket() []byte {
380+
// This function does not check that r.HeaderFields[1] is correct or
381+
// anything. The caller is expected to provide a legal RawNotice.
382+
parts := make([][]byte, 0, len(r.HeaderFields)+1)
383+
parts = append(parts, r.HeaderFields...)
384+
parts = append(parts, r.Body)
385+
return bytes.Join(parts, []byte{0})
386+
}

‎raw_notice_test.go

+344
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"bytes"
19+
"reflect"
20+
"strings"
21+
"testing"
22+
23+
"github.com/zephyr-im/krb5-go"
24+
"github.com/zephyr-im/krb5-go/krb5test"
25+
)
26+
27+
func makeServerContextAndKeyTab(t *testing.T) (*krb5.Context, *krb5.KeyTab) {
28+
ctx, err := krb5.NewContext()
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
keytab, err := krb5test.MakeServerKeyTab(ctx)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
return ctx, keytab
37+
}
38+
39+
func TestParseZephyrVersion(t *testing.T) {
40+
if major, minor, err := parseZephyrVersion("ZEPH0.2"); err != nil {
41+
t.Errorf("parseZephyrVersion(\"ZEPH0.2\") failed: %v", err)
42+
} else if major != 0 || minor != 2 {
43+
t.Errorf("parseZephyrVersion(\"ZEPH0.2\") = %v, %v; want %v, %v",
44+
major, minor, 0, 2)
45+
}
46+
47+
if vers := formatZephyrVersion(0, 2); vers != "ZEPH0.2" {
48+
t.Errorf("formatZephyrVersion(0, 2) = %v; want \"ZEPH0.2\"", vers)
49+
}
50+
51+
badVersion := []string{
52+
"BOGUS4.2",
53+
"ZEPHYR0.2",
54+
"ZEPH0.1.2",
55+
"zeph0.2",
56+
"ZEPHa.b",
57+
"ZEPH",
58+
"ZEPH-1.-2",
59+
}
60+
for _, version := range badVersion {
61+
if major, minor, err := parseZephyrVersion(version); err == nil {
62+
t.Errorf("parseZephyrVersion(%q) unexpectedly succeeded: %v, %v",
63+
version, major, minor)
64+
}
65+
}
66+
}
67+
68+
// Test some completely bogus packets.
69+
func TestDecodeBadPackets(t *testing.T) {
70+
type bad struct {
71+
pkt []byte
72+
err error
73+
}
74+
badPackets := []bad{
75+
// Bogus.
76+
bad{[]byte(""), ErrBadPacketFormat},
77+
// Bad version.
78+
bad{[]byte("Blah\x00Blah\x00Blah"), ErrBadVersionFormat},
79+
// Major version mismatch.
80+
bad{[]byte("ZEPH1.2\x000x00000002\x00hi"), ErrBadPacketVersion},
81+
// Bad field count zephyrascii.
82+
bad{[]byte("ZEPH0.2\x00two\x00hi"), nil},
83+
// Too few fields.
84+
bad{[]byte("ZEPH0.2\x000x00000002\x00hi"), ErrBadPacketFieldCount},
85+
// Too few fields.
86+
bad{makeTestPacket(1, "0x0000000E"), ErrBadPacketFieldCount},
87+
// Field count too high.
88+
bad{makeTestPacket(1, "0x00000020"), ErrBadPacketFieldCount},
89+
// Giant field count.
90+
bad{makeTestPacket(1, "0xFFFFFFFF"), ErrBadPacketFieldCount},
91+
}
92+
for _, test := range badPackets {
93+
if raw, err := DecodePacket([]byte(test.pkt)); err == nil {
94+
t.Errorf("DecodePacket(%q) = %v; want error", test.pkt, raw)
95+
} else if test.err != nil && err != test.err {
96+
t.Errorf("DecodePacket(%q) failed with %v; want %v",
97+
test.pkt, err, test.err)
98+
}
99+
}
100+
}
101+
102+
func TestDecodePacket(t *testing.T) {
103+
pkt := samplePacket()
104+
expectedRaw := sampleRawNotice()
105+
if raw, err := DecodePacket(pkt); err != nil {
106+
t.Errorf("DecodePacket(%q) failed: %v", string(pkt), err)
107+
} else if !reflect.DeepEqual(raw, expectedRaw) {
108+
t.Errorf("DecodePacket(%q) = %v\nwant %v", string(pkt), raw, expectedRaw)
109+
}
110+
111+
// Packets with 15 fields are okay.
112+
pkt = makeTestPacket(1, "0x0000000F")
113+
if _, err := DecodePacket(pkt); err != nil {
114+
t.Errorf("DecodePacket(%q) failed: %v", string(pkt), err)
115+
}
116+
117+
// Packets with extra fields are okay.
118+
pkt = makeTestPacket(1, "0x00000014")
119+
if _, err := DecodePacket(pkt); err != nil {
120+
t.Errorf("DecodePacket(%q) failed: %v", string(pkt), err)
121+
}
122+
}
123+
124+
func TestDecodeAuthenticator(t *testing.T) {
125+
raw := sampleRawNotice()
126+
raw.HeaderFields[6] = []byte("0x00000004")
127+
raw.HeaderFields[7] = []byte("Ztest")
128+
129+
if auth, err := raw.DecodeAuthenticator(); err != nil {
130+
t.Errorf("raw.DecodeAuthenticator() failed: %v", err)
131+
} else if string(auth) != "test" {
132+
t.Errorf("raw.DecodeAuthenticator() = %q; want %q", auth, "test")
133+
}
134+
135+
// Bogus length.
136+
raw.HeaderFields[6] = []byte("bogus")
137+
if _, err := raw.DecodeAuthenticator(); err == nil {
138+
t.Errorf("raw.DecodeAuthenticator() unexpected succeeded")
139+
}
140+
141+
// Mismatch length.
142+
raw.HeaderFields[6] = []byte("0xDEADBEEF")
143+
if _, err := raw.DecodeAuthenticator(); err == nil {
144+
t.Errorf("raw.DecodeAuthenticator() unexpected succeeded")
145+
} else if err != ErrAuthenticatorLengthMismatch {
146+
t.Errorf("raw.DecodeAuthenticator() gave the wrong error: %v", err)
147+
}
148+
149+
// Bad zcode.
150+
raw.HeaderFields[7] = []byte("notvalidzcode")
151+
if _, err := raw.DecodeAuthenticator(); err == nil {
152+
t.Errorf("raw.DecodeAuthenticator() unexpected succeeded")
153+
}
154+
}
155+
156+
func TestDecodeChecksum(t *testing.T) {
157+
raw := sampleRawNotice()
158+
159+
if cksum, err := raw.DecodeChecksum(); err != nil {
160+
t.Errorf("raw.DecodeChecksum() failed: %v", err)
161+
} else if !bytes.Equal(cksum, sampleChecksum()) {
162+
t.Errorf("raw.DecodeChecksum() = %q; want %q", cksum, sampleChecksum())
163+
}
164+
165+
// Bad zcode.
166+
raw.HeaderFields[14] = []byte("notvalidzcode")
167+
if _, err := raw.DecodeChecksum(); err == nil {
168+
t.Errorf("raw.DecodeChecksum() unexpected succeeded")
169+
}
170+
}
171+
172+
func TestDecodeAuthStatus(t *testing.T) {
173+
raw := sampleRawNotice()
174+
175+
if auth, err := raw.DecodeAuthStatus(); err != nil {
176+
t.Errorf("raw.DecodeAuthStatus() failed: %v", err)
177+
} else if auth != AuthYes {
178+
t.Errorf("raw.DecodeAuthStatus() = %q; want %q", auth, AuthYes)
179+
}
180+
181+
// Bad authstatus.
182+
raw.HeaderFields[5] = []byte("notvalidzascii")
183+
if _, err := raw.DecodeAuthStatus(); err == nil {
184+
t.Errorf("raw.DecodeAuthStatus() unexpected succeeded")
185+
}
186+
}
187+
188+
func TestEncodePacket(t *testing.T) {
189+
raw := sampleRawNotice()
190+
expected := string(samplePacket())
191+
if enc := string(raw.EncodePacket()); enc != expected {
192+
t.Errorf("raw.EncodePacket() = %q; want %q", enc, expected)
193+
}
194+
}
195+
196+
func TestChecksumPayload(t *testing.T) {
197+
raw := sampleRawNotice()
198+
expected := strings.Replace(string(samplePacket()),
199+
string(sampleChecksumZcode())+"\x00", "", 1)
200+
if enc := string(raw.ChecksumPayload()); enc != expected {
201+
t.Errorf("raw.ChecksumPayload() = %q; want %q", enc, expected)
202+
}
203+
}
204+
205+
func TestCheckAuthFromServer(t *testing.T) {
206+
raw := sampleRawNotice()
207+
key := sampleKeyBlock()
208+
ctx, err := krb5.NewContext()
209+
if err != nil {
210+
t.Fatal(err)
211+
}
212+
defer ctx.Free()
213+
214+
if auth, err := raw.CheckAuthFromServer(ctx, key); err != nil {
215+
t.Errorf("raw.CheckAuthFromServer() failed: %v", err)
216+
} else if auth != AuthYes {
217+
t.Errorf("raw.CheckAuthFromServer() = %v; want %v", auth, AuthYes)
218+
}
219+
220+
// Break some random thing.
221+
raw.HeaderFields[0] = []byte("moooo")
222+
if auth, err := raw.CheckAuthFromServer(ctx, key); err != nil {
223+
t.Errorf("raw.CheckAuthFromServer() failed: %v", err)
224+
} else if auth != AuthFailed {
225+
t.Errorf("raw.CheckAuthFromServer() = %v; want %v", auth, AuthFailed)
226+
}
227+
228+
// Doesn't claim authentication.
229+
raw = sampleRawNotice()
230+
raw.HeaderFields[5] = []byte("0x00000000")
231+
if auth, err := raw.CheckAuthFromServer(ctx, key); err != nil {
232+
t.Errorf("raw.CheckAuthFromServer() failed: %v", err)
233+
} else if auth != AuthNo {
234+
t.Errorf("raw.CheckAuthFromServer() = %v; want %v", auth, AuthNo)
235+
}
236+
237+
// Bogus authstatus.
238+
raw.HeaderFields[5] = []byte("bogus")
239+
if _, err := raw.CheckAuthFromServer(ctx, key); err == nil {
240+
t.Errorf("raw.CheckAuthFromServer() unexpectedly ran")
241+
}
242+
243+
// Checksum length error.
244+
raw = sampleRawNotice()
245+
raw.HeaderFields[14] = []byte("Zasdf")
246+
if _, err := raw.CheckAuthFromServer(ctx, key); err == nil {
247+
t.Errorf("raw.CheckAuthFromServer() unexpectedly ran")
248+
}
249+
250+
// Bogus checksum.
251+
raw.HeaderFields[14] = []byte("bogus")
252+
if _, err := raw.CheckAuthFromServer(ctx, key); err == nil {
253+
t.Errorf("raw.CheckAuthFromServer() unexpectedly ran")
254+
}
255+
}
256+
257+
func TestClientToServerAuth(t *testing.T) {
258+
// The client makes the notice.
259+
clientCtx, err := krb5.NewContext()
260+
if err != nil {
261+
t.Fatal(err)
262+
}
263+
defer clientCtx.Free()
264+
notice := sampleNotice()
265+
raw, err := notice.EncodeRawNoticeForServer(
266+
clientCtx, krb5test.Credential())
267+
if err != nil {
268+
t.Fatalf("notice.EncodeRawNoticeForServer failed: %v", err)
269+
}
270+
271+
// Server checks the notice.
272+
serverCtx, keytab := makeServerContextAndKeyTab(t)
273+
defer serverCtx.Free()
274+
defer keytab.Close()
275+
auth, key, err := raw.CheckAuthFromClient(serverCtx, krb5test.Service(), keytab)
276+
if err != nil {
277+
t.Errorf("CheckAuthFromClient failed: %v", err)
278+
} else {
279+
if auth != AuthYes {
280+
t.Errorf("CheckAuthFromClient returned %v; want AuthYes", auth)
281+
}
282+
if !reflect.DeepEqual(key, krb5test.SessionKey()) {
283+
t.Errorf("CheckAuthFromClient return %v; want %v",
284+
key, krb5test.SessionKey())
285+
}
286+
}
287+
288+
// Perturb the checksum a little. It should fail now.
289+
cksum, err := raw.DecodeChecksum()
290+
if err != nil {
291+
t.Fatal(err)
292+
}
293+
cksum[0]++
294+
raw.HeaderFields[14] = EncodeZcode(cksum)
295+
auth, _, err = raw.CheckAuthFromClient(serverCtx, krb5test.Service(), keytab)
296+
if err != nil {
297+
t.Errorf("CheckAuthFromClient failed: %v", err)
298+
} else if auth != AuthFailed {
299+
t.Errorf("CheckAuthFromClient returned %v; want AuthFailed", auth)
300+
}
301+
302+
// Malformed checksums also fail.
303+
raw.HeaderFields[14] = []byte("bogus")
304+
auth, _, _ = raw.CheckAuthFromClient(serverCtx, krb5test.Service(), keytab)
305+
if auth != AuthFailed {
306+
t.Errorf("CheckAuthFromClient returned %v; want AuthFailed", auth)
307+
}
308+
309+
// An unauthenticated packet should return unauthenticated.
310+
raw = notice.EncodeRawNoticeUnauth()
311+
auth, _, err = raw.CheckAuthFromClient(serverCtx, krb5test.Service(), keytab)
312+
if err != nil {
313+
t.Errorf("CheckAuthFromClient failed: %v", err)
314+
} else if auth != AuthNo {
315+
t.Errorf("CheckAuthFromClient returned %v; want AuthNo", auth)
316+
}
317+
318+
// Malformed authstatus fields error.
319+
raw.HeaderFields[5] = []byte("bogus")
320+
auth, _, _ = raw.CheckAuthFromClient(serverCtx, krb5test.Service(), keytab)
321+
if auth != AuthFailed {
322+
t.Errorf("CheckAuthFromClient returned %v; want AuthFailed", auth)
323+
}
324+
325+
// Bad authenticator fails.
326+
raw, err = notice.EncodeRawNoticeForServer(
327+
clientCtx, krb5test.Credential())
328+
if err != nil {
329+
t.Fatalf("notice.EncodeRawNoticeForServer failed: %v", err)
330+
}
331+
raw.HeaderFields[6] = []byte("0x00000005")
332+
raw.HeaderFields[7] = []byte("Z12345")
333+
auth, _, _ = raw.CheckAuthFromClient(serverCtx, krb5test.Service(), keytab)
334+
if auth != AuthFailed {
335+
t.Errorf("CheckAuthFromClient returned %v; want AuthFailed", auth)
336+
}
337+
338+
// Malformed authenticator fails.
339+
raw.HeaderFields[7] = []byte("Bogus")
340+
auth, _, _ = raw.CheckAuthFromClient(serverCtx, krb5test.Service(), keytab)
341+
if auth != AuthFailed {
342+
t.Errorf("CheckAuthFromClient returned %v; want AuthFailed", auth)
343+
}
344+
}

‎reader.go

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"io"
19+
"log"
20+
"net"
21+
"time"
22+
23+
"github.com/zephyr-im/krb5-go"
24+
)
25+
26+
// MaxPacketLength is the maximum size of a zephyr notice on the wire.
27+
const MaxPacketLength = 1024
28+
29+
// A RawReaderResult is an output of a ReadRawNotices call. It either
30+
// contains a RawNotice and a source address or an error.
31+
type RawReaderResult struct {
32+
RawNotice *RawNotice
33+
Addr net.Addr
34+
}
35+
36+
// ReadRawNotices decodes packets from a PacketConn into RawNotices
37+
// and returns a stream of them. Non-fatal errors are returned through
38+
// the stream. On a fatal error or EOF, the channel is closed.
39+
func ReadRawNotices(conn net.PacketConn, logger *log.Logger) <-chan RawReaderResult {
40+
sink := make(chan RawReaderResult)
41+
go readRawNoticeLoop(conn, logger, sink)
42+
return sink
43+
}
44+
45+
func readRawNoticeLoop(
46+
conn net.PacketConn,
47+
logger *log.Logger,
48+
sink chan<- RawReaderResult,
49+
) {
50+
defer close(sink)
51+
var buf [MaxPacketLength]byte
52+
var tempDelay time.Duration
53+
for {
54+
n, addr, err := conn.ReadFrom(buf[:])
55+
if err != nil {
56+
// Send the error out to the consumer.
57+
if err != io.EOF {
58+
logPrintf(logger, "Error reading packet: %v\n", err)
59+
}
60+
if ne, ok := err.(net.Error); ok && ne.Temporary() {
61+
// Delay logic from net/http.Serve.
62+
if tempDelay == 0 {
63+
tempDelay = 5 * time.Millisecond
64+
} else {
65+
tempDelay *= 2
66+
}
67+
if max := 1 * time.Second; tempDelay > max {
68+
tempDelay = max
69+
}
70+
time.Sleep(tempDelay)
71+
continue
72+
}
73+
break
74+
}
75+
tempDelay = 0
76+
77+
// Copy the packet so we can reuse the buffer.
78+
raw, err := DecodePacket(copyByteSlice(buf[0:n]))
79+
if err != nil {
80+
logPrintf(logger, "Error decoding notice: %v\n", err)
81+
continue
82+
}
83+
sink <- RawReaderResult{raw, addr}
84+
}
85+
}
86+
87+
// A NoticeReaderResult is an output of a ReadNoticesFromServer
88+
// call. It either contains a notice with authentication status and
89+
// source address or an error.
90+
type NoticeReaderResult struct {
91+
Notice *Notice
92+
AuthStatus AuthStatus
93+
Addr net.Addr
94+
}
95+
96+
// ReadNoticesFromServer decodes and authenticates notices sent from
97+
// the server. Returns a channel containing authenticated notices and
98+
// errors. The channel is closed on fatal errors. If key is nil, all
99+
// notices appear as AuthFailed.
100+
func ReadNoticesFromServer(
101+
conn net.PacketConn,
102+
key *krb5.KeyBlock,
103+
logger *log.Logger,
104+
) <-chan NoticeReaderResult {
105+
// TODO(davidben): Should this channel be buffered a little?
106+
sink := make(chan NoticeReaderResult)
107+
go readNoticeLoop(ReadRawNotices(conn, logger), key, logger, sink)
108+
return sink
109+
}
110+
111+
func readNoticeLoop(
112+
rawReader <-chan RawReaderResult,
113+
key *krb5.KeyBlock,
114+
logger *log.Logger,
115+
sink chan<- NoticeReaderResult,
116+
) {
117+
defer close(sink)
118+
ctx, err := krb5.NewContext()
119+
if err != nil {
120+
logPrintf(logger, "Error creating krb5 context: %v", err)
121+
return
122+
}
123+
defer ctx.Free()
124+
for r := range rawReader {
125+
notice, err := DecodeRawNotice(r.RawNotice)
126+
if err != nil {
127+
logPrintf(logger, "Error parsing notice: %v", err)
128+
continue
129+
}
130+
131+
authStatus := AuthFailed
132+
if notice.Kind.IsACK() {
133+
// Don't bother; ACKs' auth bits are always lies.
134+
authStatus = AuthNo
135+
} else if key != nil {
136+
authStatus, err = r.RawNotice.CheckAuthFromServer(ctx, key)
137+
if err != nil {
138+
logPrintf(logger, "Error authenticating notice: %v", err)
139+
authStatus = AuthFailed
140+
}
141+
}
142+
sink <- NoticeReaderResult{notice, authStatus, r.Addr}
143+
}
144+
}

‎reader_test.go

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"bufio"
19+
"errors"
20+
"io"
21+
"log"
22+
"net"
23+
"reflect"
24+
"testing"
25+
26+
"github.com/zephyr-im/krb5-go"
27+
"github.com/zephyr-im/zephyr-go/zephyrtest"
28+
)
29+
30+
func newTestLogger() (*log.Logger, io.Closer, <-chan string) {
31+
pr, pw := io.Pipe()
32+
l := log.New(pw, "", 0)
33+
c := make(chan string)
34+
go func() {
35+
s := bufio.NewScanner(pr)
36+
for s.Scan() {
37+
c <- s.Text()
38+
}
39+
// Meh.
40+
if err := s.Err(); err != nil {
41+
c <- "Error scanning: " + err.Error()
42+
}
43+
close(c)
44+
}()
45+
return l, pw, c
46+
}
47+
48+
func expectNoLogs(t *testing.T) (*log.Logger, io.Closer) {
49+
l, closer, c := newTestLogger()
50+
go func() {
51+
for line := range c {
52+
t.Error(line)
53+
}
54+
}()
55+
return l, closer
56+
}
57+
58+
func TestReadNoticesFromServer(t *testing.T) {
59+
addr1 := &net.UDPAddr{IP: net.IPv4(1, 1, 1, 1), Port: 1111}
60+
addr2 := &net.UDPAddr{IP: net.IPv4(2, 2, 2, 2), Port: 2222}
61+
clientAddr := &net.UDPAddr{IP: net.IPv4(3, 3, 3, 3), Port: 3333}
62+
fatalErr := errors.New("polarity insufficiently reversed")
63+
64+
type resultOrErr struct {
65+
r *NoticeReaderResult
66+
line string
67+
}
68+
result := func(n *Notice, as AuthStatus, addr net.Addr) resultOrErr {
69+
return resultOrErr{r: &NoticeReaderResult{n, as, addr}}
70+
}
71+
err := func(line string) resultOrErr {
72+
return resultOrErr{line: line}
73+
}
74+
75+
type test struct {
76+
keyblock *krb5.KeyBlock
77+
reads []zephyrtest.PacketRead
78+
expected []resultOrErr
79+
}
80+
tests := []test{
81+
// Basic case
82+
{
83+
sampleKeyBlock(),
84+
[]zephyrtest.PacketRead{
85+
{samplePacket(), addr1, nil},
86+
}, []resultOrErr{
87+
result(sampleNotice(), AuthYes, addr1),
88+
},
89+
},
90+
// Various non-fatal errors.
91+
{
92+
sampleKeyBlock(),
93+
[]zephyrtest.PacketRead{
94+
{nil, nil, zephyrtest.TemporaryError},
95+
{samplePacket(), addr1, nil},
96+
{nil, nil, zephyrtest.TemporaryError},
97+
{nil, nil, zephyrtest.TemporaryError},
98+
{samplePacket(), addr2, nil},
99+
{sampleFailPacket(), addr1, nil},
100+
{sampleMalformedChecksumPacket(), addr2, nil},
101+
{sampleMalformedPortPacket(), addr1, nil},
102+
{[]byte("bogus"), addr2, nil},
103+
{samplePacket(), addr1, nil},
104+
},
105+
[]resultOrErr{
106+
err("Error reading packet: Temporary error"),
107+
result(sampleNotice(), AuthYes, addr1),
108+
err("Error reading packet: Temporary error"),
109+
err("Error reading packet: Temporary error"),
110+
// samplePacket
111+
result(sampleNotice(), AuthYes, addr2),
112+
// sampleFailPacket
113+
result(sampleNotice(), AuthFailed, addr1),
114+
// sampleMalformedChecksumPacket
115+
err("Error authenticating notice: invalid zcode"),
116+
result(sampleNotice(), AuthFailed, addr2),
117+
// sampleMalformedPortPacket
118+
err("Error parsing notice: bad length for " +
119+
"uint16 zephyrascii"),
120+
// bogus
121+
err("Error decoding notice: bad packet format"),
122+
// samplePacket
123+
result(sampleNotice(), AuthYes, addr1),
124+
},
125+
},
126+
// Stop after fatal error.
127+
{
128+
sampleKeyBlock(),
129+
[]zephyrtest.PacketRead{
130+
{nil, nil, fatalErr},
131+
{samplePacket(), addr1, nil},
132+
},
133+
[]resultOrErr{
134+
err("Error reading packet: polarity " +
135+
"insufficiently reversed"),
136+
},
137+
},
138+
// nil key.
139+
{
140+
nil,
141+
[]zephyrtest.PacketRead{
142+
{samplePacket(), addr1, nil},
143+
}, []resultOrErr{
144+
result(sampleNotice(), AuthFailed, addr1),
145+
},
146+
},
147+
}
148+
for ti, test := range tests {
149+
// Buffer of 1 because one of the tests intentionally
150+
// has an extra read.
151+
readChan := make(chan zephyrtest.PacketRead, 1)
152+
go func() {
153+
for _, read := range test.reads {
154+
readChan <- read
155+
}
156+
close(readChan)
157+
}()
158+
mock := zephyrtest.NewMockPacketConn(clientAddr, readChan)
159+
l, closer, lines := newTestLogger()
160+
out := ReadNoticesFromServer(mock, test.keyblock, l)
161+
for ei, expect := range test.expected {
162+
if expect.r != nil {
163+
if r, ok := <-out; !ok {
164+
t.Errorf("%d.%d. Expected notice: %v",
165+
ti, ei, expect.r)
166+
} else {
167+
expectNoticesEqual(t, r.Notice, expect.r.Notice)
168+
if r.AuthStatus != expect.r.AuthStatus {
169+
t.Errorf("%d.%d. AuthStatus = %v; want %v",
170+
ti, ei,
171+
r.AuthStatus,
172+
expect.r.AuthStatus)
173+
}
174+
if !reflect.DeepEqual(r.Addr, expect.r.Addr) {
175+
t.Errorf("%d.%d. Addr = %v; want %v",
176+
ti, ei,
177+
r.Addr,
178+
expect.r.Addr)
179+
}
180+
}
181+
} else {
182+
if line, ok := <-lines; !ok {
183+
t.Errorf("%d.%d. Expected error: %v",
184+
ti, ei, expect.line)
185+
} else if line != expect.line {
186+
t.Errorf("%d.%d. line = %v; wanted %v",
187+
ti, ei, line, expect.line)
188+
}
189+
}
190+
}
191+
closer.Close()
192+
for line := range lines {
193+
t.Errorf("%d. unexpected line: %v", ti, line)
194+
}
195+
for r := range out {
196+
t.Errorf("%d. unexpected notice: %v", ti, r)
197+
}
198+
}
199+
}

‎reassembly.go

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright 2014 The zephyr-go authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package zephyr
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"strconv"
21+
"strings"
22+
)
23+
24+
// MaxMessageBodyLength is the maximum length of a reassembled message
25+
// body.
26+
const MaxMessageBodyLength = 400000
27+
28+
// ErrBodyTooLong is returned if the reassembled body is too long.
29+
var ErrBodyTooLong = errors.New("reassembled body too long")
30+
31+
// ErrBodyLengthMismatch is returned if a notice's body length field
32+
// is incompatible the reassembler being used.
33+
var ErrBodyLengthMismatch = errors.New("reassembled body length mismatch")
34+
35+
// ErrBodyFragmentOutOfBounds is returned if a notice's body is out of
36+
// bounds of the reassembled message body.
37+
var ErrBodyFragmentOutOfBounds = errors.New("message fragment out of bounds")
38+
39+
// ParseMultipart parses the multipart field of a notice. It returns
40+
// the part and partof parts of the field. On parse error, it returns
41+
// 0 and the length of the body.
42+
func ParseMultipart(n *Notice) (int, int) {
43+
ss := strings.Split(n.Multipart, "/")
44+
if len(ss) != 2 {
45+
return 0, len(n.RawBody)
46+
}
47+
part, err := strconv.ParseInt(ss[0], 10, 0)
48+
if err != nil || part < 0 {
49+
return 0, len(n.RawBody)
50+
}
51+
partof, err := strconv.ParseInt(ss[1], 10, 0)
52+
if err != nil || partof < 0 {
53+
return 0, len(n.RawBody)
54+
}
55+
if part >= partof {
56+
return 0, len(n.RawBody)
57+
}
58+
return int(part), int(partof)
59+
}
60+
61+
// EncodeMultipart encodes a pair of integers for the multipart field.
62+
func EncodeMultipart(part, partof int) string {
63+
return fmt.Sprintf("%d/%d", part, partof)
64+
}
65+
66+
type chunk struct {
67+
offset int
68+
buf []byte
69+
}
70+
71+
func (c chunk) end() int {
72+
return c.offset + len(c.buf)
73+
}
74+
75+
// Reassembler maintains state for a reassembled notice.
76+
type Reassembler struct {
77+
length int
78+
// We maintain a list of chunks that are ordered and separated
79+
// by gaps. When completed, there is exactly one chunk. This
80+
// differs from the libzephyr strategy of allocating a buffer
81+
// ahead of time to be slightly less of a DoS vector.
82+
chunks []chunk
83+
header Header
84+
haveHeader bool
85+
// TODO(davidben): This is using libzephyr's behavior. After
86+
// this is working, experiment with just including the
87+
// AuthStatus into the key. The main concern is problems with
88+
// the zhm retransmit bug.
89+
authStatus AuthStatus
90+
}
91+
92+
// NewReassembler creates a Reassembler for a message with a given
93+
// body length.
94+
func NewReassembler(length int) *Reassembler {
95+
return &Reassembler{length, []chunk{}, Header{}, false, AuthYes}
96+
}
97+
98+
// NewReassemblerFromMultipartField creates a Reassembler for a given
99+
// notice's multipart field. Note that this does not call AddNotice.
100+
func NewReassemblerFromMultipartField(n *Notice) (*Reassembler, error) {
101+
_, partof := ParseMultipart(n)
102+
if partof > MaxMessageBodyLength {
103+
return nil, ErrBodyTooLong
104+
}
105+
return NewReassembler(partof), nil
106+
}
107+
108+
// TODO(davidben): Add serialization/deserialization methods for crazy
109+
// fault-tolerant Roost version.
110+
111+
// Done returns true when the message body has been reassembled.
112+
func (r *Reassembler) Done() bool {
113+
if !r.haveHeader {
114+
return false
115+
}
116+
if r.length == 0 {
117+
return true
118+
}
119+
return len(r.chunks) == 1 && r.chunks[0].offset == 0 &&
120+
len(r.chunks[0].buf) == r.length
121+
}
122+
123+
// Message returns the reassembled message once it is done.
124+
func (r *Reassembler) Message() (*Message, AuthStatus) {
125+
if !r.Done() {
126+
return nil, AuthFailed
127+
}
128+
if r.length == 0 {
129+
return &Message{r.header, []string{""}}, r.authStatus
130+
}
131+
return &Message{r.header, strings.Split(string(r.chunks[0].buf), "\x00")}, r.authStatus
132+
}
133+
134+
// AddNotice adds a notice into the reassembler state. If the notice
135+
// is incompatible and discarded, it returns an error.
136+
func (r *Reassembler) AddNotice(n *Notice, authStatus AuthStatus) error {
137+
if r.Done() {
138+
return nil
139+
}
140+
141+
// Check if this notice is compatible.
142+
part, partof := ParseMultipart(n)
143+
if partof != r.length {
144+
return ErrBodyLengthMismatch
145+
}
146+
if part+len(n.RawBody) > r.length {
147+
return ErrBodyFragmentOutOfBounds
148+
}
149+
150+
// Incorporate the AuthStatus.
151+
if authStatus == AuthFailed {
152+
r.authStatus = AuthFailed
153+
} else if authStatus == AuthNo && r.authStatus != AuthFailed {
154+
r.authStatus = AuthNo
155+
}
156+
157+
// Copy the header over.
158+
if part == 0 {
159+
r.header = n.Header
160+
r.haveHeader = true
161+
}
162+
163+
if len(n.RawBody) == 0 {
164+
return nil
165+
}
166+
167+
// Fill in the new chunks. First we insert our new chunk in order.
168+
ordered := []chunk{}
169+
added := false
170+
for _, c := range r.chunks {
171+
if c.offset > part && !added {
172+
added = true
173+
ordered = append(ordered, chunk{part, n.RawBody})
174+
}
175+
ordered = append(ordered, c)
176+
}
177+
if !added {
178+
ordered = append(ordered, chunk{part, n.RawBody})
179+
}
180+
181+
// Now collapse chunks that touch.
182+
dedup := []chunk{}
183+
for _, c := range ordered {
184+
if len(dedup) == 0 || dedup[len(dedup)-1].end() < c.offset {
185+
dedup = append(dedup, c)
186+
} else {
187+
// Merge c into last by appending the last n
188+
// bytes of c.
189+
last := dedup[len(dedup)-1]
190+
if n := c.end() - last.end(); n > 0 {
191+
dedup[len(dedup)-1] = chunk{
192+
last.offset,
193+
append(last.buf, c.buf[len(c.buf)-n:]...)}
194+
}
195+
}
196+
}
197+
r.chunks = dedup
198+
return nil
199+
}

0 commit comments

Comments
 (0)
Please sign in to comment.