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  }