webwire

package module
v0.1.0-alpha1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 3, 2018 License: MIT Imports: 13 Imported by: 0

README

Build Status Go Report Card Donate

WebWire for Golang

WebWire is a high-level asynchronous duplex messaging library built on top of WebSockets and an open source binary message protocol with builtin support for UTF8 and UTF16 encoding. The webwire-go library provides both a client and a server implementation for the Go programming language. An official JavaScript client implementation is also available. WebWire provides a compact set of useful features that are not available and/or cumbersome to implement on raw WebSockets such as Request-Reply, Sessions, Thread-safety etc.

WebWire Binary Protocol

WebWire is built for speed and portability implementing an open source binary protocol. Protocol Subset Diagram

The first byte defines the type of the message. Requests and replies contain an incremental 8-byte identifier that must be unique in the context of the senders' session. A 0 to 255 bytes long 7-bit ASCII encoded name is contained in the header of a signal or request message. A header-padding byte is applied in case of UTF16 payload encoding to properly align the payload sequence. Fraudulent messages are recognized by analyzing the message length, out-of-range memory access attacks are therefore prevented.

Examples

Features

Request-Reply

Clients can initiate multiple simultaneous requests and receive replies asynchronously. Requests are multiplexed through the connection similar to HTTP2 pipelining.

// Send a request to the server, will block the goroutine until replied
reply, err := client.Request("", wwr.Payload{Data: []byte("sudo rm -rf /")})
if err != nil {
  // Oh oh, request failed for some reason!
}
reply // Here we go!

Timed requests will timeout and return an error if the server doesn't manage to reply within the specified time frame.

// Send a request to the server, will block the goroutine for 200ms at max
reply, err := client.TimedRequest("", wwr.Payload{Data: []byte("hurry up!")}, 200*time.Millisecond)
if err != nil {
  // Probably timed out!
}
reply // Just in time!
Client-side Signals

Individual clients can send signals to the server. Signals are one-way messages guaranteed to arrive not requiring any reply though.

// Send signal to server
err := client.Signal("eventA", wwr.Payload{Data: []byte("something")})
Server-side Signals

The server also can send signals to individual connected clients.

func onRequest(ctx context.Context) (wwr.Payload, *wwr.Error) {
  msg := ctx.Value(wwr.Msg).(wwr.Message)
  // Send a signal to the client before replying to the request
  msg.Client.Signal("", wwr.Payload{Data: []byte("ping!")})
  return wwr.Payload{}, nil
}
Namespaces

Different kinds of requests and signals can be differentiated using the builtin namespacing feature.

func onRequest(ctx context.Context) (wwr.Payload, *wwr.Error) {
  msg := ctx.Value(wwr.Msg).(wwr.Message)
  switch msg.Name {
  case "auth":
    // Authentication request
  case "query":
    // Query request
  }
  return wwr.Payload{}, nil
}
func onSignal(ctx context.Context) {
  msg := ctx.Value(wwr.Msg).(wwr.Message)
  switch msg.Name {
  case "event A":
    // handle event A
  case "event B":
    // handle event B
  }
}
Sessions

Individual connections can get sessions assigned to identify them. The state of the session is automagically synchronized between the client and the server. WebWire doesn't enforce any kind of authentication technique though, it just provides you a way to authenticate a connection. WebWire also doesn't enforce any kind of session storage, it's up to the user to implement any kind of volatile or persistent session storage, be it a database or a simple map.

func onRequest(ctx context.Context) (wwr.Payload, *wwr.Error) {
  msg := ctx.Value(wwr.Msg).(wwr.Message)
  client := msg.Client
  // Verify credentials
  if string(msg.Payload.Data) != "secret:pass" {
    return wwr.Payload{}, wwr.Error {
      Code: "WRONG_CREDENTIALS",
      Message: "Incorrect username or password, try again"
    }
  }
  // Create session, will automatically synchronize to the client
  err := client.CreateSession(/*arbitrary data*/); err != nil {
    return wwr.Payload{}, wwr.Error {
      Code: "INTERNAL_ERROR",
      Message: "Couldn't create session for some reason"
    }
  }
  client.Session // return wwr.Payload{}, nil
}
Thread Safety

It's safe to use both the session agents (those that are provided by the server through messages) and the client concurrently from multiple goroutines, the library automatically synchronizes concurrent operations.

Hooks

Various hooks provide the ability to asynchronously react to different kinds of events and control the behavior of both the client and the server.

Server-side Hooks
  • OnOptions
  • OnClientConnected
  • OnClientDisconnected
  • OnSignal
  • OnRequest
  • OnSessionCreated
  • OnSessionLookup
  • OnSessionClosed
Client-side Hooks
  • OnServerSignal
  • OnSessionCreated
  • OnSessionClosed

© 2018 Roman Sharkov roman.sharkov@qbeon.com

Documentation

Index

Constants

