-
Notifications
You must be signed in to change notification settings - Fork 3
Writing a Plugin: Server Type
It is about Casket v1.
Join the Casket Community Forum to chat with other Casket developers!
Casket ships with an HTTP server, but you can implement other server types and plug them into Casket. Other server types could be SSH, SFTP, TCP, something else used internally, etc.
To Casket, the notion of a server is anything that can Listen()
and Serve()
. What that means and how that works is up to you. Feel free to be creative and liberal with this idea.
If your server type can use TLS, it should take advantage of Casket's magic TLS features. We describe how to do that at the end of this guide.
At a high level, plugging in a server type is very easy, using the casket.RegisterServerType() function. You pass in the name of the server type and a casket.ServerType struct describing it.
Here's a (somewhat simplified) example of how the HTTP server does it:
import "github.com/mholt/casket"
func init() {
casket.RegisterServerType("http", casket.ServerType{
Directives: directives,
DefaultInput: func() casket.Input {
return casket.CasketfileInput{
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
ServerTypeName: "http",
}
},
NewContext: newContext,
})
}
As you can see, server types consist of a name ("http"), a list of directives, a default Casketfile input (optional, but recommended), and a context. We'll talk about each of these now.
Each server type gets to define which directives it recognizes and in what order they should be executed. This is simply a list of strings, where each string is the name of a directive:
var directives = []string{
"dir1",
"dir2",
"etc",
}
In most server types, the execution order matters significantly, so directives will not be executed if they are not in this list.
This is optional, but highly recommended in case you would rather not default to a blank Casketfile. What this means for your server type is up to you. But you can return an instance of casket.CasketfileInput to satisfy this function. Casket will call your function only if it is needed.
Casket tries to avoid as much global state as possible. When Casket loads, parses, and executes a Casketfile, it does so in a scope called an Instance so that a group of servers can be managed individually and multiple server types can be started in the same process.
Each time Casket goes through this startup phase, it will create a new casket.Instance and Casket will ask your server type for a new casket.Context value so that it can execute your server's directives without sharing global state. Read the godoc for casket.Context for more information.
You should not store a a list of contexts with your server type; they will be stored with the Instance and are accessible from Controllers. Keeping references to contexts lying around will prevent them from being garbage collected if the server is stopped later, which is likely a memory leak.
Ultimately, your goal is to make it so that MakeServers()
returns a list of casket.Server values that Casket can use. How you do that is entirely up to you.
For example: the HTTP server uses its context type to keep a map of site configs. The package also has an exported function, GetConfig(c Controller)
that gets a config for a certain site, designated by the casket.Controller that is passed in. The config, in turn, stores the list of middleware, etc -- all the information needed to set up a site. Each directive's action calls the GetConfig function so they can help set up the site properly. By the time MakeServers()
is called, all it has to do is combine those site configs into server instances and return them. (Server instances implement casket.Server.)
The casket.Server
interface considers both TCP and non-TCP servers. It has four methods; two are for TCP, two are for UDP or other:
type Server interface {
TCPServer // Required if using TCP
UDPServer // Required if using UDP or other
}
type TCPServer interface {
Listen() (net.Listener, error)
Serve(net.Listener) error
}
type UDPServer interface {
ListenPacket() (net.PacketConn, error)
ServePacket(net.PacketConn) error
}
If your server only uses TCP, the *Packet()
methods may be no-ops (i.e. they return nil
). The inverse is true for non-TCP servers. A server may also use both TCP and UDP, and implement all four methods.
Once this interface is implemented and configuration is properly applied from Casketfile directives, your server type is ready to roll.
Server types that can use TLS should enable TLS automatically before we add it to the Casket download page. Import the caskettls
package to use Casket's magic TLS features. This can seem a little confusing to integrate at first, but once it's working you'll love it and realize it's soooo worth it.
- You will need a way to store Casket TLS configurations for your server instances. Typically this just means adding a field to your server's config struct.
- That same server config struct type should implement the
caskettls.ConfigHolder
interface. This is just a few getter methods. - Call
RegisterConfigGetter()
in your package's init() so that the caskettls package knows how to ask for a config when it is parsing the Casketfile for your server type. (Your "config getter" will have to make a newcaskettls.Config
if one does not already exist for the given Controller.) - Add the
tls
directive to your server type's list of directives. Usually it goes near the front of the list.
When you are instantiating your actual server value and need a tls.Config
, you can call caskettls.MakeTLSConfig(tlsConfigs)
where tlsConfigs
is a []caskettls.Config
. This function will convert a list of Casket TLS configs to a single standard library tls.Config for you. You can then use this in a call to tls.NewListener()
.
Finally, you typically want to enable TLS while the Casketfile is being parsed. For example, the HTTP server configures HTTPS right after the tls
directive is executed. You should use your package's init()
function to register a parsing callback that goes through the configurations and configures TLS:
// replace "http" with your own server type name
casket.RegisterParsingCallback("http", "tls", activateHTTPS)
This code executes the activateHTTPS()
function after the tls
directive is finished setting up. Your server type should have a similar function that enables TLS in a way that makes sense to it. To give you an idea, the HTTP server's activateHTTPS function does the following:
- Prints a message to stdout, "Activating privacy features..." (if the operator is present; i.e.
casket.Started() == false
) because the process can take a few seconds - Sets the
Managed
field totrue
on all configs that should be fully managed - Calls
ObtainCert()
for each config (this method only obtains certificates if the config qualifies and has its Managed field set to true). - Configures the server struct to use the newly-obtained certificates by setting the
Enabled
field of the TLS config totrue
and callingcaskettls.CacheManagedCertificate()
which actually loads the cert into memory for use - Calls
caskettls.SetDefaultTLSParams()
to make sure all the necessary fields have a value - Calls
caskettls.RenewManagedCertificates(true)
to ensure that all certificates that were loaded into memory have been renewed if necessary
That's a lot to do, but you can also look at how the HTTP server does it (<-- this is a permalink, so the latest code may be better) for guidance.
To help preserve perfect forward secrecy, you should call caskettls.RotateSessionTicketKeys()
when instantiating your server value, passing in the TLS config. Be sure to close the channel it returns when your server is stopped.
Everything else about TLS: renewals, OCSP, and other maintenance, happen for you, since those are the same for all server types. All these steps simply hook your server type up to Casket's TLS package so that it knows how to do its job.
Test your integration of the caskettls package thoroughly. Once it's working well, let's add your server type to the download page!