github.com/jhalter/mobius@v0.12.1/hotline/transaction.go (about)

     1  package hotline
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"errors"
     7  	"fmt"
     8  	"github.com/jhalter/mobius/concat"
     9  	"math/rand"
    10  )
    11  
    12  const (
    13  	TranError                = 0
    14  	TranGetMsgs              = 101
    15  	TranNewMsg               = 102
    16  	TranOldPostNews          = 103
    17  	TranServerMsg            = 104
    18  	TranChatSend             = 105
    19  	TranChatMsg              = 106
    20  	TranLogin                = 107
    21  	TranSendInstantMsg       = 108
    22  	TranShowAgreement        = 109
    23  	TranDisconnectUser       = 110
    24  	TranDisconnectMsg        = 111 // TODO: implement server initiated friendly disconnect
    25  	TranInviteNewChat        = 112
    26  	TranInviteToChat         = 113
    27  	TranRejectChatInvite     = 114
    28  	TranJoinChat             = 115
    29  	TranLeaveChat            = 116
    30  	TranNotifyChatChangeUser = 117
    31  	TranNotifyChatDeleteUser = 118
    32  	TranNotifyChatSubject    = 119
    33  	TranSetChatSubject       = 120
    34  	TranAgreed               = 121
    35  	TranServerBanner         = 122
    36  	TranGetFileNameList      = 200
    37  	TranDownloadFile         = 202
    38  	TranUploadFile           = 203
    39  	TranNewFolder            = 205
    40  	TranDeleteFile           = 204
    41  	TranGetFileInfo          = 206
    42  	TranSetFileInfo          = 207
    43  	TranMoveFile             = 208
    44  	TranMakeFileAlias        = 209
    45  	TranDownloadFldr         = 210
    46  	TranDownloadInfo         = 211 // TODO: implement file transfer queue
    47  	TranDownloadBanner       = 212
    48  	TranUploadFldr           = 213
    49  	TranGetUserNameList      = 300
    50  	TranNotifyChangeUser     = 301
    51  	TranNotifyDeleteUser     = 302
    52  	TranGetClientInfoText    = 303
    53  	TranSetClientUserInfo    = 304
    54  	TranListUsers            = 348
    55  	TranUpdateUser           = 349
    56  	TranNewUser              = 350
    57  	TranDeleteUser           = 351
    58  	TranGetUser              = 352
    59  	TranSetUser              = 353
    60  	TranUserAccess           = 354
    61  	TranUserBroadcast        = 355
    62  	TranGetNewsCatNameList   = 370
    63  	TranGetNewsArtNameList   = 371
    64  	TranDelNewsItem          = 380
    65  	TranNewNewsFldr          = 381
    66  	TranNewNewsCat           = 382
    67  	TranGetNewsArtData       = 400
    68  	TranPostNewsArt          = 410
    69  	TranDelNewsArt           = 411
    70  	TranKeepAlive            = 500
    71  )
    72  
    73  type Transaction struct {
    74  	clientID *[]byte
    75  
    76  	Flags      byte   // Reserved (should be 0)
    77  	IsReply    byte   // Request (0) or reply (1)
    78  	Type       []byte // Requested operation (user defined)
    79  	ID         []byte // Unique transaction ID (must be != 0)
    80  	ErrorCode  []byte // Used in the reply (user defined, 0 = no error)
    81  	TotalSize  []byte // Total data size for the transaction (all parts)
    82  	DataSize   []byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
    83  	ParamCount []byte // Number of the parameters for this transaction
    84  	Fields     []Field
    85  }
    86  
    87  func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction {
    88  	typeSlice := make([]byte, 2)
    89  	binary.BigEndian.PutUint16(typeSlice, uint16(t))
    90  
    91  	idSlice := make([]byte, 4)
    92  	binary.BigEndian.PutUint32(idSlice, rand.Uint32())
    93  
    94  	return &Transaction{
    95  		clientID:  clientID,
    96  		Flags:     0x00,
    97  		IsReply:   0x00,
    98  		Type:      typeSlice,
    99  		ID:        idSlice,
   100  		ErrorCode: []byte{0, 0, 0, 0},
   101  		Fields:    fields,
   102  	}
   103  }
   104  
   105  // Write implements io.Writer interface for Transaction
   106  func (t *Transaction) Write(p []byte) (n int, err error) {
   107  	totalSize := binary.BigEndian.Uint32(p[12:16])
   108  
   109  	// the buf may include extra bytes that are not part of the transaction
   110  	// tranLen represents the length of bytes that are part of the transaction
   111  	tranLen := int(20 + totalSize)
   112  
   113  	if tranLen > len(p) {
   114  		return n, errors.New("buflen too small for tranLen")
   115  	}
   116  	fields, err := ReadFields(p[20:22], p[22:tranLen])
   117  	if err != nil {
   118  		return n, err
   119  	}
   120  
   121  	t.Flags = p[0]
   122  	t.IsReply = p[1]
   123  	t.Type = p[2:4]
   124  	t.ID = p[4:8]
   125  	t.ErrorCode = p[8:12]
   126  	t.TotalSize = p[12:16]
   127  	t.DataSize = p[16:20]
   128  	t.ParamCount = p[20:22]
   129  	t.Fields = fields
   130  
   131  	return len(p), err
   132  }
   133  
   134  const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields
   135  
   136  // transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
   137  func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) {
   138  	// The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes
   139  	if len(data) < 16 {
   140  		return 0, nil, nil
   141  	}
   142  
   143  	totalSize := binary.BigEndian.Uint32(data[12:16])
   144  
   145  	// tranLen represents the length of bytes that are part of the transaction
   146  	tranLen := int(tranHeaderLen + totalSize)
   147  	if tranLen > len(data) {
   148  		return 0, nil, nil
   149  	}
   150  
   151  	return tranLen, data[0:tranLen], nil
   152  }
   153  
   154  const minFieldLen = 4
   155  
   156  func ReadFields(paramCount []byte, buf []byte) ([]Field, error) {
   157  	paramCountInt := int(binary.BigEndian.Uint16(paramCount))
   158  	if paramCountInt > 0 && len(buf) < minFieldLen {
   159  		return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
   160  	}
   161  
   162  	// A Field consists of:
   163  	// ID: 2 bytes
   164  	// Size: 2 bytes
   165  	// Data: FieldSize number of bytes
   166  	var fields []Field
   167  	for i := 0; i < paramCountInt; i++ {
   168  		if len(buf) < minFieldLen {
   169  			return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
   170  		}
   171  		fieldID := buf[0:2]
   172  		fieldSize := buf[2:4]
   173  		fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4]))
   174  		expectedLen := minFieldLen + fieldSizeInt
   175  		if len(buf) < expectedLen {
   176  			return []Field{}, fmt.Errorf("field length too short")
   177  		}
   178  
   179  		fields = append(fields, Field{
   180  			ID:        fieldID,
   181  			FieldSize: fieldSize,
   182  			Data:      buf[4 : 4+fieldSizeInt],
   183  		})
   184  
   185  		buf = buf[fieldSizeInt+4:]
   186  	}
   187  
   188  	if len(buf) != 0 {
   189  		return []Field{}, fmt.Errorf("extra field bytes")
   190  	}
   191  
   192  	return fields, nil
   193  }
   194  
   195  func (t *Transaction) MarshalBinary() (data []byte, err error) {
   196  	payloadSize := t.Size()
   197  
   198  	fieldCount := make([]byte, 2)
   199  	binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
   200  
   201  	var fieldPayload []byte
   202  	for _, field := range t.Fields {
   203  		fieldPayload = append(fieldPayload, field.Payload()...)
   204  	}
   205  
   206  	return concat.Slices(
   207  		[]byte{t.Flags, t.IsReply},
   208  		t.Type,
   209  		t.ID,
   210  		t.ErrorCode,
   211  		payloadSize,
   212  		payloadSize, // this is the dataSize field, but seeming the same as totalSize
   213  		fieldCount,
   214  		fieldPayload,
   215  	), err
   216  }
   217  
   218  // Size returns the total size of the transaction payload
   219  func (t *Transaction) Size() []byte {
   220  	bs := make([]byte, 4)
   221  
   222  	fieldSize := 0
   223  	for _, field := range t.Fields {
   224  		fieldSize += len(field.Data) + 4
   225  	}
   226  
   227  	binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
   228  
   229  	return bs
   230  }
   231  
   232  func (t *Transaction) GetField(id int) Field {
   233  	for _, field := range t.Fields {
   234  		if id == int(binary.BigEndian.Uint16(field.ID)) {
   235  			return field
   236  		}
   237  	}
   238  
   239  	return Field{}
   240  }
   241  
   242  func (t *Transaction) IsError() bool {
   243  	return bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 1})
   244  }