View Source
const (
	// MsgMinLenSignal represents the minimum binary/UTF8 encoded signal message length
	MsgMinLenSignal = int(3)

	// MsgMinLenSignalUtf16 represents the minimum UTF16 encoded signal message length
	MsgMinLenSignalUtf16 = int(4)

	// MsgMinLenRequest represents the minimum binary/UTF8 encoded request message length
	MsgMinLenRequest = int(11)

	// MsgMinLenRequestUtf16 represents the minimum UTF16 encoded request message length
	MsgMinLenRequestUtf16 = int(12)

	// MsgMinLenReply represents the minimum binary/UTF8 encoded reply message length
	MsgMinLenReply = int(10)

	// MsgMinLenReplyUtf16 represents the minimum UTF16 encoded reply message length
	MsgMinLenReplyUtf16 = int(12)

	// MsgMinLenErrorReply represents the minimum error reply message length
	MsgMinLenErrorReply = int(10)

	// MsgMinLenRestoreSession represents the minimum session restoration request message length
	MsgMinLenRestoreSession = int(10)

	// MsgMinLenCloseSession represents the minimum session destruction request message length
	MsgMinLenCloseSession = int(9)

	// MsgMinLenSessionCreated represents the minimum session creation notification message length
	MsgMinLenSessionCreated = int(2)

	// MsgMinLenSessionClosed represents the minimum session creation notification message length
	MsgMinLenSessionClosed = int(1)
)
View Source
const (

	// MsgErrorReply is sent by the server
	// and represents an error-reply to a previously sent request
	MsgErrorReply = byte(0)

	// MsgSessionCreated is sent by the server
	// to notify the client about the session creation
	MsgSessionCreated = byte(21)

	// MsgSessionClosed is sent by the server
	// to notify the client about the session destruction
	MsgSessionClosed = byte(22)

	// MsgCloseSession is sent by the client
	// and represents a request for the destruction of the currently active session
	MsgCloseSession = byte(31)

	// MsgRestoreSession is sent by the client
	// to request session restoration
	MsgRestoreSession = byte(32)

	// MsgSignalBinary represents a signal with binary payload
	MsgSignalBinary = byte(63)

	// MsgSignalUtf8 represents a signal with UTF8 encoded payload
	MsgSignalUtf8 = byte(64)

	// MsgSignalUtf16 represents a signal with UTF16 encoded payload
	MsgSignalUtf16 = byte(65)

	// MsgRequestBinary represents a request with binary payload
	MsgRequestBinary = byte(127)

	// MsgRequestUtf8 represents a request with a UTF8 encoded payload
	MsgRequestUtf8 = byte(128)

	// MsgRequestUtf16 represents a request with a UTF16 encoded payload
	MsgRequestUtf16 = byte(129)

	// MsgReplyBinary represents a reply with a binary payload
	MsgReplyBinary = byte(191)

	// MsgReplyUtf8 represents a reply with a UTF8 encoded payload
	MsgReplyUtf8 = byte(192)

	// MsgReplyUtf16 represents a reply with a UTF16 encoded payload
	MsgReplyUtf16 = byte(193)
)

Variables

This section is empty.

Functions

func GenerateSessionKey

func GenerateSessionKey() string

GenerateSessionKey returns a URL-safe, base64 encoded securely generated random string. It will return an error if the system's secure random number generator fails to function correctly, in which case the caller should not continue.

func NewEmptyRequestMessage

func NewEmptyRequestMessage(msgType byte, id [8]byte) (msg []byte)

NewEmptyRequestMessage composes a new request message consisting only of the type and identifier and returns its binary representation

func NewNamelessRequestMessage

func NewNamelessRequestMessage(reqType byte, id [8]byte, payload []byte) (msg []byte)

NewNamelessRequestMessage composes a new nameless (initially without a name) request message and returns its binary representation

func NewReplyMessage

func NewReplyMessage(requestID [8]byte, payload Payload) (msg []byte)

NewReplyMessage composes a new reply message and returns its binary representation

func NewRequestMessage

func NewRequestMessage(id [8]byte, name string, payload Payload) (msg []byte)

NewRequestMessage composes a new named request message and returns its binary representation

func NewSignalMessage

func NewSignalMessage(name string, payload Payload) (msg []byte)

NewSignalMessage composes a new named signal message and returns its binary representation

Types

type Client

type Client struct {
	Session *Session
	// contains filtered or unexported fields
}

Client represents a client connected to the server

func (*Client) CloseSession

func (clt *Client) CloseSession() error

CloseSession destroys the currently active session for this client. It automatically synchronizes the session destruction to the client. The synchronization happens asynchronously using a signal and doesn't block the calling goroutine. Does nothing if there's no active session

func (*Client) ConnectionTime

func (clt *Client) ConnectionTime() time.Time

ConnectionTime returns the time when the connection was established

func (*Client) CreateSession

func (clt *Client) CreateSession(attachment interface{}) error

CreateSession creates a new session for this client. It automatically synchronizes the new session to the remote client. The synchronization happens asynchronously using a signal and doesn't block the calling goroutine. Returns an error if there's already another session active

func (*Client) RemoteAddr

func (clt *Client) RemoteAddr() net.Addr

RemoteAddr returns the address of the client. Returns empty string if the client is not connected.

