github.com/la5nta/wl2k-go@v0.11.8/fbb/handshake.go (about)

     1  // Copyright 2015 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  package fbb
     6  
     7  import (
     8  	"bufio"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"regexp"
    14  	"strings"
    15  )
    16  
    17  var ErrNoFB2 = errors.New("Remote does not support B2 Forwarding Protocol")
    18  
    19  // IsLoginFailure returns a boolean indicating whether the error is known to
    20  // report that the secure login failed.
    21  func IsLoginFailure(err error) bool {
    22  	if err == nil {
    23  		return false
    24  	}
    25  	errStr := strings.ToLower(err.Error())
    26  	return strings.Contains(errStr, "secure login failed")
    27  }
    28  
    29  func (s *Session) handshake(rw io.ReadWriter) error {
    30  	if s.master {
    31  		// Send MOTD lines
    32  		for _, line := range s.motd {
    33  			fmt.Fprintf(rw, "%s\r", line)
    34  		}
    35  
    36  		if err := s.sendHandshake(rw, ""); err != nil {
    37  			return err
    38  		}
    39  	}
    40  
    41  	hs, err := s.readHandshake()
    42  	if err != nil {
    43  		return err
    44  	}
    45  
    46  	// Did we get SID codes?
    47  	if hs.SID == "" {
    48  		return errors.New("No sid in handshake")
    49  	}
    50  
    51  	s.remoteSID = hs.SID
    52  	s.remoteFW = hs.FW
    53  
    54  	if !s.master {
    55  		return s.sendHandshake(rw, hs.SecureChallenge)
    56  	} else {
    57  		return nil
    58  	}
    59  }
    60  
    61  type handshakeData struct {
    62  	SID             sid
    63  	FW              []Address
    64  	SecureChallenge string
    65  }
    66  
    67  func (s *Session) readHandshake() (handshakeData, error) {
    68  	data := handshakeData{}
    69  
    70  	for {
    71  		if bytes, err := s.rd.Peek(1); err != nil {
    72  			return data, err
    73  		} else if bytes[0] == 'F' && s.master {
    74  			return data, nil // Next line is a protocol command, handshake is done
    75  		}
    76  
    77  		// Ignore remote errors here, as the server sometimes sends lines like
    78  		// '*** MTD Stats Total connects = 2580 Total messages = 3900', which
    79  		// are not errors
    80  		line, err := s.nextLineRemoteErr(false)
    81  		if err != nil {
    82  			return data, err
    83  		}
    84  
    85  		//REVIEW: We should probably be more strict on what to allow here,
    86  		// to ensure we disconnect early if the remote is not talking the expected
    87  		// protocol. (We should at least allow unknown ; prefixed lines aka "comments")
    88  		switch {
    89  		// Header with sid (ie. [WL2K-2.8.4.8-B2FWIHJM$])
    90  		case isSID(line):
    91  			data.SID, err = parseSID(line)
    92  			if err != nil {
    93  				return data, err
    94  			}
    95  
    96  			// Do we support the remote's SID codes?
    97  			if !data.SID.Has(sFBComp2) { // We require FBB compressed protocol v2 for now
    98  				return data, ErrNoFB2
    99  			}
   100  		case strings.HasPrefix(line, ";FW"): // Forwarders
   101  			data.FW, err = parseFW(line)
   102  			if err != nil {
   103  				return data, err
   104  			}
   105  		case strings.HasPrefix(line, ";PQ"): // Secure password challenge
   106  			data.SecureChallenge = line[5:]
   107  
   108  		case strings.HasSuffix(line, ">"): // Prompt
   109  			return data, nil
   110  		default:
   111  			// Ignore
   112  		}
   113  	}
   114  }
   115  
   116  func (s *Session) sendHandshake(writer io.Writer, secureChallenge string) error {
   117  	if secureChallenge != "" && s.secureLoginHandleFunc == nil {
   118  		return errors.New("Got secure login challenge, please register a SecureLoginHandleFunc.")
   119  	}
   120  
   121  	w := bufio.NewWriter(writer)
   122  
   123  	// Request messages on behalf of every localFW
   124  	fmt.Fprintf(w, ";FW:")
   125  	for i, addr := range s.localFW {
   126  		switch {
   127  		case secureChallenge != "" && i > 0:
   128  			// Include passwordhash for auxiliary addresses (required by WL2K-4.x or later)
   129  			if password, _ := s.secureLoginHandleFunc(addr); password != "" {
   130  				resp := secureLoginResponse(secureChallenge, password)
   131  				// In the B2F specs they use space as delimiter, but Winlink Express uses pipe.
   132  				// I'm not sure space as a delimiter would even work when passwords for aux addresses
   133  				// are optional (according to the very same document).
   134  				fmt.Fprintf(w, " %s|%s", addr.Addr, resp)
   135  				break
   136  			}
   137  			// Password is not required for all aux addresses according to Winlink's B2F specs.
   138  			fallthrough
   139  		default:
   140  			fmt.Fprintf(w, " %s", addr.Addr)
   141  		}
   142  	}
   143  	fmt.Fprintf(w, "\r")
   144  
   145  	writeSID(w, s.ua.Name, s.ua.Version)
   146  
   147  	if secureChallenge != "" {
   148  		password, err := s.secureLoginHandleFunc(s.localFW[0])
   149  		if err != nil {
   150  			return err
   151  		}
   152  		resp := secureLoginResponse(secureChallenge, password)
   153  		writeSecureLoginResponse(w, resp)
   154  	}
   155  
   156  	fmt.Fprintf(w, "; %s DE %s (%s)", s.targetcall, s.mycall, s.locator)
   157  	if s.master {
   158  		fmt.Fprintf(w, ">\r")
   159  	} else {
   160  		fmt.Fprintf(w, "\r")
   161  	}
   162  
   163  	return w.Flush()
   164  }
   165  
   166  func parseFW(line string) ([]Address, error) {
   167  	if !strings.HasPrefix(line, ";FW: ") {
   168  		return nil, errors.New("Malformed forward line")
   169  	}
   170  
   171  	fws := strings.Split(line[5:], " ")
   172  	addrs := make([]Address, 0, len(fws))
   173  
   174  	for _, str := range strings.Split(line[5:], " ") {
   175  		str = strings.Split(str, "|")[0] // Strip password hashes (unsupported)
   176  		addrs = append(addrs, AddressFromString(str))
   177  	}
   178  
   179  	return addrs, nil
   180  }
   181  
   182  type sid string
   183  
   184  const localSID = sFBComp2 + sFBBasic + sHL + sMID + sBID
   185  
   186  // The SID codes
   187  const (
   188  	sAckForPM   = "A"  // Acknowledge for person messages
   189  	sFBBasic    = "F"  // FBB basic ascii protocol supported
   190  	sFBComp0    = "B"  // FBB compressed protocol v0 supported
   191  	sFBComp1    = "B1" // FBB compressed protocol v1 supported
   192  	sFBComp2    = "B2" // FBB compressed protocol v2 (aka B2F) supported
   193  	sHL         = "H"  // Hierarchical Location designators supported
   194  	sMID        = "M"  // Message identifier supported
   195  	sCompBatchF = "X"  // Compressed batch forwarding supported
   196  	sI          = "I"  // "Identify"? Palink-unix sends ";target de mycall QTC n" when remote has this
   197  	sBID        = "$"  // BID supported (must be last character in SID)
   198  
   199  	sGzip = "G" // Gzip compressed messages supported (GZIP_EXPERIMENT)
   200  )
   201  
   202  func gzipExperimentEnabled() bool { return os.Getenv("GZIP_EXPERIMENT") == "1" }
   203  
   204  func writeSID(w io.Writer, appName, appVersion string) error {
   205  	sid := localSID
   206  
   207  	if gzipExperimentEnabled() {
   208  		sid = sid[0:len(sid)-1] + sGzip + sid[len(sid)-1:]
   209  	}
   210  
   211  	_, err := fmt.Fprintf(w, "[%s-%s-%s]\r", appName, appVersion, sid)
   212  	return err
   213  }
   214  
   215  func writeSecureLoginResponse(w io.Writer, response string) error {
   216  	_, err := fmt.Fprintf(w, ";PR: %s\r", response)
   217  	return err
   218  }
   219  
   220  func isSID(str string) bool {
   221  	return strings.HasPrefix(str, `[`) && strings.HasSuffix(str, `]`)
   222  }
   223  
   224  func parseSID(str string) (sid, error) {
   225  	code := regexp.MustCompile(`\[.*-(.*)\]`).FindStringSubmatch(str)
   226  	if len(code) != 2 {
   227  		return sid(""), errors.New(`Bad SID line: ` + str)
   228  	}
   229  
   230  	return sid(
   231  		strings.ToUpper(code[len(code)-1]),
   232  	), nil
   233  }
   234  
   235  func (s sid) Has(code string) bool {
   236  	return strings.Contains(string(s), strings.ToUpper(code))
   237  }