diff --git a/whisperv6/mailserver_response.go b/whisperv6/mailserver_response.go new file mode 100644 index 0000000..5ec0721 --- /dev/null +++ b/whisperv6/mailserver_response.go @@ -0,0 +1,136 @@ +package whisperv6 + +import ( + "bytes" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + mailServerFailedPayloadPrefix = "ERROR=" + cursorSize = 36 +) + +func invalidResponseSizeError(size int) error { + return fmt.Errorf("unexpected payload size: %d", size) +} + +// CreateMailServerRequestCompletedPayload creates a payload representing +// a successful request to mailserver +func CreateMailServerRequestCompletedPayload(requestID, lastEnvelopeHash common.Hash, cursor []byte) []byte { + payload := append(requestID[:], lastEnvelopeHash[:]...) + payload = append(payload, cursor...) + return payload +} + +// CreateMailServerRequestFailedPayload creates a payload representing +// a failed request to a mailserver +func CreateMailServerRequestFailedPayload(requestID common.Hash, err error) []byte { + payloadPrefix := []byte(mailServerFailedPayloadPrefix) + errorString := []byte(err.Error()) + payload := append(payloadPrefix, requestID[:]...) + payload = append(payload, errorString[:]...) + return payload +} + +// CreateMailServerEvent returns EnvelopeEvent with correct data +// if payload corresponds to any of the know mailserver events: +// * request completed successfully +// * request failed +// If the payload is unknown/unparseable, it returns `nil` +func CreateMailServerEvent(payload []byte) (*EnvelopeEvent, error) { + + if len(payload) < common.HashLength { + return nil, invalidResponseSizeError(len(payload)) + } + + event, err := tryCreateMailServerRequestFailedEvent(payload) + + if err != nil || event != nil { + return event, err + } + + return tryCreateMailServerRequestCompletedEvent(payload) +} + +func tryCreateMailServerRequestFailedEvent(payload []byte) (*EnvelopeEvent, error) { + if len(payload) < common.HashLength+len(mailServerFailedPayloadPrefix) { + return nil, nil + } + + prefix, remainder := extractPrefix(payload, len(mailServerFailedPayloadPrefix)) + + if !bytes.Equal(prefix, []byte(mailServerFailedPayloadPrefix)) { + return nil, nil + } + + var ( + requestID common.Hash + errorMsg string + ) + + requestID, remainder = extractHash(remainder) + errorMsg = string(remainder) + + event := EnvelopeEvent{ + Hash: requestID, + Event: EventMailServerRequestCompleted, + Data: &MailServerResponse{ + Error: errors.New(errorMsg), + }, + } + + return &event, nil + +} + +func tryCreateMailServerRequestCompletedEvent(payload []byte) (*EnvelopeEvent, error) { + // check if payload is + // - requestID or + // - requestID + lastEnvelopeHash or + // - requestID + lastEnvelopeHash + cursor + // requestID is the hash of the request envelope. + // lastEnvelopeHash is the last envelope sent by the mail server + // cursor is the db key, 36 bytes: 4 for the timestamp + 32 for the envelope hash. + if len(payload) > common.HashLength*2+cursorSize { + return nil, invalidResponseSizeError(len(payload)) + } + + var ( + requestID common.Hash + lastEnvelopeHash common.Hash + cursor []byte + ) + + requestID, remainder := extractHash(payload) + + if len(remainder) >= common.HashLength { + lastEnvelopeHash, remainder = extractHash(remainder) + } + + if len(remainder) >= cursorSize { + cursor = remainder + } + + event := EnvelopeEvent{ + Hash: requestID, + Event: EventMailServerRequestCompleted, + Data: &MailServerResponse{ + LastEnvelopeHash: lastEnvelopeHash, + Cursor: cursor, + }, + } + + return &event, nil +} + +func extractHash(payload []byte) (common.Hash, []byte) { + prefix, remainder := extractPrefix(payload, common.HashLength) + return common.BytesToHash(prefix), remainder +} + +func extractPrefix(payload []byte, size int) ([]byte, []byte) { + return payload[:size], payload[size:] +} diff --git a/whisperv6/mailserver_response_test.go b/whisperv6/mailserver_response_test.go new file mode 100644 index 0000000..0bd0ff3 --- /dev/null +++ b/whisperv6/mailserver_response_test.go @@ -0,0 +1,130 @@ +package whisperv6 + +import ( + "bytes" + "encoding/binary" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/syndtr/goleveldb/leveldb/errors" +) + +func checkValidErrorPayload(t *testing.T, id []byte, errorMsg string) { + requestID := common.BytesToHash(id) + + errPayload := CreateMailServerRequestFailedPayload(requestID, errors.New(errorMsg)) + + event, err := CreateMailServerEvent(errPayload) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if event == nil { + t.Errorf("Could not parse payload: %v", errPayload) + return + } + + if !bytes.Equal(event.Hash[:], requestID[:]) { + t.Errorf("Unexpected hash: %v, expected %v", event.Hash, requestID) + return + } + + eventData, ok := event.Data.(*MailServerResponse) + if !ok { + t.Errorf("Unexpected data in event: %v, expected a MailServerResponse", event.Data) + return + } + + if strings.Compare(eventData.Error.Error(), errorMsg) != 0 { + t.Errorf("Unexpected error string: '%s', expected '%s'", eventData, errorMsg) + return + } +} + +func checkValidSuccessPayload(t *testing.T, id []byte, lastHash []byte, timestamp uint32, envHash []byte) { + requestID := common.BytesToHash(id) + lastEnvelopeHash := common.BytesToHash(lastHash) + + timestampBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(timestampBytes, timestamp) + + envelopeHash := common.BytesToHash(envHash) + + cursor := append(timestampBytes, envelopeHash[:]...) + + successPayload := CreateMailServerRequestCompletedPayload(requestID, lastEnvelopeHash, cursor) + + event, err := CreateMailServerEvent(successPayload) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if event == nil { + t.Errorf("Could not parse payload: %v", successPayload) + return + } + + if !bytes.Equal(event.Hash[:], requestID[:]) { + t.Errorf("Unexpected hash: %v, expected %v", event.Hash, requestID) + return + } + + eventData, ok := event.Data.(*MailServerResponse) + if !ok { + t.Errorf("Unexpected data in event: %v, expected a MailServerResponse", event.Data) + return + } + + if !bytes.Equal(eventData.LastEnvelopeHash[:], lastEnvelopeHash[:]) { + t.Errorf("Unexpected LastEnvelopeHash: %v, expected %v", + eventData.LastEnvelopeHash, lastEnvelopeHash) + return + } + + if !bytes.Equal(eventData.Cursor, cursor) { + t.Errorf("Unexpected cursor: %v, expected: %v", eventData.Cursor, cursor) + return + } + + if eventData.Error != nil { + t.Errorf("Unexpected error: %v", eventData.Error) + return + } +} + +func TestCreateMailServerEvent(t *testing.T) { + // valid cases + longErrorMessage := "longMessage|" + for i := 0; i < 5; i++ { + longErrorMessage = longErrorMessage + longErrorMessage + } + checkValidErrorPayload(t, []byte{0x01}, "test error 1") + checkValidErrorPayload(t, []byte{0x02}, "test error 2") + checkValidErrorPayload(t, []byte{0x02}, "") + checkValidErrorPayload(t, []byte{0x00}, "test error 3") + checkValidErrorPayload(t, []byte{}, "test error 4") + + checkValidSuccessPayload(t, []byte{0x01}, []byte{0x02}, 123, []byte{0x03}) + // invalid payloads + + // too small + _, err := CreateMailServerEvent([]byte{0x00}) + if err == nil { + t.Errorf("Expected an error, got nil") + return + } + + // too big and not error payload + payloadTooBig := make([]byte, common.HashLength*2+cursorSize+100) + _, err = CreateMailServerEvent(payloadTooBig) + if err == nil { + t.Errorf("Expected an error, got nil") + return + } + +} diff --git a/whisperv6/whisper.go b/whisperv6/whisper.go index e508cb1..f118429 100644 --- a/whisperv6/whisper.go +++ b/whisperv6/whisper.go @@ -66,6 +66,7 @@ const ( type MailServerResponse struct { LastEnvelopeHash common.Hash Cursor []byte + Error error } // Whisper represents a dark communication interface through the Ethereum @@ -911,44 +912,17 @@ func (whisper *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { return errors.New("invalid request response message") } - // check if payload is - // - requestID or - // - requestID + lastEnvelopeHash or - // - requestID + lastEnvelopeHash + cursor - // requestID is the hash of the request envelope. - // lastEnvelopeHash is the last envelope sent by the mail server - // cursor is the db key, 36 bytes: 4 for the timestamp + 32 for the envelope hash. - // length := len(payload) + event, err := CreateMailServerEvent(payload) - if len(payload) < common.HashLength || len(payload) > common.HashLength*3+4 { - log.Warn("invalid response message, peer will be disconnected", "peer", p.peer.ID(), "err", err, "payload size", len(payload)) - return errors.New("invalid response size") + if err != nil { + log.Warn("error while parsing request complete code, peer will be disconnected", "peer", p.peer.ID(), "err", err) + return err } - var ( - requestID common.Hash - lastEnvelopeHash common.Hash - cursor []byte - ) - - requestID = common.BytesToHash(payload[:common.HashLength]) - - if len(payload) >= common.HashLength*2 { - lastEnvelopeHash = common.BytesToHash(payload[common.HashLength : common.HashLength*2]) + if event != nil { + whisper.envelopeFeed.Send(*event) } - if len(payload) >= common.HashLength*2+36 { - cursor = payload[common.HashLength*2 : common.HashLength*2+36] - } - - whisper.envelopeFeed.Send(EnvelopeEvent{ - Hash: requestID, - Event: EventMailServerRequestCompleted, - Data: &MailServerResponse{ - LastEnvelopeHash: lastEnvelopeHash, - Cursor: cursor, - }, - }) } default: // New message types might be implemented in the future versions of Whisper.