func (*Client) Signal

func (clt *Client) Signal(name string, payload Payload) error

Signal sends a named signal containing the given payload to the client

type ContextKey

type ContextKey int

ContextKey represents the identifiers of objects passed to the handlers context

const (
	// Msg identifies the message object stored in the handler context
	Msg ContextKey = iota
)

type Error

type Error struct {
	Code    string `json:"c"`
	Message string `json:"m,omitempty"`
}

Error represents an error returned in case of a wrong request

type Hooks

type Hooks struct {
	// OnOptions is an optional hook.
	// It's invoked when the websocket endpoint is examined by the client
	// using the HTTP OPTION method.
	OnOptions func(resp http.ResponseWriter)

	// OnClientConnected is an optional hook.
	// It's invoked when a new client establishes a connection to the server
	OnClientConnected func(client *Client)

	// OnClientDisconnected is an optional hook.
	// It's invoked when a client closes the connection to the server
	OnClientDisconnected func(client *Client)

	// OnSignal is a required hook.
	// It's invoked when the webwire server receives a signal from the client
	OnSignal func(ctx context.Context)

	// OnRequest is an optional hook.
	// It's invoked when the webwire server receives a request from the client.
	// It must return either a response payload or an error
	OnRequest func(ctx context.Context) (response Payload, err *Error)

	// OnSessionCreated is a required hook for sessions to be supported.
	// It's invoked right after the synchronisation of the new session to the remote client.
	// The WebWire server isn't responsible for permanently storing the sessions it creates,
	// it's up to the user to save the given session in this hook either to a database,
	// a filesystem or any other kind of persistent or volatile storage
	// for OnSessionLookup to later be able to restore it by the session key.
	// If OnSessionCreated fails returning an error then the failure is logged
	// but the session isn't destroyed and remains active.
	// The only consequence of OnSessionCreation failing is that the server won't be able
	// to restore the session after the client is disconnected
	OnSessionCreated func(client *Client) error

	// OnSessionLookup is a required hook for sessions to be supported.
	// It's invoked when the server is looking for a specific session given its key.
	// The user is responsible for returning the exact copy of the session object
	// associated with the given key for sessions to be restorable.
	// If OnSessionLookup fails returning an error then the failure is logged
	OnSessionLookup func(key string) (*Session, error)

	// OnSessionClosed is a required hook for sessions to be supported.
	// It's invoked when the active session of the given client
	// is closed (thus destroyed) either by the server or the client himself.
	// The user is responsible for removing the current session of the given client
	// from its storage for the session to be actually and properly destroyed.
	// If OnSessionClosed fails returning an error then the failure is logged
	OnSessionClosed func(client *Client) error
}

Hooks represents all callback hook functions

func (*Hooks) SetDefaults

func (hooks *Hooks) SetDefaults()

SetDefaults sets undefined required hooks

type Message

type Message struct {
	Name    string
	Payload Payload
	Client  *Client
	// contains filtered or unexported fields
}

Message represents a WebWire protocol message

func (*Message) Parse

func (msg *Message) Parse(message []byte) (err error)

Parse tries to parse the message from a byte slice

type Options

type Options struct {
	Addr     string
	Hooks    Hooks
	WarnLog  io.Writer
	ErrorLog io.Writer
}

Options represents the options for a headed server setup

func (*Options) SetDefaults

func (opts *Options) SetDefaults()

SetDefaults sets default values to undefined options

type Payload

type Payload struct {
	Encoding PayloadEncoding
	Data     []byte
}

Payload represents an encoded message payload

type PayloadEncoding

type PayloadEncoding int

PayloadEncoding represents the type of encoding of the message payload

const (
	// EncodingBinary represents unencoded binary data
	EncodingBinary PayloadEncoding = iota

	// EncodingUtf8 represents UTF8 encoding
	EncodingUtf8

	// EncodingUtf16 represents UTF16 encoding
	EncodingUtf16
)

type Server

type Server struct {
	SessionRegistry sessionRegistry
	// contains filtered or unexported fields
}

Server represents a headless WebWire server instance, where headless means there's no HTTP server that's hosting it

func NewServer

func NewServer(opts Options) *Server

NewServer creates a new WebWire server instance

func SetupServer

func SetupServer(opts Options) (
	wwrSrv *Server,
	httpServer *http.Server,
	addr string,
	runFunc func() error,
	err error,
)

SetupServer sets up a new headed WebWire server with an HTTP server hosting it

func (*Server) ServeHTTP

func (srv *Server) ServeHTTP(
	resp http.ResponseWriter,
	req *http.Request,
)

ServeHTTP will make the server listen for incoming HTTP requests eventually trying to upgrade them to WebSocket connections

type Session

type Session struct {
	Key          string      `json:"key"`
	CreationDate time.Time   `json:"crt"`
	Info         interface{} `json:"inf"`
}

Session represents a session object. If the key is empty the session is invalid. Info can contain arbitrary attached data

func NewSession

func NewSession(info interface{}) Session

NewSession generates a new session object generating a cryptographically random secure key

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL