github.com/la5nta/wl2k-go@v0.11.8/fbb/wl2k.go (about) 1 // Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved. 2 // Use of this source code is governed by the MIT-license that can be 3 // found in the LICENSE file. 4 5 // fbb provides a client-side implementation of the B2 Forwarding Protocol 6 // and Winlink 2000 Message Structure for transfer of messages to and from 7 // a Winlink 2000 Radio Email Server (RMS) gateway. 8 package fbb 9 10 import ( 11 "bufio" 12 "errors" 13 "fmt" 14 "io" 15 "log" 16 "net" 17 "os" 18 "sort" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/la5nta/wl2k-go/transport" 24 ) 25 26 // ErrConnLost is returned by Session.Exchange if the connection is prematurely closed. 27 var ErrConnLost = errors.New("connection lost") 28 29 // Objects implementing the MBoxHandler interface can be used to handle inbound and outbound messages for a Session. 30 type MBoxHandler interface { 31 InboundHandler 32 OutboundHandler 33 34 // Prepare is called before any other operation in a session. 35 // 36 // The returned error can be used to indicate that the mailbox is 37 // not ready for a new session, the error will be forwarded to the 38 // remote node. 39 Prepare() error 40 } 41 42 // An OutboundHandler offer messages that can be delivered (a proposal) to the remote node and is notified when a message is sent or defered. 43 type OutboundHandler interface { 44 // GetOutbound should return all pending (outbound) messages addressed to (and only to) one of the fw addresses. 45 // 46 // No fw address implies that the remote node could be a Winlink CMS and all oubound 47 // messages can be delivered through the connected node. 48 GetOutbound(fw ...Address) (out []*Message) 49 50 // SetSent should mark the the message identified by MID as successfully sent. 51 // 52 // If rejected is true, it implies that the remote node has already received the message. 53 SetSent(MID string, rejected bool) 54 55 // SetDeferred should mark the outbound message identified by MID as deferred. 56 // 57 // SetDeferred is called when the remote want's to receive the proposed message 58 // (see MID) later. 59 SetDeferred(MID string) 60 } 61 62 // An InboundHandler handles all messages that can/is sent from the remote node. 63 type InboundHandler interface { 64 // ProcessInbound should persist/save/process all messages received (msgs) returning an error if the operation was unsuccessful. 65 // 66 // The error will be delivered (if possble) to the remote to indicate that an error has occurred. 67 ProcessInbound(msg ...*Message) error 68 69 // GetInboundAnswer should return a ProposalAnwer (Accept/Reject/Defer) based on the remote's message Proposal p. 70 // 71 // An already successfully received message (see MID) should be rejected. 72 GetInboundAnswer(p Proposal) ProposalAnswer 73 } 74 75 // Session represents a B2F exchange session. 76 // 77 // A session should only be used once. 78 type Session struct { 79 mycall string 80 targetcall string 81 locator string 82 motd []string 83 84 h MBoxHandler 85 statusUpdater StatusUpdater 86 87 // Callback when secure login password is needed 88 secureLoginHandleFunc func(addr Address) (password string, err error) 89 90 master bool 91 robustMode robustMode 92 93 remoteSID sid 94 remoteFW []Address // Addresses the remote requests messages on behalf of 95 localFW []Address // Addresses we request messages on behalf of 96 97 trafficStats TrafficStats 98 99 quitReceived bool 100 quitSent bool 101 remoteNoMsgs bool // True if last remote turn had no more messages 102 103 rd *bufio.Reader 104 105 log *log.Logger 106 pLog *log.Logger 107 ua UserAgent 108 } 109 110 // Struct used to hold information that is reported during B2F handshake. 111 // 112 // Non of the fields must contain a dash (-). 113 // 114 type UserAgent struct { 115 Name string 116 Version string 117 } 118 119 type StatusUpdater interface { 120 UpdateStatus(s Status) 121 } 122 123 // Status holds information about ongoing transfers. 124 type Status struct { 125 Receiving *Proposal 126 Sending *Proposal 127 BytesTransferred int 128 BytesTotal int 129 Done bool 130 When time.Time 131 } 132 133 // TrafficStats holds exchange message traffic statistics. 134 type TrafficStats struct { 135 Received []string // Received message MIDs. 136 Sent []string // Sent message MIDs. 137 } 138 139 var StdLogger = log.New(os.Stderr, "", log.LstdFlags) 140 var StdUA = UserAgent{Name: "wl2kgo", Version: "0.1a"} 141 142 // Constructs a new Session object. 143 // 144 // The Handler can be nil (but no messages will be exchanged). 145 // 146 // Mycall and targetcall will be upper-cased. 147 func NewSession(mycall, targetcall, locator string, h MBoxHandler) *Session { 148 mycall, targetcall = strings.ToUpper(mycall), strings.ToUpper(targetcall) 149 150 return &Session{ 151 mycall: mycall, 152 localFW: []Address{AddressFromString(mycall)}, 153 targetcall: targetcall, 154 log: StdLogger, 155 h: h, 156 pLog: StdLogger, 157 ua: StdUA, 158 locator: locator, 159 trafficStats: TrafficStats{ 160 Received: make([]string, 0), 161 Sent: make([]string, 0), 162 }, 163 } 164 } 165 166 type robustMode int 167 168 // The different robust-mode settings. 169 const ( 170 RobustAuto robustMode = iota // Run the connection in robust-mode when not transferring outbound messages. 171 RobustForced // Always run the connection in robust-mode. 172 RobustDisabled // Never run the connection in robust-mode. 173 ) 174 175 // SetRobustMode sets the RobustMode for this exchange. 176 // 177 // The mode is ignored if the exchange connection does not implement the transport.Robust interface. 178 // 179 // Default is RobustAuto. 180 func (s *Session) SetRobustMode(mode robustMode) { 181 s.robustMode = mode 182 //TODO: If NewSession took the net.Conn (not Exchange), we could return an error here to indicate that the operation was unsupported. 183 } 184 185 // SetMOTD sets one or more lines to be sent before handshake. 186 // 187 // The MOTD is only sent if the local node is session master. 188 func (s *Session) SetMOTD(line ...string) { s.motd = line } 189 190 // IsMaster sets whether this end should initiate the handshake. 191 func (s *Session) IsMaster(isMaster bool) { s.master = isMaster } 192 193 // RemoteSID returns the remote's SID (if available). 194 func (s *Session) RemoteSID() string { return string(s.remoteSID) } 195 196 // Exchange is the main method for exchanging messages with a remote over the B2F protocol. 197 // 198 // Sends outbound messages and downloads inbound messages prepared for this session. 199 // 200 // Outbound messages should be added as proposals before calling the Exchange() method. 201 // 202 // If conn implements the transport.Robust interface, the connection is run in robust-mode 203 // except when an outbound message is transferred. 204 // 205 // After Exchange(), messages that was accepted and delivered successfully to the RMS is 206 // available through a call to Sent(). Messages downloaded successfully from the RMS is 207 // retrieved by calling Received(). 208 // 209 // The connection is closed at the end of the exchange. If the connection is closed before 210 // the exchange is done, ErrConnLost is returned. 211 // 212 // Subsequent Exchange calls on the same session is a noop. 213 func (s *Session) Exchange(conn net.Conn) (stats TrafficStats, err error) { 214 if s.Done() { 215 return stats, nil 216 } 217 218 // Experimental support for fetching messages only for auxiliary addresses (not mycall). 219 // Ref https://groups.google.com/g/pat-users/c/5G1JIEyFXe4 220 if t, _ := strconv.ParseBool(os.Getenv("FW_AUX_ONLY_EXPERIMENT")); t && len(s.localFW) > 1 { 221 s.localFW = s.localFW[1:] 222 s.log.Printf("FW_AUX_ONLY_EXPERIMENT: Requesting messages for %v", s.localFW) 223 } 224 225 // The given conn should always be closed after returning from this method. 226 // If an error occurred, echo it to the remote. 227 defer func() { 228 defer conn.Close() 229 switch { 230 case err == nil: 231 // Success :-) 232 return 233 case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF): 234 // Connection closed prematurely by modem (link failure) or 235 // remote peer. 236 err = ErrConnLost 237 case errors.Is(err, net.ErrClosed): 238 // Closed locally, but still... 239 err = ErrConnLost 240 default: 241 // Probably a protocol related error. 242 // Echo the error to the remote peer and disconnect. 243 conn.SetDeadline(time.Now().Add(time.Minute)) 244 fmt.Fprintf(conn, "*** %s\r\n", err) 245 } 246 }() 247 248 // Prepare mailbox handler 249 if s.h != nil { 250 err = s.h.Prepare() 251 if err != nil { 252 return 253 } 254 } 255 256 // Set connection's robust-mode according to setting 257 if r, ok := conn.(transport.Robust); ok { 258 r.SetRobust(s.robustMode != RobustDisabled) 259 defer r.SetRobust(false) 260 } 261 262 s.rd = bufio.NewReader(conn) 263 264 err = s.handshake(conn) 265 if err != nil { 266 return 267 } 268 269 if gzipExperimentEnabled() && s.remoteSID.Has(sGzip) { 270 s.log.Println("GZIP_EXPERIMENT:", "Gzip compression enabled in this session.") 271 } 272 273 for myTurn := !s.master; !s.Done(); myTurn = !myTurn { 274 if myTurn { 275 s.quitSent, err = s.handleOutbound(conn) 276 } else { 277 s.quitReceived, err = s.handleInbound(conn) 278 } 279 280 if err != nil { 281 return s.trafficStats, err 282 } 283 } 284 285 return s.trafficStats, conn.Close() 286 } 287 288 // Done() returns true if either parties have existed from this session. 289 func (s *Session) Done() bool { return s.quitReceived || s.quitSent } 290 291 // Waits for connection to be closed, returning an error if seen on the line. 292 func waitRemoteHangup(conn net.Conn) error { 293 conn.SetDeadline(time.Now().Add(time.Minute)) 294 295 scanner := bufio.NewScanner(conn) 296 for scanner.Scan() { 297 line := scanner.Text() 298 299 if err := errLine(line); err != nil { 300 conn.Close() 301 return err 302 } 303 log.Println(line) 304 } 305 return scanner.Err() 306 } 307 308 func remoteErr(str string) error { 309 if !strings.HasPrefix(str, "***") { 310 return nil 311 } 312 313 idx := strings.LastIndex(str, "*") 314 if idx+1 >= len(str) { 315 return nil 316 } 317 318 return fmt.Errorf(strings.TrimSpace(str[idx+1:])) 319 } 320 321 // Mycall returns this stations call sign. 322 func (s *Session) Mycall() string { return s.mycall } 323 324 // Targetcall returns the remote stations call sign (if known). 325 func (s *Session) Targetcall() string { return s.targetcall } 326 327 // SetSecureLoginHandleFunc registers a callback function used to prompt for password when a secure login challenge is received. 328 func (s *Session) SetSecureLoginHandleFunc(f func(addr Address) (password string, err error)) { 329 s.secureLoginHandleFunc = f 330 } 331 332 // This method returns the call signs the remote is requesting traffic on behalf of. The call signs are not available until 333 // the handshake is done. 334 // 335 // It will typically be the call sign of the remote P2P station and empty when the remote is a Winlink CMS. 336 func (s *Session) RemoteForwarders() []Address { return s.remoteFW } 337 338 // AddAuxiliaryAddress adds one or more addresses to request messages on behalf of. 339 // 340 // Currently the Winlink System only support requesting messages for call signs, not full email addresses. 341 func (s *Session) AddAuxiliaryAddress(aux ...Address) { s.localFW = append(s.localFW, aux...) } 342 343 // Set callback for status updates on receiving / sending messages 344 func (s *Session) SetStatusUpdater(updater StatusUpdater) { s.statusUpdater = updater } 345 346 // Sets custom logger. 347 func (s *Session) SetLogger(logger *log.Logger) { 348 if logger == nil { 349 logger = StdLogger 350 } 351 s.log = logger 352 s.pLog = logger 353 354 } 355 356 // Set this session's user agent 357 func (s *Session) SetUserAgent(ua UserAgent) { s.ua = ua } 358 359 // Get this session's user agent 360 func (s *Session) UserAgent() UserAgent { return s.ua } 361 362 func (s *Session) outbound() []*Proposal { 363 if s.h == nil { 364 return []*Proposal{} 365 } 366 367 msgs := s.h.GetOutbound(s.remoteFW...) 368 props := make([]*Proposal, 0, len(msgs)) 369 370 for _, m := range msgs { 371 // It seems reasonable to ignore these with a warning 372 if err := m.Validate(); err != nil { 373 s.log.Printf("Ignoring invalid outbound message '%s': %s", m.MID(), err) 374 continue 375 } 376 377 prop, err := m.Proposal(s.highestPropCode()) 378 if err != nil { 379 s.log.Printf("Unable to prepare proposal for '%s'. Corrupt message? Ignoring...", m.MID()) 380 continue 381 } 382 383 props = append(props, prop) 384 } 385 386 sortProposals(props) 387 return props 388 } 389 390 func sortProposals(props []*Proposal) { 391 // sort first by ascending size, then stable sort by descending precedence 392 sort.Sort(bySize(props)) 393 sort.Stable(byPrecedence(props)) 394 } 395 396 type bySize []*Proposal 397 398 func (s bySize) Len() int { return len(s) } 399 func (s bySize) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 400 func (s bySize) Less(i, j int) bool { 401 if s[i].compressedSize != s[j].compressedSize { 402 return s[i].compressedSize < s[j].compressedSize 403 } 404 return s[i].MID() < s[j].MID() 405 } 406 407 type byPrecedence []*Proposal 408 409 func (s byPrecedence) Len() int { return len(s) } 410 func (s byPrecedence) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 411 func (s byPrecedence) Less(i, j int) bool { 412 return s[i].precedence() < s[j].precedence() 413 } 414 415 func (s *Session) highestPropCode() PropCode { 416 if s.remoteSID.Has(sGzip) && gzipExperimentEnabled() { 417 return GzipProposal 418 } 419 return Wl2kProposal 420 }