rmhttp

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2024 License: MIT Imports: 17 Imported by: 2

README

rmhttp2

Documentation

Overview

Package rmhttp implements a lightweight wrapper around the standard library web server provided by http.Server and http.ServeMux, and adds an intuitive fluent interface for easy use and configuration of route grouping, centralised error handling, logging, CORS, panic recovery, SSL configuration, header management, timeouts and middleware.

The package allows you to use either standard http.Handler functions, or rmhttp.Handler functions, which are identical to http.Handler functions, with the addition of returning an error. Returning an error from your handlers allows rmhttp to provide centralised error handling, but if you'd rather handle your errors in your handler, you can simply use the net/http compayible App, and use net/http handlers natively.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConvertHandlerFunc

func ConvertHandlerFunc(
	handlerFunc func(http.ResponseWriter, *http.Request),
) func(http.ResponseWriter, *http.Request) error

------------------------------------------------------------------------------------------------ CONVERSION FUNCTIONS ------------------------------------------------------------------------------------------------ ConvertHandlerFunc converts, then returns, the passed Net/HTTP compatible HandlerFunc function to one that fulfils the rmhttp.HandlerFunc signature

func Error

func Error(w http.ResponseWriter, body string, code int)

Error is designed as a drop in replacement for http.Error.

This function will check for a Content-Type header in the Response and create either a JSON or plain text error in the Response via the ResponseWriter.

Plain text errors will default internally to being created with the http.Error function.

func ValidHTTPMethods

func ValidHTTPMethods() []string

ValidHTTPMethods returns a slice of strings containing all of the HTTP methods that rmhttp will accept.

Types

type App

type App struct {
	Server *Server
	Router *Router
	// contains filtered or unexported fields
}

App encapsulates the application and provides the public API, as well as orchestrating the core library functionality.

func New

func New(c ...Config) *App

New creates, initialises and returns a pointer to a new App. An optional configuration can be passed to configure many parts of the system, such as cors, SSL, and timeouts.

If you chose not to pass in a configuration, rmhttp will first attempt to load configuration values from environment variables, and if they're not found, will apply sensible defaults.

func (*App) Compile

func (app *App) Compile()

Compile prepares the app for starting by applying the middleware, and loading the Routes. It should be the last function to be called before starting the Server.

func (*App) Group added in v0.2.0

func (app *App) Group(pattern string) *Group

Group creates, initialises, and returns a pointer to a Route Group.

This is typically used to create new Routes as part of the Group, but can also be used to add Group specific middleware, timeouts, etc.

This method will return a pointer to the new Group, allowing the user to chain any of the other builder methods that Group implements.

func (*App) Handle

func (app *App) Handle(method string, pattern string, handler Handler) *Route

Handle binds the passed rmhttp.Handler to the specified route method and pattern.

This method will return a pointer to the new Route, allowing the user to chain any of the other builder methods that Route implements.

func (*App) HandleFunc

func (app *App) HandleFunc(
	method string,
	pattern string,
	handlerFunc func(http.ResponseWriter, *http.Request) error,
) *Route

HandleFunc converts the passed handler function to a rmhttp.HandlerFunc, and then binds it to the specified route method and pattern.

This method will return a pointer to the new Route, allowing the user to chain any of the other builder methods that Route implements.

func (*App) ListenAndServe

func (app *App) ListenAndServe() error

ListenAndServe compiles and loads the registered Routes, and then starts the Server without SSL.

func (*App) ListenAndServeTLS

func (app *App) ListenAndServeTLS() error

ListenAndServeTLS compiles and loads the registered Routes, and then starts the Server with SSL.

func (*App) Route added in v0.1.0

func (app *App) Route(route *Route)

Route adds a Route to the application at the top level.

This allows us to overwrite Routes prior to application start without causing the underlying http.ServeMux to throw an error.

func (*App) Routes

func (app *App) Routes() map[string]*Route

Routes returns a map of the currently added Routes.

func (*App) Shutdown

func (app *App) Shutdown() error

Shutdown stops the Server.

func (*App) Start

func (app *App) Start() error

Start compiles and loads the registered Routes, and then starts the Server with graceful shutdown management.

type CaptureWriter added in v0.2.0

type CaptureWriter struct {
	Writer http.ResponseWriter
	Code   int
	Body   string

	Mu sync.Mutex
	// contains filtered or unexported fields
}

A CaptureWriter wraps a http.ResponseWriter in order to capture HTTP the response code, body & headers that handlers will set. We do this to allow further processing based on this values before the final response is written, as writing a response status code can only be done once.

func NewCaptureWriter added in v0.2.0

func NewCaptureWriter(w http.ResponseWriter) *CaptureWriter

func (*CaptureWriter) Flush added in v0.2.0

func (cw *CaptureWriter) Flush()

Flush implements the Flusher interface.

func (*CaptureWriter) Header added in v0.2.0

func (cw *CaptureWriter) Header() http.Header

Header implements part of the http.ResponseWriter interface. We override it here in order to store any added headers without actually writing the response.

func (*CaptureWriter) Hijack added in v0.2.0

func (cw *CaptureWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)

Hijack implements the Hijacker interface.

func (*CaptureWriter) Persist added in v0.2.0

func (cw *CaptureWriter) Persist()

Persist writes the current status, body and headers to the underlying ResponseWriter.

func (*CaptureWriter) Push added in v0.2.0

func (cw *CaptureWriter) Push(target string, opts *http.PushOptions) error

Push implements the Pusher interface.

func (*CaptureWriter) Write added in v0.2.0

func (cw *CaptureWriter) Write(body []byte) (int, error)

Write implements part of the http.ResponseWriter interface. We override it here in order to store the response body without actually writing the response.

func (*CaptureWriter) WriteHeader added in v0.2.0

func (cw *CaptureWriter) WriteHeader(code int)

WriteHeader implements part of the http.ResponseWriter interface. We override it here in order to store the response code without actually writing the response.

type Config

type Config struct {
	Host                    string   `env:"HOST"`
	Port                    int      `env:"PORT"                       envDefault:"8080"`
	Debug                   bool     `env:"DEBUG"`
	EnablePanicRecovery     bool     `env:"ENABLE_PANIC_RECOVERY"`
	EnableHTTPLogging       bool     `env:"ENABLE_HTTP_LOGGING"`
	EnableHTTPErrorHandling bool     `env:"ENABLE_HTTP_ERROR_HANDLING"`
	LoggerAllowedMethods    []string `env:"LOGGER_ALLOWED_METHODS"     envDefault:"GET,POST,PATCH,PUT,DELETE,OPTIONS"`
	Logger                  Logger
	Cors                    CorsConfig
	SSL                     SSLConfig
	Timeout                 TimeoutConfig
}

------------------------------------------------------------------------------------------------ CONFIG ------------------------------------------------------------------------------------------------ The Config contains settings (with defaults) for configuring the app, server and router.

func LoadConfig

func LoadConfig(cfg Config) (Config, error)

loadConfig parses the environment variables defined in the Config objects (with defaults), then merges those with the config that the user may have supplied. This function only gets called during app initialisation.

This function will return a completed config, or error if the environment variables cannot be parsed.

type CorsConfig

type CorsConfig struct {
	Enable               bool     `env:"ENABLE_CORS"                 envDefault:"false"`
	AllowedOrigin        string   `env:"CORS_ALLOWED_ORIGIN"         envDefault:"*"`
	AllowedMethods       []string `env:"CORS_ALLOWED_METHODS"        envDefault:"GET,POST,PUT,PATCH,DELETE"`
	AllowedHeaders       []string `env:"CORS_ALLOWED_HEADERS"        envDefault:"Origin,Authorization,X-Forwarded-For"`
	ExposedHeaders       []string `env:"CORS_EXPOSED_HEADERS"        envDefault:"Origin,Authorization,X-Forwarded-For"`
	MaxAge               int      `env:"CORS_MAX_AGE"                envDefault:"300"`
	OptionsSuccessStatus int      `env:"CORS_OPTIONS_SUCCESS_STATUS" envDefault:"204"`
	AllowCredentials     bool     `env:"CORS_ALLOW_CREDENTIALS"      envDefault:"false"`
	PreflightVary        []string `env:"CORS_PREFLIGHT_VARY"         envDefault:"Origin"`
}

------------------------------------------------------------------------------------------------ CORS CONFIG ------------------------------------------------------------------------------------------------ The CorsConfig contains settings (with defaults) for configuring CORS in the router.

type Group

type Group struct {
	Pattern    string
	Middleware []MiddlewareFunc
	Timeout    Timeout
	Headers    map[string]string
	Parent     *Group
	Routes     map[string]*Route
	Groups     map[string]*Group
}

A Group allows for grouping sub groups or routes under a route prefix. It also enables you to add headers, timeout and middleware once to every sub group and route included in the group.

func NewGroup

func NewGroup(pattern string) *Group

NewGroup creates, initialises, and returns a pointer to a new Group

func (*Group) ComputedRoutes added in v0.1.0

func (group *Group) ComputedRoutes() map[string]*Route

ComputedRoutes returns a map of unique Routes composed from this Group and any sub Groups of this Group.

func (*Group) Group

func (group *Group) Group(g *Group) *Group

Group adds the passed Group as a sub group to this Group.

This method will return a pointer to the receiver Group, allowing the user to chain any of the other builder methods that Group implements.

func (*Group) Route

func (group *Group) Route(route *Route) *Group

Route adds the passed Route to this Group.

This method will return a pointer to the receiver Group, allowing the user to chain any of the other builder methods that Group implements.

func (*Group) Use

func (group *Group) Use(middlewares ...func(Handler) Handler) *Group

Use is a convenience method for adding middleware handlers to a Group. It uses WithMiddleware behind the scenes.

This method will return a pointer to the receiver Group, allowing the user to chain any of the other builder methods that Group implements.

func (*Group) WithHeader

func (group *Group) WithHeader(key, value string) *Group

WithHeader sets an HTTP header for this Group. Calling this method more than once will either overwrite an existing header, or add a new one.

This method will return a pointer to the receiver Group, allowing the user to chain any of the other builder methods that Group implements.

func (*Group) WithMiddleware

func (group *Group) WithMiddleware(middlewares ...func(Handler) Handler) *Group

WithMiddleware adds Middleware handlers to the receiver Group.

Each middleware handler will be wrapped to create a call stack with the order in which the middleware is added being maintained. So, for example, if the user added A and B middleware via this method, the resulting callstack would be as follows -

Middleware A -> Middleware B -> Route Handler -> Middleware B -> Middleware A

(This actually a slight simplification, as internal middleware such as HTTP Logging, CORS, HTTP Error Handling and Route Panic Recovery may also be inserted into the call stack, depending on how the App is configured).

The middlewares argument is variadic, allowing the user to add multiple middleware functions in a single call.

This method will return a pointer to the receiver Group, allowing the user to chain any of the other builder methods that Group implements.

func (*Group) WithTimeout

func (group *Group) WithTimeout(timeout time.Duration, message string) *Group

WithTimeout sets a request timeout amount and message for this Group.

This method will return a pointer to the receiver Group, allowing the user to chain any of the other builder methods that Group implements.

type HTTPError

type HTTPError struct {
	Err  error
	Code int
}

An HTTPError represents an error with an additional HTTP status code

func NewHTTPError

func NewHTTPError(err error, code int) HTTPError

NewHTTPError creates and returns a new, initialised pointer to a HTTPError

func (HTTPError) Error

func (e HTTPError) Error() string

Error returns the error text of the receiver HTTPError as a string.

This method allows HTTPError to implement the standard library Error interface.

func (HTTPError) Unwrap added in v0.3.0

func (e HTTPError) Unwrap() error

Unwrap returns the underlying Error that this HTTPError wraps.

This method allows an HTTPError to be used by errors.Is().

type Handler

type Handler interface {
	ServeHTTP(http.ResponseWriter, *http.Request)
	ServeHTTPWithError(http.ResponseWriter, *http.Request) error
}

------------------------------------------------------------------------------------------------ HANDLER INTERFACE ------------------------------------------------------------------------------------------------ Handler implements the http.Handler interface and adds ServeHTTPWithError, allowing Handlers to return errors.

type HandlerFunc

type HandlerFunc func(http.ResponseWriter, *http.Request) error

