server

package
v0.0.1-20240408-0001 Latest Latest
Warning

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

Go to latest
Published: Apr 8, 2024 License: MIT Imports: 43 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// Time allowed to write a Message to the peer.
	WriteWait = 10 * time.Second

	// Time allowed to read the next pong Message from the peer.
	PongWait = 60 * time.Second

	// send pings to peer with this period. Must be less than pongWait.
	PingPeriod = (PongWait * 9) / 10

	// Maximum Message size allowed from peer. (By default its 512)
	MaxMessageSize = 128 * 1024 * 1024 // This is approx 134 MB?

	// Enable Automatic HTTP Upgrade to WS
	EnableHttpToWebSocketUpgrade = true
)
View Source
const DefaultCloseCode = 1000
View Source
const DefaultCloseReason = "No specific reason!"
View Source
const DefaultEnableCompression = true
View Source
const DefaultListeningAddress = "0.0.0.0:8080"
View Source
const DefaultReadBufferSize = 1024
View Source
const DefaultSSLListeningAddress = "0.0.0.0:8443"
View Source
const DefaultWriteBufferSize = 1024

Variables

This section is empty.

Functions

func GetClientsInChunks

func GetClientsInChunks(clients map[*Client]bool, nrOfChunks uint16) []map[*Client]bool

the programmer should handle locks before!! func GetClientsInChunks(clients interface{}, nrOfChunks uint16) []map[*Client]bool {

func GetClientsInChunksWithConn

func GetClientsInChunksWithConn(clients map[uint64]*Client, nrOfChunks uint16) []map[uint64]*Client

func NewClientsInstance

func NewClientsInstance() *clientsData

Types

type BinaryPayload

type BinaryPayload struct {
	// This is the unique ID of the payload
	PayloadID string
	// This is chunk size based on what the data will be split!
	ChunkSize  int64
	OnResponse TextPayloadOnResponse
	// That's when we receive a response from the client with a specific chunk id!
	OnChunkResponse func()
	// This is the data
	Data []byte
}

type BroadcastHub

type BroadcastHub struct {
	// contains filtered or unexported fields
}

func NewBroadcastHub

func NewBroadcastHub(s *Server) *BroadcastHub

type Client

type Client struct {

	// Logger -> it's specifically related to client
	// Logs will be written to client file, but not in the main websocket log file
	// If needed, this can be enabled
	Logger *model.Logger
	// contains filtered or unexported fields
}

func (*Client) BroadcastBinary

func (c *Client) BroadcastBinary(message []byte) *Client

BroadcastBinary -> It sends clear bytes to the c

func (*Client) BroadcastJSON

func (c *Client) BroadcastJSON(message interface{}, onJsonError OnJsonError) *Client

BroadcastJSON - It sends Any structure to the c encoded as JSON!

func (*Client) BroadcastText

func (c *Client) BroadcastText(message string) *Client

BroadcastText - It sends clear Text to the c! without any encoding!

func (*Client) Disconnect

func (c *Client) Disconnect() error

Disconnect the client forcefully!!

func (*Client) DisconnectGracefully

func (c *Client) DisconnectGracefully(code uint16, message string)

DisconnectGracefully -> set 0 and "" for default values!

func (*Client) Get

func (c *Client) Get(key string) interface{}

Get custom data from the client connection!

func (*Client) GetAuthDetails

func (c *Client) GetAuthDetails() *authentication.AuthDetails

func (*Client) GetAuthToken

func (c *Client) GetAuthToken() string

func (*Client) GetAuthTokenID

func (c *Client) GetAuthTokenID() string

func (*Client) GetCancelContext

func (c *Client) GetCancelContext() *_context.CancelCtx

func (*Client) GetConnectTime

func (c *Client) GetConnectTime() time.Time

func (*Client) GetConnectedTimeSeconds

func (c *Client) GetConnectedTimeSeconds() int64

func (*Client) GetConnectionID

func (c *Client) GetConnectionID() uint64

func (*Client) GetDeviceID

func (c *Client) GetDeviceID() string

func (*Client) GetDeviceUUID

func (c *Client) GetDeviceUUID() string

func (*Client) GetHttpContext

func (c *Client) GetHttpContext() *gin.Context

func (*Client) GetIPAddress

func (c *Client) GetIPAddress() string

func (*Client) GetRemoteIP

func (c *Client) GetRemoteIP() string

func (*Client) GetRequestPath

func (c *Client) GetRequestPath() string

func (*Client) GetSafeHttpContext

func (c *Client) GetSafeHttpContext() *gin.Context

func (*Client) GetTokenExpirationTime

func (c *Client) GetTokenExpirationTime() time.Time

func (*Client) GetUserID

func (c *Client) GetUserID() string

func (*Client) IsDisconnecting

func (c *Client) IsDisconnecting() bool

func (*Client) LDebug

func (c *Client) LDebug() *zerolog.Event

LDebug -> 0

func (*Client) LDebugF

func (c *Client) LDebugF(functionName string) *zerolog.Event

func (*Client) LError

func (c *Client) LError() *zerolog.Event

LError -> 3

func (*Client) LErrorF

func (c *Client) LErrorF(functionName string) *zerolog.Event

LErrorF -> when you need specifically to indicate in what function the logging is happening

func (*Client) LEvent

func (c *Client) LEvent(eventType string, eventName string, beforeMsg func(event *zerolog.Event))

func (*Client) LFatal

func (c *Client) LFatal() *zerolog.Event

LFatal -> 4

func (*Client) LInfo

func (c *Client) LInfo() *zerolog.Event

LInfo -> 1

func (*Client) LInfoF

func (c *Client) LInfoF(functionName string) *zerolog.Event

LInfoF -> when you need specifically to indicate in what function the logging is happening

func (*Client) LPanic

func (c *Client) LPanic() *zerolog.Event

LPanic -> 5

func (*Client) LWarn

func (c *Client) LWarn() *zerolog.Event

LWarn -> 2

func (*Client) LWarnF

func (c *Client) LWarnF(functionName string) *zerolog.Event

LWarnF -> when you need specifically to indicate in what function the logging is happening

func (*Client) LiveStreaming

func (c *Client) LiveStreaming() *Client

func (*Client) SendBinary

func (c *Client) SendBinary(message []byte) *Client

func (*Client) SendJSON

func (c *Client) SendJSON(message interface{}, onJsonError OnJsonError) *Client

func (*Client) SendText

func (c *Client) SendText(message string) *Client

func (*Client) SendTextPayload

func (c *Client) SendTextPayload(textPayload TextPayload) *Client

func (*Client) Set

func (c *Client) Set(key string, value interface{}) *Client

Set custom Data to client connection!

func (*Client) WriteBinary

func (c *Client) WriteBinary(message []byte) *Client

WriteBinary - It sends clear bytes to the client

func (*Client) WriteBinaryPayload

func (c *Client) WriteBinaryPayload(binaryPayload BinaryPayload) *Client

WriteBinaryPayload - This can be large files, audio data, anything... this is multiparted!

func (*Client) WriteFile

func (c *Client) WriteFile(filePayload FilePayload) *Client

func (*Client) WriteJSON

func (c *Client) WriteJSON(message interface{}, onJsonError OnJsonError) *Client

WriteJSON - It sends Any structure to the client encoded as JSON!

func (*Client) WriteText

func (c *Client) WriteText(message string) *Client

WriteText - It sends clear Text to the client! without any encoding!

func (*Client) WriteTextPayload

func (c *Client) WriteTextPayload(textPayload TextPayload) *Client

WriteTextPayload - We will write any type of Message which will be formatted into a JSON and into a specific structure! This also will receive a response from the client! It's also limited to a specific packet length! It's destined to receive back a response from the Client!

type ClientDetails

type ClientDetails struct {
	ConnectionID     int64
	ClientIP         string
	RemoteIP         string
	RequestPath      string
	ConnectedAt      time.Time
	ConnectedSeconds int64
	UserID           string
	DeviceID         string
}

type ClientsIndex

type ClientsIndex struct {

	// Indexes
	Users       map[string]map[uint64]*Client
	Devices     map[string]map[uint64]*Client
	Connections map[uint64]*Client
	AuthTokens  map[string]map[uint64]*Client
	IPAddresses map[string]map[uint64]*Client
	RequestPath map[string]map[uint64]*Client
	// contains filtered or unexported fields
}

Here we store reverse map of the connections!

type ClientsStatus

type ClientsStatus struct {
	NrOfClients int64
	Clients     map[int64]ClientDetails
}

type FilePayload

type FilePayload struct {
	// Complete file path for reading!
	FilePath string
	// Auto continue if connection interrupted
	AutoContinue bool

	// The size of the part should be sent!
	ChunkSize int64

	FileName string
}

type FindClientsFilter

type FindClientsFilter struct {
	All bool
	// Users (ID's)
	Users []string
	// Devices (ID's)
	Devices []string
	// AuthTokens (Tokens)
	AuthTokens []string
	// Connections (ID's)
	Connections []uint64
	// IP Addresses (Addresses)
	IPAddresses []string
	// Route Paths
	RequestPaths []string
	// Exception List (usually used in tandem with All param) if sending to everyone
	ExceptConnections  []uint64
	ExceptUsers        []string
	ExceptDevices      []string
	ExceptAuthTokens   []string
	ExceptIPAddresses  []string
	ExceptRequestPaths []string
	// contains filtered or unexported fields
}

type FullStatus

type FullStatus struct {
	Name                  string
	ListeningAddresses    []string
	ListeningAddressesSSL []string
	CurrentConnectionID   uint64
	NrOfClients           uint
	Hubs                  []HubStatus
	SystemStatus          info.SystemStatus
}

func (*FullStatus) Collect

func (s *FullStatus) Collect()

type Hub

type Hub struct {

	// If the stop has being called!
	StopCalled *_bool.Bool

	// ControlMessages
	ControlChannel chan int

	// Unregistered ClientsStatus
	UnregisterClientChannel chan *Client
	// contains filtered or unexported fields
}

func (*Hub) BroadcastBinary

func (h *Hub) BroadcastBinary(message []byte) *Hub

func (*Hub) BroadcastByReceivedMessageType

func (h *Hub) BroadcastByReceivedMessageType(message *ReceivedMessage) *Hub

func (*Hub) BroadcastByReceivedMessageTypeTo

func (h *Hub) BroadcastByReceivedMessageTypeTo(message *ReceivedMessage, to FindClientsFilter) *Hub

func (*Hub) BroadcastJSON

func (h *Hub) BroadcastJSON(message interface{}, onJsonError OnJsonError) *Hub

func (*Hub) BroadcastText

func (h *Hub) BroadcastText(message string) *Hub

func (*Hub) BroadcastTextTo

func (h *Hub) BroadcastTextTo(message string, to FindClientsFilter) *Hub

func (*Hub) GetClients

func (h *Hub) GetClients() map[*Client]bool

func (*Hub) GetClientsByFilter

func (h *Hub) GetClientsByFilter(filter FindClientsFilter) map[uint64]*Client

func (*Hub) GetCreatedTime

func (h *Hub) GetCreatedTime() time.Time

func (*Hub) GetNrOfClients

func (h *Hub) GetNrOfClients() uint

func (*Hub) HasOnClientRegister

func (h *Hub) HasOnClientRegister(name string) bool

func (*Hub) HasOnClientUnRegister

func (h *Hub) HasOnClientUnRegister(name string) bool

func (*Hub) NewCancelContext

func (h *Hub) NewCancelContext() *Hub

func (*Hub) NrOfClients

func (h *Hub) NrOfClients() uint

func (*Hub) OnClientRegister

func (h *Hub) OnClientRegister(name string, callback HubOnClientRegister) bool

func (*Hub) OnClientRegisterRemove

func (h *Hub) OnClientRegisterRemove(name string)

func (*Hub) OnClientUnRegister

func (h *Hub) OnClientUnRegister(name string, callback HubOnClientUnRegister) bool

func (*Hub) OnClientUnRegisterRemove

func (h *Hub) OnClientUnRegisterRemove(name string)

func (*Hub) OnStart

func (h *Hub) OnStart(onStart HubOnStart) bool

func (*Hub) OnStartBroadcast

func (h *Hub) OnStartBroadcast(onStartBroadCast HubOnStartBroadCast) bool

func (*Hub) OnStartGetter

func (h *Hub) OnStartGetter(onStartGetter HubOnStartGetter) bool

func (*Hub) RegisterClient

func (h *Hub) RegisterClient(client *Client) *Hub

RegisterClient -> Adds the client to the hub!

func (*Hub) SetContext

func (h *Hub) SetContext(ctx context.Context)

func (*Hub) SetGetter

func (h *Hub) SetGetter(getter HubGetter) bool

SetGetter -> This is the function which gets the info and sends to the broadcaster!

func (*Hub) Start

func (h *Hub) Start() *Hub

Start -> You can start the Hub before the websocket server has being started! This is the function which starts the hub!

func (*Hub) Stop

func (h *Hub) Stop() *Hub

It stops the hub!

func (*Hub) UnRegisterClient

func (h *Hub) UnRegisterClient(client *Client) *Hub

UnRegisterClient -> Removes the client from the Hub

type HubGetter

type HubGetter func(h *Hub)

type HubOnClientRegister

type HubOnClientRegister func(h *Hub, client *Client)

type HubOnClientUnRegister

type HubOnClientUnRegister func(h *Hub, client *Client)

type HubOnStart

type HubOnStart func(h *Hub)

type HubOnStartBroadCast

type HubOnStartBroadCast func(h *Hub)

type HubOnStartGetter

type HubOnStartGetter func(h *Hub)

type HubStatus

type HubStatus struct {
	Name        string
	NrOfClients uint
}

type HubsStatus

type HubsStatus struct {
	Hubs []HubStatus
}

type NrOfClientsStatus

type NrOfClientsStatus struct {
	CurrentConnectionID uint64
	NrOfClients         uint
}

type OnBeforeStart

type OnBeforeStart func(s *Server)

type OnBeforeStop

type OnBeforeStop func(s *Server)

type OnBeforeUpgrade

type OnBeforeUpgrade func(s *Server)

type OnClose

type OnClose func(c *Client, s *Server)

type OnConnect

type OnConnect func(c *Client, s *Server)

On Connect it will be launched in a goroutine!

type OnEvent

type OnEvent func(...interface{})

type OnJsonError

type OnJsonError func(err error, message interface{})

type OnMessage

type OnMessage func(message *ReceivedMessage, c *Client, s *Server)

type OnStart

type OnStart func(s *Server)

Start

type OnStarted

type OnStarted func(s *Server)

type OnStop

type OnStop func(s *Server)

Stop

type OnStopped

type OnStopped func(s *Server)

type OnUpgrade

type OnUpgrade func(c *Client, s *Server)

type ReceivedMessage

type ReceivedMessage struct {
	// There are few types...
	MessageType int8
	// Bytes?!!
	MessageLength uint
	Message       []byte
}

func (*ReceivedMessage) Binary

func (r *ReceivedMessage) Binary() []byte

It gives the Binary!

func (*ReceivedMessage) IsBinary

func (r *ReceivedMessage) IsBinary() bool

func (*ReceivedMessage) IsClose

func (r *ReceivedMessage) IsClose() bool

func (*ReceivedMessage) IsContinuation

func (r *ReceivedMessage) IsContinuation() bool

func (*ReceivedMessage) IsPing

func (r *ReceivedMessage) IsPing() bool

func (*ReceivedMessage) IsPong

func (r *ReceivedMessage) IsPong() bool

func (*ReceivedMessage) IsText

func (r *ReceivedMessage) IsText() bool

func (*ReceivedMessage) JSONDecode

func (r *ReceivedMessage) JSONDecode() (interface{}, error)

It decodes JSON into a Structure!

func (*ReceivedMessage) JSONDecodeTo

func (r *ReceivedMessage) JSONDecodeTo(to interface{}) error

func (*ReceivedMessage) Text

func (r *ReceivedMessage) Text() string

It returns a string

type RegistrationHub

type RegistrationHub struct {
	// contains filtered or unexported fields
}

func NewRegistrationHub

func NewRegistrationHub(s *Server) *RegistrationHub

type Server

type Server struct {
	Name        string
	Description string

	// This will be the main folder where we will store the logs
	LoggerDirPath string
	// This is the logger configuration!
	Logger *model.Logger

	// It also includes port
	ListeningAddresses    []string // This is for unencrypted
	ListeningAddressesSSL []string // This is for encrypted

	// This is GIN Server
	WSServer *gin.Engine
	// This is the registrationHub that registers the c...
	WSRegistrationHub *RegistrationHub
	// This is the registrationHub which sends broadcast messages!
	WSBroadcastHub *BroadcastHub
	// This is the upgrader which transfers from http to WebSocket
	WSUpgrader websocket.Upgrader

	// It enables automatic upgrade from http to ws
	// Usually the server will be behind a proxy server, so it's not necessary....
	EnableHttpToWSUpgrade *_bool.Bool

	// ------Settings ---------\\
	// Time allowed to write a Message to the peser.
	//WriteWait time.Duration
	WriteWait *duration.Duration
	// Time allowed to read the next pong Message from the peer.
	PongWait *duration.Duration
	// send pings to peer with this period. Must be less than pongWait.
	PingPeriod *duration.Duration
	// Maximum Message size allowed from peer.
	MaxMessageSize *_uint64.Uint64

	// Here we store the created Hubs
	// The Hubs are being added here when they are started only!
	// There is no reference until they are started!
	Hubs map[*Hub]bool
	// contains filtered or unexported fields
}

func New

func New(
	ctx context.Context,
	config config.Config,
) (*Server, error)

New -> You can use the default constructor!

func (*Server) ClientsStatus

func (s *Server) ClientsStatus(onCollected func(clients ClientsStatus))

func (*Server) DisableServerStatus

func (s *Server) DisableServerStatus() *Server

func (*Server) DisableUnsecure

func (s *Server) DisableUnsecure() *Server

func (*Server) EnableCompression

func (s *Server) EnableCompression(status bool)

func (*Server) EnableSSL

func (s *Server) EnableSSL(keyPath string, certPath string) error

THe user can start it in a goroutine

func (*Server) EnableServerStatus

func (s *Server) EnableServerStatus() *Server

func (*Server) EnableUnsecure

func (s *Server) EnableUnsecure() *Server

func (*Server) GetClients

func (s *Server) GetClients() map[*Client]bool

func (*Server) GetClientsByFilter

func (s *Server) GetClientsByFilter(filter FindClientsFilter) map[uint64]*Client

func (*Server) GetClientsLogPath

func (s *Server) GetClientsLogPath() string

GetClientsLogPath -> returns the path where the logs for clients are stored

func (*Server) GetClientsOrderedByConnectionID

func (s *Server) GetClientsOrderedByConnectionID() map[int64]*Client

func (*Server) GetNrOfClients

func (s *Server) GetNrOfClients() uint

func (*Server) GetWSServer

func (s *Server) GetWSServer() *gin.Engine

func (*Server) IsStarted

func (s *Server) IsStarted() bool

func (*Server) IsStarting

func (s *Server) IsStarting() bool

func (*Server) IsStopped

func (s *Server) IsStopped() bool

func (*Server) IsStopping

func (s *Server) IsStopping() bool

func (*Server) LDebug

func (s *Server) LDebug() *zerolog.Event

LDebug -> 0

func (*Server) LDebugF

func (s *Server) LDebugF(functionName string) *zerolog.Event

LDebugF -> when you need specifically to indicate in what function the logging is happening

func (*Server) LError

func (s *Server) LError() *zerolog.Event

LError -> 3

func (*Server) LErrorF

func (s *Server) LErrorF(functionName string) *zerolog.Event

LErrorF -> when you need specifically to indicate in what function the logging is happening

func (*Server) LEvent

func (s *Server) LEvent(eventType string, eventName string, beforeMsg func(event *zerolog.Event))

func (*Server) LFatal

func (s *Server) LFatal() *zerolog.Event

LFatal -> 4

func (*Server) LInfo

func (s *Server) LInfo() *zerolog.Event

LInfo -> 1

func (*Server) LInfoF

func (s *Server) LInfoF(functionName string) *zerolog.Event

LInfoF -> when you need specifically to indicate in what function the logging is happening

func (*Server) LPanic

func (s *Server) LPanic() *zerolog.Event

LPanic -> 5

func (*Server) LWarn

func (s *Server) LWarn() *zerolog.Event

LWarn -> 2

func (*Server) LWarnF

func (s *Server) LWarnF(functionName string) *zerolog.Event

LWarnF -> when you need specifically to indicate in what function the logging is happening

func (*Server) NewCancelContext

func (s *Server) NewCancelContext() *Server

func (*Server) NewHub

func (s *Server) NewHub(getter ...HubGetter) *Hub

NewHub -> It creates a special custom hub with specific functionality for handling c

func (*Server) OnBeforeStart

func (s *Server) OnBeforeStart(name string, callback OnStart) bool

func (*Server) OnBeforeStartRemove

func (s *Server) OnBeforeStartRemove(name string)

func (*Server) OnBeforeStop

func (s *Server) OnBeforeStop(name string, callback OnStop) bool

func (*Server) OnBeforeStopRemove

func (s *Server) OnBeforeStopRemove(name string)

func (*Server) OnBeforeUpgrade

func (s *Server) OnBeforeUpgrade(name string, callback OnBeforeUpgrade) bool

func (*Server) OnBeforeUpgradeRemove

func (s *Server) OnBeforeUpgradeRemove(name string)

func (*Server) OnClose

func (s *Server) OnClose(name string, callback OnClose) bool

func (*Server) OnCloseRemove

func (s *Server) OnCloseRemove(name string)

func (*Server) OnConnect

func (s *Server) OnConnect(name string, callback OnConnect) bool

func (*Server) OnConnectRemove

func (s *Server) OnConnectRemove(name string)

func (*Server) OnMessage

func (s *Server) OnMessage(name string, callback OnMessage) bool

func (*Server) OnMessageRemove

func (s *Server) OnMessageRemove(name string)

func (*Server) OnStart

func (s *Server) OnStart(name string, callback OnStart) bool

func (*Server) OnStartRemove

func (s *Server) OnStartRemove(name string)

func (*Server) OnStarted

func (s *Server) OnStarted(name string, callback OnStart) bool

func (*Server) OnStartedRemove

func (s *Server) OnStartedRemove(name string)

func (*Server) OnStop

func (s *Server) OnStop(name string, callback OnStop) bool

func (*Server) OnStopRemove

func (s *Server) OnStopRemove(name string)

func (*Server) OnStopped

func (s *Server) OnStopped(name string, callback OnStop) bool

func (*Server) OnStoppedRemove

func (s *Server) OnStoppedRemove(name string)

func (*Server) ServerStatus

func (s *Server) ServerStatus(onCollected func(status Status))

func (*Server) SetContext

func (s *Server) SetContext(ctx context.Context)

func (*Server) SetReadBufferSize

func (s *Server) SetReadBufferSize(bufferSize uint64)

func (*Server) SetStatusCredentials

func (s *Server) SetStatusCredentials(username string, password string) *Server

func (*Server) SetWriteBufferSize

func (s *Server) SetWriteBufferSize(bufferSize uint64)

func (*Server) Stack

func (s *Server) Stack(onCollected func(stack string))

func (*Server) Start

func (s *Server) Start() error

Start THe user can start it in a goroutine

func (*Server) Status

func (s *Server) Status(onCollected func(status FullStatus))

func (*Server) StatusHubs

func (s *Server) StatusHubs(onCollected func(status HubsStatus))

func (*Server) StatusNrOfClients

func (s *Server) StatusNrOfClients(onCollected func(status NrOfClientsStatus))

func (*Server) StatusSystemStatus

func (s *Server) StatusSystemStatus(onCollected func(status SystemStatus))

func (*Server) Stop

func (s *Server) Stop() error

func (*Server) UpgradeToWS

func (s *Server) UpgradeToWS(
	c *gin.Context,
	onMessage OnMessage,
	onUpgrade OnUpgrade,
) bool

UpgradeToWS -> You can call it from Http Server before the connection has being initialized

type Status

type Status struct {
	Name                  string
	ListeningAddresses    []string
	ListeningAddressesSSL []string
	CurrentConnectionID   uint64
	NrOfClients           uint
}

type SystemStatus

type SystemStatus struct {
	SystemStatus info.SystemStatus
}

type TextPayload

type TextPayload struct {
	// This is the data which should be sent!
	Message interface{}
	// This when the payload has being sent successfully
	OnFinish TextPayloadOnResponse
	// On response of a specific part
	OnPartResponse func()
	// On json error
	OnJsonError OnJsonError
	// This is the size of the packet... the message will be split into parts
	ChunkSize int64
}

type TextPayloadOnResponse

type TextPayloadOnResponse func(response ReceivedMessage)

type TextPayloadStr

type TextPayloadStr struct {
	PayloadID string
	SentTime  time.Time
	Data      interface{}
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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