github.com/la5nta/wl2k-go@v0.11.8/fbb/b2f.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  	"bytes"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"log"
    14  	"mime"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/la5nta/wl2k-go/transport"
    20  )
    21  
    22  var ErrOffsetLimitExceeded error = errors.New("Protocol does not support offset larger than 6 digits")
    23  
    24  const (
    25  	ProtocolOffsetSizeLimit = 999999
    26  	MaxBlockSize            = 5
    27  
    28  	// Paclink-unix uses 250, protocol maximum is 255, but we use 125 to allow use of AX.25 links with a paclen of 128.
    29  	// TODO: Consider setting this dynamically.
    30  	MaxMsgLength = 125
    31  )
    32  
    33  const (
    34  	cmdPrefix = 'F'
    35  	cmdPrompt = '>'
    36  
    37  	cmdNoMoreMessages = 'F'
    38  	cmdQuit           = 'Q'
    39  	cmdPropAnswer     = 'S'
    40  
    41  	cmdPropA = 'A'
    42  	cmdPropB = 'B'
    43  	cmdPropC = 'C' // Wl2k extended B2 message
    44  
    45  	cmdPropD = 'D' // Gzip compressed B2 message (GZIP_EXPERIMENT)
    46  )
    47  
    48  const (
    49  	_CHRNUL byte = 0
    50  	_CHRSOH      = 1
    51  	_CHRSTX      = 2
    52  	_CHREOT      = 4
    53  )
    54  
    55  func (s *Session) handleOutbound(rw io.ReadWriter) (quitSent bool, err error) {
    56  	var sent map[string]bool
    57  
    58  	// Send outbound messages
    59  	if len(s.outbound()) > 0 {
    60  		sent, err = s.sendOutbound(rw)
    61  		if err != nil {
    62  			return
    63  		}
    64  	}
    65  
    66  	// Report rejected now, they can safely be omitted even if an error occures
    67  	for mid, rej := range sent {
    68  		if rej {
    69  			s.h.SetSent(mid, rej)
    70  			delete(sent, mid)
    71  		}
    72  	}
    73  
    74  	// If all messages was deferred/rejected, we should propose new messages
    75  	if len(sent) == 0 && len(s.outbound()) > 0 {
    76  		return s.handleOutbound(rw)
    77  	}
    78  
    79  	// Handle session turnover
    80  	switch {
    81  	case len(sent) > 0:
    82  		// Turnover is implied
    83  	case s.remoteNoMsgs && len(sent) == 0:
    84  		s.pLog.Print(">FQ")
    85  		fmt.Fprint(rw, "FQ\r")
    86  		quitSent = true
    87  		return // No need to check for remote error since we did not send any messages
    88  	default:
    89  		s.pLog.Print(">FF")
    90  		fmt.Fprint(rw, "FF\r")
    91  	}
    92  
    93  	// Error reporting from remote is not defined by the protocol,
    94  	// but usually indicated by sending a line prefixed with '***'.
    95  	// The only valid bytes (according to protocol) after a session
    96  	// turnover is 'F' or ';', so we use those to confirm the block
    97  	// was successfully received.
    98  	var p []byte
    99  	if p, err = s.rd.Peek(1); err != nil {
   100  		return
   101  	} else if p[0] != 'F' && p[0] != ';' {
   102  		var line string
   103  		line, err = s.nextLine()
   104  		if err != nil {
   105  			return
   106  		}
   107  		err = fmt.Errorf("Unexpected response: '%s'", line)
   108  		return
   109  	}
   110  
   111  	// Report successfully sent messages
   112  	for mid, rej := range sent {
   113  		s.h.SetSent(mid, rej)
   114  		if !rej {
   115  			s.trafficStats.Sent = append(s.trafficStats.Sent, mid)
   116  		}
   117  	}
   118  	return
   119  }
   120  
   121  func (s *Session) sendOutbound(rw io.ReadWriter) (sent map[string]bool, err error) {
   122  	sent = make(map[string]bool) // Use this to keep track of sent (rejected or not) mids.
   123  	var checksum int64
   124  
   125  	outbound := s.outbound()
   126  	if len(outbound) > MaxBlockSize {
   127  		outbound = outbound[0:MaxBlockSize]
   128  	}
   129  
   130  	for _, prop := range outbound {
   131  		sp := fmt.Sprintf("F%c %s %s %d %d %d",
   132  			prop.code,           // Proposal code
   133  			prop.msgType,        // Message type (1 or 2 alphanumeric)
   134  			prop.mid,            // Max 12 characters
   135  			prop.size,           // Uncompressed size of message
   136  			prop.compressedSize, // Compressed size of message
   137  			0)                   // ?
   138  
   139  		s.pLog.Printf(">%s", sp)
   140  		fmt.Fprintf(rw, "%s\r", sp)
   141  		for _, c := range sp {
   142  			checksum += int64(c)
   143  		}
   144  		checksum += int64('\r')
   145  	}
   146  	checksum = (-checksum) & 0xff
   147  
   148  	s.log.Printf(`Sending checksum %02X`, checksum)
   149  	fmt.Fprintf(rw, "F> %02X\r", checksum)
   150  
   151  	var reply string
   152  	for reply == "" {
   153  		line, err := s.nextLine()
   154  		switch {
   155  		case err != nil:
   156  			return sent, err
   157  		case strings.HasPrefix(line, "FS "):
   158  			reply = line // The expected proposal answer
   159  		case strings.HasPrefix(line, ";"):
   160  			continue // Ignore comment
   161  		default:
   162  			return sent, fmt.Errorf("Expected proposal answer from remote. Got: '%s'", reply)
   163  		}
   164  	}
   165  
   166  	if err = parseProposalAnswer(reply, outbound, s.log); err != nil {
   167  		return sent, fmt.Errorf("Unable to parse proposal answer: %w", err)
   168  	}
   169  
   170  	if len(outbound) == 0 {
   171  		return
   172  	}
   173  
   174  	if r, ok := rw.(transport.Robust); ok && s.robustMode == RobustAuto {
   175  		r.SetRobust(false)
   176  		defer r.SetRobust(true)
   177  	}
   178  
   179  	for _, prop := range outbound {
   180  		switch prop.answer {
   181  		case Defer:
   182  			s.h.SetDeferred(prop.mid)
   183  		case Reject:
   184  			sent[prop.mid] = true
   185  		case Accept:
   186  			if err = s.writeCompressed(rw, prop); err != nil {
   187  				return
   188  			}
   189  			sent[prop.mid] = false
   190  		}
   191  	}
   192  	return
   193  }
   194  
   195  func (s *Session) handleInbound(rw io.ReadWriter) (quitReceived bool, err error) {
   196  	var ourChecksum int64
   197  	proposals := make([]*Proposal, 0)
   198  	var nAccepted int
   199  
   200  Loop:
   201  	for {
   202  		var line string
   203  		line, err = s.nextLine()
   204  		if err != nil {
   205  			return
   206  		}
   207  
   208  		// Ignore comments and empty lines
   209  		if line == "" || line[0] == ';' {
   210  			continue
   211  		}
   212  
   213  		// The line should be prefixed F? (? is the command character)
   214  		if len(line) < 2 || line[0] != 'F' {
   215  			return false, fmt.Errorf("Got unexpected protocol line: '%s'", line)
   216  		}
   217  
   218  		switch line[:2] {
   219  		case "FA", "FB", "FC", "FD": // Proposals
   220  			for _, c := range line {
   221  				ourChecksum += int64(c)
   222  			}
   223  			ourChecksum += int64('\r')
   224  
   225  			prop := new(Proposal)
   226  			if err = parseProposal(line, prop); err != nil {
   227  				err = fmt.Errorf("Unable to parse proposal: %w", err)
   228  				return
   229  			}
   230  			proposals = append(proposals, prop)
   231  
   232  		case "FF": // No more messages
   233  			break Loop
   234  
   235  		case "FQ": // Quit
   236  			quitReceived = true
   237  			break Loop
   238  
   239  		case "F>": // Prompt (end of proposal block)
   240  			// Verify checksum
   241  			ourChecksum = (-ourChecksum) & 0xff
   242  			their, _ := strconv.ParseInt(line[3:], 16, 64)
   243  			if their != ourChecksum {
   244  				err = errors.New(fmt.Sprintf(`Checksum error (%d-%d)`, ourChecksum, their))
   245  				return
   246  			}
   247  
   248  			// If we didn't get any proposals, return
   249  			if len(proposals) == 0 {
   250  				return
   251  			}
   252  
   253  			// Answer proposal
   254  			s.log.Printf(`%d proposal(s) received`, len(proposals))
   255  			nAccepted, err = s.writeProposalsAnswer(rw, proposals)
   256  			if err != nil {
   257  				return quitReceived, err
   258  			}
   259  
   260  			if nAccepted > 0 {
   261  				break Loop // Session turn over is implied after receiving the messages
   262  			}
   263  
   264  			// Continue receiving proposals if all where rejected/deferred
   265  			return s.handleInbound(rw)
   266  		default: //TODO: Ignore?
   267  			return false, fmt.Errorf("Unknown protocol command %c", line[1])
   268  		}
   269  	}
   270  
   271  	if quitReceived && nAccepted > 0 {
   272  		return true, errors.New("Got quit command when inbound proposals were pending")
   273  	}
   274  
   275  	// Fetch and decompress accepted
   276  	s.remoteNoMsgs = true
   277  	for _, prop := range proposals {
   278  		if prop.answer != Accept {
   279  			continue
   280  		}
   281  		s.remoteNoMsgs = false
   282  
   283  		var msg *Message
   284  		if err = s.readCompressed(rw, prop); err != nil {
   285  			return
   286  		} else if msg, err = prop.Message(); err != nil {
   287  			return
   288  		}
   289  
   290  		if err = s.h.ProcessInbound(msg); err != nil {
   291  			return
   292  		}
   293  		s.trafficStats.Received = append(s.trafficStats.Received, prop.MID())
   294  	}
   295  
   296  	return
   297  }
   298  
   299  // The B2F protocol does not support offsets larger than 6 digits, the author of the protocol
   300  // seems to have thrown away the idea of supporting transfer of fragmented messages.
   301  //
   302  // If we ever want to support requests of message with offset, we must guard against asking for
   303  // offsets > 999999. RMS Express does not do this (in Winmor P2P anyway), we must avoid that pitfall.
   304  func (s *Session) writeProposalsAnswer(rw io.ReadWriter, proposals []*Proposal) (nAccepted int, err error) {
   305  	answers := make([]byte, len(proposals))
   306  
   307  	seen := make(map[string]bool)
   308  
   309  	for i, prop := range proposals {
   310  		if seen[prop.MID()] {
   311  			// Radio Only gateways will sometimes send multiple proposals for the same MID in the same batch.
   312  			// Instead of rejecting them right away, let's defer the dups until we know we have sucessfully received at least one of the copies.
   313  			s.log.Printf("Defering duplicate message %s", prop.MID())
   314  			prop.answer = Defer
   315  		} else if prop.code != Wl2kProposal && prop.code != GzipProposal {
   316  			s.log.Printf("Defering %s (unsupported format)", prop.MID())
   317  			prop.answer = Defer
   318  		} else if s.h == nil {
   319  			s.log.Printf("Defering %s (missing handler)", prop.MID())
   320  			prop.answer = Defer
   321  		} else if prop.answer = s.h.GetInboundAnswer(*prop); prop.answer == Accept {
   322  			s.log.Printf("Accepting %s", prop.MID()) //TODO: Remove?
   323  			nAccepted++
   324  		}
   325  
   326  		seen[prop.MID()] = true
   327  		answers[i] = byte(prop.answer)
   328  	}
   329  
   330  	_, err = fmt.Fprintf(rw, "FS %s\r", answers)
   331  	return
   332  }
   333  
   334  // Parses the proposal answer (str) and updates the proposals given (in that order)
   335  func parseProposalAnswer(str string, props []*Proposal, l *log.Logger) error {
   336  	str = strings.TrimPrefix(str, "FS ")
   337  
   338  	var c byte
   339  	for i := 0; len(str) > 0; i++ {
   340  		if i >= len(props) {
   341  			return errors.New("Got answer for more proposals than expected")
   342  		}
   343  
   344  		prop := props[i]
   345  		c, str = str[0], str[1:]
   346  
   347  		switch c {
   348  		case 'Y', 'y', '+':
   349  			if l != nil {
   350  				l.Printf("Remote accepted %s", prop.MID())
   351  			}
   352  			prop.answer = Accept
   353  		case 'N', 'n', 'R', 'r', '-':
   354  			if l != nil {
   355  				l.Printf("Remote already received %s", prop.MID())
   356  			}
   357  			prop.answer = Reject
   358  		case 'L', 'l', '=', 'H', 'h':
   359  			if l != nil {
   360  				l.Printf("Remote defered %s", prop.MID())
   361  			}
   362  			prop.answer = Defer
   363  		case 'A', 'a', '!':
   364  			idx := strings.LastIndexAny(str, "0123456789")
   365  			if idx < 0 {
   366  				return errors.New("Got offset request without offset index")
   367  			}
   368  			prop.answer = Accept // Offset is not implemented as a ProposalAnswer
   369  			prop.offset, _ = strconv.Atoi(str[:idx+1])
   370  			str = str[idx+1:]
   371  
   372  			if prop.offset > ProtocolOffsetSizeLimit { // RMS Express does this (in Winmor P2P for sure)
   373  				prop.offset = 0
   374  				if l != nil {
   375  					l.Printf(
   376  						"Remote requested %s at offset %d which exceeds the binary protocol offset limit. Ignoring offset.",
   377  						prop.MID(), prop.offset,
   378  					)
   379  				}
   380  			} else if l != nil {
   381  				l.Printf("Remote accepted %s at offset %d", prop.MID(), prop.offset)
   382  			}
   383  		default:
   384  			return fmt.Errorf("Invalid character (%c) in proposal answer line", c)
   385  		}
   386  	}
   387  	return nil
   388  }
   389  
   390  func (s *Session) writeCompressed(rw io.ReadWriter, p *Proposal) (err error) {
   391  	s.log.Printf("Transmitting [%s] [offset %d]", p.title, p.offset)
   392  
   393  	if p.code == GzipProposal {
   394  		s.log.Println("GZIP_EXPERIMENT:", "Transmitting gzip compressed message.")
   395  	}
   396  
   397  	writer := bufio.NewWriter(rw)
   398  
   399  	var (
   400  		title    = mime.QEncoding.Encode("utf-8", p.title) // Word-encode the title since this field must be ASCII-only
   401  		offset   = fmt.Sprintf("%d", p.offset)
   402  		length   = len(title) + len(offset) + 2
   403  		checksum int64
   404  	)
   405  
   406  	writer.Write([]byte{_CHRSOH, byte(length)})
   407  	writer.WriteString(title) // Max 80 bytes, min 1 byte
   408  	writer.WriteByte(_CHRNUL)
   409  	writer.WriteString(offset) // Max 6 bytes, min 1 byte. Highest supported offset is 1MB-1B.
   410  	writer.WriteByte(_CHRNUL)
   411  	writer.Flush()
   412  
   413  	if p.compressedSize < 6 { // lzhuf's smallest valid length (empty)
   414  		return errors.New(`Invalid compressed data`)
   415  	}
   416  
   417  	buffer := bytes.NewBuffer(p.compressedData[p.offset:])
   418  
   419  	// Update Status of message transfer every 250ms
   420  	statusTicker := time.NewTicker(250 * time.Millisecond)
   421  	statusDone := make(chan struct{})
   422  	go func() {
   423  		for {
   424  			select {
   425  			case <-statusTicker.C:
   426  				if s.statusUpdater == nil || buffer == nil {
   427  					continue
   428  				}
   429  
   430  				// Take into account that the modem has an internal tx buffer (if possible).
   431  				var txBufLen int
   432  				if b, ok := rw.(transport.TxBuffer); ok {
   433  					txBufLen = b.TxBufferLen()
   434  				}
   435  
   436  				transferred := p.compressedSize - buffer.Len() - txBufLen
   437  				if transferred < 0 {
   438  					transferred = 0
   439  				}
   440  
   441  				if s.statusUpdater != nil {
   442  					s.statusUpdater.UpdateStatus(Status{
   443  						Sending:          p,
   444  						BytesTransferred: transferred,
   445  						BytesTotal:       p.compressedSize,
   446  					})
   447  				}
   448  			case <-statusDone:
   449  				if s.statusUpdater != nil {
   450  					s.statusUpdater.UpdateStatus(Status{
   451  						Sending:          p,
   452  						BytesTransferred: p.compressedSize - buffer.Len(),
   453  						BytesTotal:       p.compressedSize,
   454  						Done:             true,
   455  					})
   456  				}
   457  				return
   458  			}
   459  		}
   460  	}()
   461  	defer func() { close(statusDone) }()
   462  
   463  	// Data (in chunks of max 250)
   464  	for buffer.Len() > 0 {
   465  		msgLen := MaxMsgLength
   466  		if buffer.Len() < MaxMsgLength {
   467  			msgLen = buffer.Len()
   468  		}
   469  
   470  		if _, err = writer.Write([]byte{_CHRSTX, byte(msgLen)}); err != nil {
   471  			return err
   472  		}
   473  
   474  		for i := 0; i < msgLen; i++ {
   475  			c, _ := buffer.ReadByte()
   476  			if err := writer.WriteByte(c); err != nil {
   477  				return err
   478  			}
   479  			checksum += int64(c)
   480  		}
   481  
   482  		if err = writer.Flush(); err != nil {
   483  			return err
   484  		}
   485  	}
   486  
   487  	// Checksum
   488  	checksum = -checksum & 0xff
   489  	_, err = writer.Write([]byte{_CHREOT, byte(checksum)})
   490  	err = writer.Flush()
   491  
   492  	// Flush connection buffers.
   493  	// This enables us to block until the whole message has been transmitted over the air.
   494  	if f, ok := rw.(transport.Flusher); ok {
   495  		err = f.Flush()
   496  	}
   497  
   498  	statusTicker.Stop()
   499  
   500  	return err
   501  }
   502  
   503  func (s *Session) readCompressed(rw io.ReadWriter, p *Proposal) (err error) {
   504  	var (
   505  		ourChecksum int
   506  		buf         bytes.Buffer
   507  	)
   508  
   509  	var c byte
   510  	if c, err = s.rd.ReadByte(); err != nil {
   511  		return
   512  	}
   513  	switch c {
   514  	case _CHRSOH:
   515  		// what we expected...
   516  	case '*':
   517  		line, _ := s.nextLine()
   518  		return errors.New(fmt.Sprintf(`Got error from CMS: %s`, line))
   519  	default:
   520  		return errors.New(fmt.Sprintf(`First byte not as expected, got %d`, int(c)))
   521  	}
   522  
   523  	if c, err = s.rd.ReadByte(); err != nil {
   524  		return
   525  	}
   526  	headerLength := int(c)
   527  
   528  	// Read proposal title.
   529  	title, err := s.rd.ReadString(_CHRNUL)
   530  	if err != nil {
   531  		return fmt.Errorf("Unable to parse title: %w", err)
   532  	}
   533  	title = title[:len(title)-1] // Remove _CHRNUL
   534  
   535  	// The proposal title should be ASCII-only according to the protocol specification. Since RMS Express and CMS puts
   536  	// the raw subject header here, we need to handle this by decoding it the same way as the subject header.
   537  	p.title, _ = new(WordDecoder).DecodeHeader(title)
   538  
   539  	// Read offset part
   540  	var offsetStr string
   541  	if offsetStr, err = s.rd.ReadString(_CHRNUL); err != nil {
   542  		return fmt.Errorf("Unable to parse offset: %w", err)
   543  	} else {
   544  		offsetStr = offsetStr[:len(offsetStr)-1]
   545  	}
   546  
   547  	// Check overall length of header
   548  	actualHeaderLength := (len(title) + len(offsetStr)) + 2
   549  	if headerLength != actualHeaderLength {
   550  		return errors.New(fmt.Sprintf(`Header length mismatch: expected %d, got %d`, headerLength, actualHeaderLength))
   551  	}
   552  
   553  	// Parse offset as integer (and do some sanity checks)
   554  	offset, err := strconv.Atoi(offsetStr)
   555  	switch {
   556  	case err != nil:
   557  		return fmt.Errorf("Offset header not parseable as integer: %w", err)
   558  	case offset != p.offset:
   559  		return fmt.Errorf(`Expected offset %d, got %d`, p.offset, offset)
   560  	}
   561  
   562  	s.log.Printf("Receiving [%s] [offset %d]", p.title, p.offset)
   563  
   564  	if p.code == GzipProposal {
   565  		s.log.Println("GZIP_EXPERIMENT:", "Receiving gzip compressed message.")
   566  	}
   567  
   568  	statusUpdate := make(chan struct{})
   569  	go func() {
   570  		for {
   571  			_, ok := <-statusUpdate
   572  			if s.statusUpdater != nil {
   573  				s.statusUpdater.UpdateStatus(Status{
   574  					Receiving:        p,
   575  					BytesTransferred: buf.Len(),
   576  					BytesTotal:       p.compressedSize,
   577  					Done:             !ok,
   578  				})
   579  			}
   580  			if !ok {
   581  				return
   582  			}
   583  		}
   584  	}()
   585  	defer func() { close(statusUpdate) }()
   586  	updateStatus := func() {
   587  		select {
   588  		case statusUpdate <- struct{}{}:
   589  		default:
   590  		}
   591  	}
   592  
   593  	for {
   594  		updateStatus()
   595  		c, err = s.rd.ReadByte()
   596  		if err != nil {
   597  			return err
   598  		}
   599  
   600  		switch c {
   601  		case _CHRSTX:
   602  			c, _ := s.rd.ReadByte()
   603  			length := int(c)
   604  			if length == 0 {
   605  				length = 256
   606  			}
   607  			for i := 0; i < length; i++ {
   608  				c, err = s.rd.ReadByte()
   609  				if err != nil {
   610  					return
   611  				}
   612  				buf.WriteByte(c)
   613  				ourChecksum = (ourChecksum + int(c)) % 256
   614  				if i%10 == 0 {
   615  					updateStatus()
   616  				}
   617  			}
   618  		case _CHREOT:
   619  			c, _ = s.rd.ReadByte()
   620  			ourChecksum = (ourChecksum + int(c)) % 256
   621  			if ourChecksum != 0 {
   622  				return errors.New(`Bad checksum`)
   623  			} else if p.compressedSize != buf.Len() {
   624  				return errors.New(`Length mismatch after EOT`)
   625  			} else {
   626  				p.compressedData = buf.Bytes()
   627  			}
   628  			return
   629  		default:
   630  			return errors.New(`Unexpected byte in compressed stream: ` + string(c))
   631  		}
   632  	}
   633  }