------------------------------------------------------------------------------------------------ HANDLERFUNC ------------------------------------------------------------------------------------------------ HandlerFunc defines the function signature for HTTP handler functions in rmhttp, as well as implementing the rmhttp.Handler interface.

The only difference between a http.HandlerFunc and rmhttp.HandlerFunc is that our version can return errors. The signature is the same otherwise, so as to provide as familiar an API as possible.

func ConvertHandler

func ConvertHandler(handler http.Handler) HandlerFunc

ConvertHandler converts, then returns, the passed http.Handler to a rmhttp.HandlerFunc, which implements the rmhttp.Handler interface.

func (HandlerFunc) ServeHTTP

func (hf HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP fulfills the http.Handler interface, and part of the rmhttp.Handler interface. It behaves exactly the same as a http.Handler.ServeHTTP call.

func (HandlerFunc) ServeHTTPWithError

func (hf HandlerFunc) ServeHTTPWithError(w http.ResponseWriter, r *http.Request) error

ServeHTTPWithError implements part of the rmhttp.Handler interface. It behaves very similarly to http.Handler.ServeHTTP, except that it also returns an error.

It is also functionally equivalent to just calling the HandlerFunc directly.

type Logger

type Logger interface {
	Debug(string, ...any)
	Info(string, ...any)
	Warn(string, ...any)
	Error(string, ...any)
}

A Logger receives a message and optional args with the intention of logging the message and args with an importance level defined by which method is called.

The variadic args should be entered as pairs, with the odd numbered acting as keys for the next value. Adding an odd number of arguments here will result in unpredictable behaviour.

This interface is implicitly implemented by the standard library slog logger.

type MiddlewareFunc

type MiddlewareFunc func(Handler) Handler

func HTTPErrorHandlerMiddleware added in v0.2.0

func HTTPErrorHandlerMiddleware(registeredErrors map[error]int) MiddlewareFunc

HTTPErrorHandlerMiddleware returns a MiddlwareFunc compatible function that handles any errors that have been returned by a handler. It will also create an appropriate HTTP error in the case of the response having a status code in the error range (400 and above), but no error was returned from the handler. This will allow any other middleware to assume that if they have not received an error, then no error has occurred.

func HTTPErrorLoggerMiddleware added in v0.5.0

func HTTPErrorLoggerMiddleware(logger Logger) MiddlewareFunc

HTTPErrorHandlerMiddleware returns a MiddlwareFunc compatible function that handles any errors that have been returned by a handler. It will also create an appropriate HTTP error in the case of the response having a status code in the error range (400 and above), but no error was returned from the handler. This will allow any other middleware to assume that if they have not received an error, then no error has occurred.

func HeaderMiddleware

func HeaderMiddleware(headers map[string]string) MiddlewareFunc

HeaderMiddleware creates and returns a MiddlewareFunc that will apply all of the headers that have been passed in.

func TimeoutMiddleware

func TimeoutMiddleware(timeout Timeout) MiddlewareFunc

TimeoutMiddleware creates, initialises and returns a middleware function that will wrap the next handler in the stack with a timeout handler.

type Route

type Route struct {
	Method     string
	Pattern    string
	Handler    Handler
	Middleware []MiddlewareFunc
	Timeout    Timeout
	Headers    map[string]string
	Parent     *Group
}

A Route encapsulates all of the information that the router will need to satisfy an HTTP request. Alongside supplying standard information such as what HTTP method and URL pattern a handler should be bound to, the Route also allows the enclosed handler to be configured with their own timeout, headers, and middleware.

func NewRoute

func NewRoute(method string, pattern string, handler Handler) *Route

NewRoute validates the input, then creates, initialises and returns a pointer to a Route. The validation step ensures that a valid HTTP method has been passed (http.MethodGet will be used, if not). The method will also be transformed to uppercase, and the pattern to lowercase.

func (*Route) ComputedHeaders

func (route *Route) ComputedHeaders() map[string]string

ComputedHeaders dynamically calculates the HTTP headers that have been added to the Route and any parent Groups.

func (*Route) ComputedMiddleware

func (route *Route) ComputedMiddleware() []MiddlewareFunc

Middleware returns the slice of MiddlewareFuncs that have been added to the Route.

func (*Route) ComputedPattern

func (route *Route) ComputedPattern() string

ComputedPattern dynamically calculates the pattern for the Route. It returns the URL pattern as a string.

func (*Route) ComputedTimeout

func (route *Route) ComputedTimeout() Timeout

Timeout returns the Timeout object that has been added to the Route.

func (*Route) String

func (route *Route) String() string

String is used internally to calculate a string signature for use as map keys, etc.

func (*Route) Use

func (route *Route) Use(middlewares ...func(Handler) Handler) *Route

Use is a convenience method for adding middleware handlers to a Route. It uses WithMiddleware behind the scenes.

This method will return a pointer to the receiver Route, allowing the user to chain any of the other builder methods that Route implements.

func (*Route) WithHeader

func (route *Route) WithHeader(key, value string) *Route

WithHeader sets an HTTP header for this route. Calling this method more than once will either overwrite an existing header, or add a new one.

This method will return a pointer to the receiver Route, allowing the user to chain any of the other builder methods that Route implements.

func (*Route) WithMiddleware

func (route *Route) WithMiddleware(middlewares ...func(Handler) Handler) *Route

WithMiddleware adds Middleware handlers to the receiver Route.

Each middleware handler will be wrapped to create a call stack with the order in which the middleware is added being maintained. So, for example, if the user added A and B middleware via this method, the resulting callstack would be as follows -

Middleware A -> Middleware B -> Route Handler -> Middleware B -> Middleware A

(This actually a slight simplification, as internal middleware such as HTTP Logging, CORS, HTTP Error Handling and Route Panic Recovery may also be inserted into the call stack, depending on how the App is configured).

The middlewares argument is variadic, allowing the user to add multiple middleware functions in a single call.

This method will return a pointer to the receiver Route, allowing the user to chain any of the other builder methods that Route implements.

func (*Route) WithTimeout

func (route *Route) WithTimeout(timeout time.Duration, message string) *Route

WithTimeout sets a request timeout amount and message for this route.

This method will return a pointer to the receiver Route, allowing the user to chain any of the other builder methods that Route implements.

type Router

type Router struct {
	Mux    *http.ServeMux
	Logger Logger
}

The Router loads Routes into the underlying HTTP request multiplexer, as well as handling each request, ensuring that ResponseWriter and Request objects are properly configured. The Router also manages custom error handlers to ensure that the HTTP Error Handler can operate properly.

func NewRouter

func NewRouter(logger Logger) *Router

NewRouter intialises, creates, and then returns a pointer to a Router.

func (*Router) Handle

func (rt *Router) Handle(method string, pattern string, handler Handler)

Handle registers the passed Route with the underlying HTTP request multiplexer.

func (*Router) ServeHTTP

func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP allows the Router to fulfill the http.Handler interface, meaning that we can use it as a handler for the underlying HTTP request multiplexer (which by default is a http.ServeMux).

Having the Router act as the primary handler allows us to inject our custom ResponseWriter and add the system logger to the Request (for use by any middleware).

We can also intercept any error handlers returned by the underlying mux, and make sure that they are properly wrapped by the HTTP Error Handler and HTTP Logger (assuming the system is configured to enable them), as well as any middleware that was configured for the route.

The Router is one of the few places where you will see ServeHTTP used instead of ServeHTTPWithError in the system.

type SSLConfig

type SSLConfig struct {
	Enable bool   `env:"ENABLE_SSL"`
	Cert   string `env:"SSL_CERT"`
	Key    string `env:"SSL_KEY"`
}

------------------------------------------------------------------------------------------------ SSL CONFIG ------------------------------------------------------------------------------------------------ The SSLConfig contains settings (with defaults) for configuring SSL in the server.

type Server

type Server struct {
	Server http.Server
	Router http.Handler
	Logger Logger
	Host   string
	Port   int
	// contains filtered or unexported fields
}

A Server wraps the standard library net/http.Server. It provide default lifecycle management and debugger logging on top of the expected http.Server behaviour.

func NewServer

func NewServer(
	config ServerConfig,
	router http.Handler,
	logger Logger,
) *Server

NewServer creates, initialises and returns a pointer to a Server.

func (*Server) Address

func (srv *Server) Address() string

Address returns the server host and port as a formatted string ($HOST:$PORT).

func (*Server) ListenAndServe

func (srv *Server) ListenAndServe() error

ListenAndServe directly proxies the http.Server.ListenAndServe method. It starts the server without TLS support on the configured address and port.

func (*Server) ListenAndServeTLS

func (srv *Server) ListenAndServeTLS() error

ListenAndServeTLS directly proxies the http.Server.ListenAndServeTLS method. It starts the server with TLS support on the configured address and port.

func (*Server) Shutdown

func (srv *Server) Shutdown(ctx context.Context) error

Shutdown directly proxies the net/http.Server.Shutdown method. It will stop the Server, if running.

func (*Server) Start

func (srv *Server) Start(useTLS bool) error

Start starts and manages the lifecycle of the underlyinh http.Server, facilitating graceful shutdowns and optional SSL support.

type ServerConfig

type ServerConfig struct {
	TimeoutConfig
	SSLConfig
	Host string
	Port int
	Cert string
	Key  string
}

type Timeout

type Timeout struct {
	Duration time.Duration
	Message  string
	Enabled  bool
}

Timeout encapsulates a duration and message that should be used for applying timeouts to Route handlers, with a specific error message.

func NewTimeout

func NewTimeout(duration time.Duration, message string) Timeout

NewTimeout creates, initialises and returns a pointer to a Timeout.

type TimeoutConfig

type TimeoutConfig struct {
	TCPReadTimeout         int    `env:"TCP_READ_TIMEOUT"         envDefault:"2"`
	TCPReadHeaderTimeout   int    `env:"TCP_READ_HEADER_TIMEOUT"  envDefault:"1"`
	TCPIdleTimeout         int    `env:"TCP_IDLE_TIMEOUT"         envDefault:"120"`
	TCPWriteTimeout        int    `env:"TCP_WRITE_TIMEOUT"        envDefault:"5"`
	TCPWriteTimeoutPadding int    `env:"TCP_WRITE_TIMEOUT_BUFFER" envDefault:"1"`
	RequestTimeout         int    `env:"HTTP_REQUEST_TIMEOUT"     envDefault:"7"`
	TimeoutMessage         string `env:"HTTP_TIMEOUT_MESSAGE"     envDefault:"Request Timeout"`
}

------------------------------------------------------------------------------------------------ TIMEOUT CONFIG ------------------------------------------------------------------------------------------------ The TimeoutConfig contains settings (with defaults) for configuring timeouts in the system. These settings mostly correlate to those used by the underlying http.Server

type TimeoutHandler added in v0.2.0

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

A TimeoutHandler implements the Handler interface. Its primary purpose is to wrap an HTTPHandler and provide an execution timeout.

Every route handler, with the exception of those dynamically generated in response to an internal error, will be wrapped with a TimeoutHandler. There is no configurable option to turn this off for security reasons, but a user could set it to a very large duration, if desired.

This implementation feels very hacky but I can't think of a better way to implement per route timeouts with our custom HandlerFunc error returning. This is basically just a simplified version of the net/http implementation with some minor changes to accomodate passing errors through the timeout handler. It's necessary as net/http sets this functionality to be unexportable, so we can't just embed timeout handlers into our own structs.

https://cs.opensource.google/go/go/+/master:src/net/http/server.go

func NewTimeoutHandler added in v0.2.0

func NewTimeoutHandler(
	handler Handler,
	timeout Timeout,
) *TimeoutHandler

TimeoutHandler creates, initialises and returns a pointer to a new timeoutHandler.

func (*TimeoutHandler) ServeHTTP added in v0.2.0

func (h *TimeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP fulfills the http.Handler interface but is rarely used. You should prefer ServeHTTPWithError wherever possible.

func (*TimeoutHandler) ServeHTTPWithError added in v0.2.0

func (h *TimeoutHandler) ServeHTTPWithError(w http.ResponseWriter, r *http.Request) error

ServeHTTPWithError implements the rmhttp.Handler interface and handles the actual timeout management.

This function is a simplified version of the net/http version, with the addition of error returning.

Directories

Path Synopsis
middleware
cors module
headers module
recoverer module
pkg
capturewriter module

Jump to

Keyboard shortcuts

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