File: handshake.go

package info (click to toggle)
golang-github-la5nta-wl2k-go 0.11.9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,856 kB
  • sloc: ansic: 14; makefile: 2
file content (237 lines) | stat: -rw-r--r-- 6,352 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// Copyright 2015 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.

package fbb

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"strings"
)

var ErrNoFB2 = errors.New("Remote does not support B2 Forwarding Protocol")

// IsLoginFailure returns a boolean indicating whether the error is known to
// report that the secure login failed.
func IsLoginFailure(err error) bool {
	if err == nil {
		return false
	}
	errStr := strings.ToLower(err.Error())
	return strings.Contains(errStr, "secure login failed")
}

func (s *Session) handshake(rw io.ReadWriter) error {
	if s.master {
		// Send MOTD lines
		for _, line := range s.motd {
			fmt.Fprintf(rw, "%s\r", line)
		}

		if err := s.sendHandshake(rw, ""); err != nil {
			return err
		}
	}

	hs, err := s.readHandshake()
	if err != nil {
		return err
	}

	// Did we get SID codes?
	if hs.SID == "" {
		return errors.New("No sid in handshake")
	}

	s.remoteSID = hs.SID
	s.remoteFW = hs.FW

	if !s.master {
		return s.sendHandshake(rw, hs.SecureChallenge)
	} else {
		return nil
	}
}

type handshakeData struct {
	SID             sid
	FW              []Address
	SecureChallenge string
}

func (s *Session) readHandshake() (handshakeData, error) {
	data := handshakeData{}

	for {
		if bytes, err := s.rd.Peek(1); err != nil {
			return data, err
		} else if bytes[0] == 'F' && s.master {
			return data, nil // Next line is a protocol command, handshake is done
		}

		// Ignore remote errors here, as the server sometimes sends lines like
		// '*** MTD Stats Total connects = 2580 Total messages = 3900', which
		// are not errors
		line, err := s.nextLineRemoteErr(false)
		if err != nil {
			return data, err
		}

		//REVIEW: We should probably be more strict on what to allow here,
		// to ensure we disconnect early if the remote is not talking the expected
		// protocol. (We should at least allow unknown ; prefixed lines aka "comments")
		switch {
		// Header with sid (ie. [WL2K-2.8.4.8-B2FWIHJM$])
		case isSID(line):
			data.SID, err = parseSID(line)
			if err != nil {
				return data, err
			}

			// Do we support the remote's SID codes?
			if !data.SID.Has(sFBComp2) { // We require FBB compressed protocol v2 for now
				return data, ErrNoFB2
			}
		case strings.HasPrefix(line, ";FW"): // Forwarders
			data.FW, err = parseFW(line)
			if err != nil {
				return data, err
			}
		case strings.HasPrefix(line, ";PQ"): // Secure password challenge
			data.SecureChallenge = line[5:]

		case strings.HasSuffix(line, ">"): // Prompt
			return data, nil
		default:
			// Ignore
		}
	}
}

func (s *Session) sendHandshake(writer io.Writer, secureChallenge string) error {
	if secureChallenge != "" && s.secureLoginHandleFunc == nil {
		return errors.New("Got secure login challenge, please register a SecureLoginHandleFunc.")
	}

	w := bufio.NewWriter(writer)

	// Request messages on behalf of every localFW
	fmt.Fprintf(w, ";FW:")
	for i, addr := range s.localFW {
		switch {
		case secureChallenge != "" && i > 0:
			// Include passwordhash for auxiliary addresses (required by WL2K-4.x or later)
			if password, _ := s.secureLoginHandleFunc(addr); password != "" {
				resp := secureLoginResponse(secureChallenge, password)
				// In the B2F specs they use space as delimiter, but Winlink Express uses pipe.
				// I'm not sure space as a delimiter would even work when passwords for aux addresses
				// are optional (according to the very same document).
				fmt.Fprintf(w, " %s|%s", addr.Addr, resp)
				break
			}
			// Password is not required for all aux addresses according to Winlink's B2F specs.
			fallthrough
		default:
			fmt.Fprintf(w, " %s", addr.Addr)
		}
	}
	fmt.Fprintf(w, "\r")

	writeSID(w, s.ua.Name, s.ua.Version)

	if secureChallenge != "" {
		password, err := s.secureLoginHandleFunc(s.localFW[0])
		if err != nil {
			return err
		}
		resp := secureLoginResponse(secureChallenge, password)
		writeSecureLoginResponse(w, resp)
	}

	fmt.Fprintf(w, "; %s DE %s (%s)", s.targetcall, s.mycall, s.locator)
	if s.master {
		fmt.Fprintf(w, ">\r")
	} else {
		fmt.Fprintf(w, "\r")
	}

	return w.Flush()
}

func parseFW(line string) ([]Address, error) {
	if !strings.HasPrefix(line, ";FW: ") {
		return nil, errors.New("Malformed forward line")
	}

	fws := strings.Split(line[5:], " ")
	addrs := make([]Address, 0, len(fws))

	for _, str := range strings.Split(line[5:], " ") {
		str = strings.Split(str, "|")[0] // Strip password hashes (unsupported)
		addrs = append(addrs, AddressFromString(str))
	}

	return addrs, nil
}

type sid string

const localSID = sFBComp2 + sFBBasic + sHL + sMID + sBID

// The SID codes
const (
	sAckForPM   = "A"  // Acknowledge for person messages
	sFBBasic    = "F"  // FBB basic ascii protocol supported
	sFBComp0    = "B"  // FBB compressed protocol v0 supported
	sFBComp1    = "B1" // FBB compressed protocol v1 supported
	sFBComp2    = "B2" // FBB compressed protocol v2 (aka B2F) supported
	sHL         = "H"  // Hierarchical Location designators supported
	sMID        = "M"  // Message identifier supported
	sCompBatchF = "X"  // Compressed batch forwarding supported
	sI          = "I"  // "Identify"? Palink-unix sends ";target de mycall QTC n" when remote has this
	sBID        = "$"  // BID supported (must be last character in SID)

	sGzip = "G" // Gzip compressed messages supported (GZIP_EXPERIMENT)
)

func gzipExperimentEnabled() bool { return os.Getenv("GZIP_EXPERIMENT") == "1" }

func writeSID(w io.Writer, appName, appVersion string) error {
	sid := localSID

	if gzipExperimentEnabled() {
		sid = sid[0:len(sid)-1] + sGzip + sid[len(sid)-1:]
	}

	_, err := fmt.Fprintf(w, "[%s-%s-%s]\r", appName, appVersion, sid)
	return err
}

func writeSecureLoginResponse(w io.Writer, response string) error {
	_, err := fmt.Fprintf(w, ";PR: %s\r", response)
	return err
}

func isSID(str string) bool {
	return strings.HasPrefix(str, `[`) && strings.HasSuffix(str, `]`)
}

func parseSID(str string) (sid, error) {
	code := regexp.MustCompile(`\[.*-(.*)\]`).FindStringSubmatch(str)
	if len(code) != 2 {
		return sid(""), errors.New(`Bad SID line: ` + str)
	}

	return sid(
		strings.ToUpper(code[len(code)-1]),
	), nil
}

func (s sid) Has(code string) bool {
	return strings.Contains(string(s), strings.ToUpper(code))
}