Write a Web Service with Go Plug-Ins

The target of this article is to inspect why and how to build a modular web service.

In an automation driven environment, there are many conditions that bring project evolution to Hell. Some of those arise from a non-modular environment.

Even if you are on a microservices architecture, with a compiled language all this case leads to rebuilding the whole service project:

  • Edit how a single part of the project work.
  • Optimize the scaling up of a single endpoint.

And also, you will face some of these uncomfortable situations:

  • Reuse the same behaviour you have in a service into another service.
  • You probably can use only a single programming language.
  • You need to share the whole project if you have a team (this little thing in an off-shoring project can be very bad).

With a plugin architecture, you can develop the service as a set of components that enable to:

  • Have function-dedicated teams
  • Detached deployment for the components with a rolling update.
  • Reuse a component in several projects.
  • Customize how to scale a service by configuring winch component you have to include.
  • Last but not least, you can develop the component in each language that builds a library!

Architecture

Taking an example web service, we can split our project into the following parts:

  • Core: that will read the configurations, load the component, listen and serve the HTTP.
  • Controller: an HTTP Handler function.
  • Middlewares: a component that checks if the request is authorized to access the controller.

In this way, if you update the core to HTTPS, you can redeploy only the core file and only for the services that need to be HTTPS compliant. In the same way, if you update the JWT plugin to use a new hash method, you have only to redeploy the plugin and reload the core.

Architecture components

Controller

A controller is an HTTP handler function that will apply logic to the request to build and return the response.

Middlewares

This kind of objects arises from the needs to do things like filter, trace or log, the incoming request, without involving all the project to manage it.

A kind of middleware can be an IP filter: you want to enable only certain IP to access your service. When the request reaches your server, you can directly check if the source IP is in your whitelist. In this way, you don’t load other parts of the service.

One of the advantages can be a reduction of the risk to expose detail in case of malicious intent.

Example: let’s create another middleware that checks the access headers. Now, chaining it with the previous IP middleware, only the users authorized in our IP pool can access the service.

So, if the first step in the controller is to query your DB for user data, you’re safe from SQL injection. That’s because you don’t start communicating with the DB if all the preconditions are fine.

Core

As explained, for this project we use the Plugin functionality to separate the components. For our scope, the Core component will care about loading Controllers and Middlewares Plugins, map it to the endpoints using a configuration file and finally start the HTTP listener.

All other routes will return “404 not found”.

Mapping middlewares and controller to an endpoint

This level of independence can be easily loved for cloud use-case:


Implementation

Let’s start from a basic core with a controller (HTTP function handler) that responds to our home:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func controller(w http.ResponseWriter, r *http.Request) {
  8. fmt.Fprintf(w, "Hi there!")
  9. }
  10. func main() {
  11. http.HandleFunc("/", controller)
  12. log.Fatal(http.ListenAndServe(":8080", nil))
  13. }

On top of this, add the constructs needed to implement the middlewares:

  1. // middleware filter incoming HTTP requests.
  2. // if the request pass the filter, it calls the next HTTP handler.
  3. type middleware func(http.HandlerFunc) http.HandlerFunc
  1. func Chain(f http.HandlerFunc, mids ...middleware) http.HandlerFunc {
  2. for _, m := range mids {
  3. f = m(f)
  4. }
  5. return f
  6. }

Now build a sample middleware: method middleware checks the match between the HTTP method and the one passed as argument. If not, returns a 400 Bad Request.

The arguments in this case, are a sequence of approved HTTP methods that we need to split and check:

  1. func pass(args string) func(http.HandlerFunc) http.HandlerFunc {
  2. return func(f http.HandlerFunc) http.HandlerFunc {
  3. // Define the http.HandlerFunc
  4. return func(w http.ResponseWriter, r *http.Request) {
  5. //split args and check if the request as this method
  6. acceptedMethods := strings.Split(args, "|")
  7. for _, v := range acceptedMethods {
  8. if r.Method == v {
  9. // Call the next middleware in chain
  10. f(w, r)
  11. return
  12. }
  13. }
  14. http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
  15. return
  16. }
  17. }
  18. }

It’s time to attach the middleware inside the Chain, passing what HTTP Method you want to allow in function arguments.

Then pass the Chain to the HandlerFunc inside our basic HTTP service:

  1. var chain []middleware //empty chain
  2. func main() {
  3. chain = append(chain, pass("GET|POST"))
  4. http.HandleFunc("/", Chain(controller, chain...))
  5. log.Fatal(http.ListenAndServe(":8080", nil))
  6. }

Full example source code here


Plugins

N.B. it works only on Linux, but containers provides a great workaround.

The package of a plugin needs to be “Main”. Unlike that, the package can’t see the entities such as types and functions in the “real” main package. So, as a suggestion, maintain plugins dumber as possible.

Inside the Plugin, you must export a variable or a function as the symbol to load

  1. package main
  2. import "fmt"
  3. func Talk() {
  4. fmt.Println("Hello FROM PLUGIN!!!")
  5. }

To build the plugin, we need to use the -buildmode=plugin flag and specify the result name

  1. $go build -buildmode=plugin -o first.so first.go

You have built your first plugin!
Search in your path ad you will see first.so file that is a standard library that you can import in any language that supports this.

I’m proud of you my little padawan… let’s use it in GO:

Make a new file “main.go” and load the library file you created before:

  1. package main
  2. import (
  3. "os"
  4. "fmt"
  5. "plugin"
  6. )
  7. func main() {
  8. //open plugin file
  9. plug, err := plugin.Open("plugins/first.so")
  10. if err != nil {
  11. fmt.Println(err)
  12. os.Exit(-1)
  13. }

Add the symbol search code:

  1. //searc for an exported symbol
  2. symbol, err := plug.Lookup("Talk")
  3. if err != nil {
  4. fmt.Println(err)
  5. os.Exit(-1)
  6. }

Now you can use your function from the plugin inside “main.go”:

  1. //call the function
  2. symbol.(func())()
  3. } //remember to close the main func :)

RUN

  1. $ go run main.go

you will see

  1. Hello FROM PLUGIN!!!

Example source code here

It’s time to move on GO ‘Oop-style’ to get more than a single function from the Plugins.

Evolve the first.go plugin using a type through which you can attach functions as methods, then export a variable symbol that refers to the type as an object:

  1. package main
  2. import "fmt"
  3. type myPlugin string
  4. func (h myPlugin) Talk() {
  5. fmt.Println("Hello FROM PLUGIN!!!")
  6. }
  7. var MyPlugin myPlugin

Now you have to change the kind of import in a more secure way in the main.go:

  1. package main
  2. import (
  3. "os"
  4. "fmt"
  5. "plugin"
  6. )
  7. //define a local interface of what you want to get from plugin symbol
  8. type MyPlug interface {
  9. Talk()
  10. }
  11. func main() {
  12. //open plugin file
  13. plug, err := plugin.Open("plugins/first.so")
  14. if err != nil {
  15. fmt.Println(err)
  16. os.Exit(-1)
  17. }
  18. //searc for an exported symbol
  19. symbol, err := plug.Lookup("MyPlugin")
  20. if err != nil {
  21. fmt.Println(err)
  22. os.Exit(-1)
  23. }
  24. // check that loaded symbol is type Controller
  25. var myPlugin MyPlug
  26. myPlugin, ok := symbol.(MyPlug)
  27. if !ok {
  28. fmt.Println("The module have wrong type")
  29. os.Exit(-1)
  30. }
  31. //call the function
  32. myPlugin.Talk()
  33. }

ugins Implementation

At this moment you know:

  • what is the project target
  • what is the target architecture
  • what is the core
  • what is a controller
  • what is a middleware
  • how to build a plugin and use it

you’re ready to build the “controller” plugin!

In our repository create a plugin folder:

  1. $mkdir plugins

Inside we create two folders, one for middlewares, one for controllers

  1. $cd plugins
  2. $mkdir controller
  3. $mkdir middlewares

Build the Controllers

Inside the plugins/controllers folder create general.go:

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. type controller string
  7. func (h controller) Fire(w http.ResponseWriter, r *http.Request) {
  8. fmt.Fprintf(w, "Hello FROM CONTROLLER PLUGIN!!!")
  9. }
  10. // Controller exported namevar
  11. Controller controller

Build the Middlewares

Now export the method middleware you have done before, inside plugin.

Under plugins/middlewares folder create method.so:

  1. package main
  2. import (
  3. "net/http"
  4. "strings"
  5. )
  6. type middleware string
  7. func (m middleware) Pass(args string) func(http.HandlerFunc) http.HandlerFunc {
  8. return func(f http.HandlerFunc) http.HandlerFunc {
  9. // Define the http.HandlerFunc
  10. return func(w http.ResponseWriter, r *http.Request) {
  11. //split args and check if the request as this method
  12. acceptedMethods := strings.Split(args, "|")
  13. for _, v := range acceptedMethods {
  14. if r.Method == v {
  15. // Call the next middleware in chain
  16. f(w, r)
  17. return
  18. }
  19. }
  20. http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
  21. return
  22. }
  23. }
  24. }
  25. // export as symbol named "Middleware"
  26. var Middleware middleware

build the plugins:

  1. $go build -buildmode=plugin -o plugins/middlewares/method.so plugins/middlewares/method.go
  1. $go build -buildmode=plugin -o plugins/controllers/genearal.so plugins/controllers/genearal.go

Import the Plugins

To import the Plugins you will load it from a configuration file that map endpoints to middlewares and controller.

Sample routes.json looks like this:

  1. {
  2. "endpoints":[
  3. {
  4. "path":"/myroute",
  5. "handler":"./plugins/controllers/general.so",
  6. "middlewares":[
  7. {
  8. "handler":"./plugins/middlewares/method.so",
  9. "params":"GET|POST"
  10. }
  11. ]
  12. }
  13. ]
  14. }

Creating the file in this way, you can attach several middlewares to a route and use a middleware in several routes.

Read the configurations

Now you can proceed on reading the configuration and map it to a struct (with this tool is very simple to translate json to go struct):

  1. //source routes configuration struct to load from the json configuration file
  2. type routes struct {
  3. Endpoints []struct {
  4. Controller string `json:"controller"`
  5. Middlewares []struct {
  6. Handler string `json:"handler"`
  7. Params string `json:"params"`
  8. } `json:"middlewares"`
  9. Path string `json:"path"`
  10. } `json:"endpoints"`
  11. }
  12. var RoutesConf routes

And let’s make a function to read from JSON:

  1. //ReadFromJSON function load a json file into a struct or return error
  2. func ReadFromJSON(t interface{}, filename string) error {
  3. jsonFile, err := ioutil.ReadFile(filename)
  4. if err != nil {
  5. return err
  6. }
  7. err = json.Unmarshal([]byte(jsonFile), t)
  8. if err != nil {
  9. log.Fatalf("error: %v", err)
  10. return err
  11. }
  12. return nil
  13. }

Load the Plugins

As you call an exported type method from the plugin, we need to adopt some conventions, I opted for:

  • Controller type with method Fire()
  • Middleware type with method Pass()

Walking into the configuration we can dynamically link the libraries:

From “plugin.Open” documentation: If a path has already been opened, then the existing *Plugin is returned It is safe for concurrent use by multiple goroutines.

Load Controller plugin:

  1. for _, v := range RoutesConf.Endpoints {
  2. //load module:
  3. plug, err := plugin.Open(v.Controller)
  4. if err != nil {
  5. kill(err)
  6. }
  7. //look up for an exported Controller type
  8. symController, err := plug.Lookup("Controller")
  9. if err != nil {
  10. kill(err)
  11. }
  12. //check that loaded symbol is type Controller
  13. var controller Controller
  14. controller, ok := symController.(Controller)
  15. if !ok {
  16. kill("The Controller module have wrong type")
  17. }
  18. //define new middleware chain
  19. var chain []Gate

Load middleware modules to attach on the route:

  1. for _, mid := range v.Middlewares {
  2. //load middleware plugin
  3. plug, midErr := plugin.Open(mid.Handler)
  4. if midErr != nil {
  5. log.Fatalf(midErr)
  6. os.Exit(-1)
  7. }
  8. //look up the Middleware type
  9. symMiddleware, midErr := plug.Lookup("Middleware")
  10. if midErr != nil {
  11. log.Fatalf(midErr)
  12. os.Exit(-1)
  13. }
  14. //check that loaded symbol is type Middleware
  15. var middleware Middleware
  16. middleware, ok := symMiddleware.(Middleware)
  17. if !ok {
  18. log.Fatalf("The middleware module have wrong type")
  19. os.Exit(-1)
  20. }
  21. //build the gate function that contain the middleware funcition instance with args
  22. nmid := Gate(middleware.Pass(mid.Params))
  23. //append to the middlewares chain
  24. chain = append(chain, nmid)
  25. }
  26. // Use all the modules to handle the request
  27. http.HandleFunc(v.Path, Chain(controller.Fire, chain...))
  28. }

Now we can put all together to work starting the web server and test our service.

  1. $go build -o start -v

You can see in the complete Repository with other features:

  • script to create plugins scaffold
  • makefile to build all and clean all
  • test for standard implementation

Thanks to @inanc?source=post_page">Inanc Gumus.


A modular web server in Go


Image 1: Architecture Representation

The target of this project is to inspect how to build a modular web server. It
will send the requests through our pluggable modules: middlewares and
controller.


how would we like to use it?

Our scope is to care only about the mapping of a request to a handlers, adding
some middlewares from a set.


Image 2: Mapping middlewares and controller to an endpoint

This kind of architecture is scalable, customizable and reusable. It enables us
to make:

  1. specialized web services
  2. use the same web services for several projects
  3. Update separately the “core” server part and the plugins.

I put particular attention to “update separately”. With plugin architecture,
you can distribute the compiled file of only one component. For example: if you
update the core to HTTPS architecture, you can redeploy only the core file. In
the same way, if you update the JWT plugin to use a new ash method, you have
only to redeploy the plugin.


Image 3: Distributed deployment example

Implementation

We can start building our routes configuration file. An example configuration
can be a single route managed. We attach a plugin to check if the request HTTP
Method is GET or POST and then send it to a controller.

All other routes will return “404 not found”.

routes.json will look like this:

  1. {
  2. "endpoints":[
  3. {
  4. "path":"/myroute",
  5. "handler":"./plugins/controllers/general.so",
  6. "middlewares":[
  7. {
  8. "handler":"./plugins/middlewares/method.so",
  9. "params":"GET|POST"
  10. }
  11. ]
  12. }
  13. ]
  14. }

Creating the file in this way, we can attach several middlewares to a route and
use a middleware in several routes.

Build the core

Make the middleware chain architecture

Our middleware concept will chain a set of functions. This functions will check
the request and if it passes the filter, send it to the next function.

Gate is the type that represents the middleware function with arguments valued:

  1. type Gate func(http.HandlerFunc) http.HandlerFunc
  2. func Chain(f http.HandlerFunc, middlewares ...Gate) http.HandlerFunc {
  3. for _, m := range middlewares {
  4. f = m(f)
  5. }
  6. return f
  7. }

Read the configurations

Now we can proceed on reading the configuration, mapping it to a struct (with
this tool is very simple):

  1. //source routes configuration struct to load from the json configuration file
  2. type routes struct {
  3. Endpoints []struct {
  4. Controller string `json:"controller"`
  5. Middlewares []struct {
  6. Handler string `json:"handler"`
  7. Params string `json:"params"`
  8. } `json:"middlewares"`
  9. Path string `json:"path"`
  10. } `json:"endpoints"`
  11. }
  12. var RoutesConf routes

and make the function to read from JSON:

  1. /ReadFromJSON function load a json file into a struct or return error
  2. func ReadFromJSON(t interface{}, filename string) error {
  3. jsonFile, err := ioutil.ReadFile(filename)
  4. if err != nil {
  5. return err
  6. }
  7. err = json.Unmarshal([]byte(jsonFile), t)
  8. if err != nil {
  9. log.Fatalf("error: %v", err)
  10. return err
  11. }
  12. return nil
  13. }

Load the Plugins

We can load plugins, using the plugin package. we can import all the exposed
functions and variables
(ELF symbols).

As we call an exported type method from the plugin, we need to adopt some
conventions, I opted for:

  • Controller type with method Fire()
  • Middleware type with method Pass()

Walking into the configuration we can dynamically link the libraries:

From “plugin.Open” documentation: If a path has already been opened, then the
existing *Plugin is returned It is safe for concurrent use by multiple
goroutines.

Load Controller plugin:

  1. for _, v := range RoutesConf.Endpoints {
  2. // load module:
  3. plug, err := plugin.Open(v.Controller)
  4. if err != nil {
  5. kill(err)
  6. }
  7. // look up for an exported Controller method
  8. symController, err := plug.Lookup("Controller")
  9. if err != nil {
  10. kill(err)
  11. }
  12. // check that loaded symbol is type Controller
  13. var controller Controller
  14. controller, ok := symController.(Controller)
  15. if !ok {
  16. kill("The Controller module have wrong type")
  17. }
  18. //define new middleware chain
  19. var chain []Gate

Load middleware modules to attach on the route:

  1. for _, mid := range v.Middlewares {
  2. // load middleware plugin
  3. plug, midErr := plugin.Open(mid.Handler)
  4. if midErr != nil {
  5. kill(midErr)
  6. }
  7. // look up the Pass function
  8. symMiddleware, midErr := plug.Lookup("Middleware")
  9. if midErr != nil {
  10. kill(midErr)
  11. }
  12. // check that loaded symbol is type Middleware
  13. var middleware Middleware
  14. middleware, ok := symMiddleware.(Middleware)
  15. if !ok {
  16. kill("The middleware module have wrong type")
  17. }
  18. // build the gate function that contain the middleware instance
  19. nmid := Gate(middleware.Pass(mid.Params))
  20. // append to the middlewares chain
  21. chain = append(chain, nmid)
  22. }
  23. // Use all the modules to handle the request
  24. http.HandleFunc(v.Path, Chain(controller.Fire, chain...))
  25. }

Plugins Implementation

The package of a plugin needs to be “Main”.

Unlike that, the package can’t see the entities such as types and functions in
the “real” main package. So, as a suggestion, maintain plugins dumber as
possible
.

In our repository create a plugin folder:

  1. mkdir plugins

Inside we create two folders, one for middlewares, one for controllers

  1. cd plugins
  2. mkdir controller
  3. mkdir middlewares

Build the Controllers

Inside the plugins/controllers folder create general.so, this will be the HTTP
Request handler:

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. type controller string
  7. func (h controller) Fire(w http.ResponseWriter, r *http.Request) {
  8. fmt.Fprintf(w, "Hello FROM CONTROLLER PLUGIN!!!")
  9. }
  10. // Controller exported namevar
  11. Controller controller

Build the Middlewares

We build a method middleware that checks the HTTP Method, else returns a 400 Bad
Request.

To leave middleware “open”, it needs some arguments. In this case, a sequence of
approved HTTP methods that we need to split and check:

  1. package main
  2. import (
  3. "net/http"
  4. "strings"
  5. )
  6. type middleware string
  7. func (m middleware) Pass(args string) func(http.HandlerFunc) http.HandlerFunc {
  8. return func(f http.HandlerFunc) http.HandlerFunc {
  9. // Define the http.HandlerFunc
  10. return func(w http.ResponseWriter, r *http.Request) {
  11. //split args and check if the request as this method
  12. acceptedMethods := strings.Split(args, "|")
  13. for _, v := range acceptedMethods {
  14. if r.Method == v {
  15. // Call the next middleware in chain
  16. f(w, r)
  17. return
  18. }
  19. }
  20. http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
  21. return
  22. }
  23. }
  24. }
  25. // export as symbol named "Middleware"
  26. var Middleware middleware

To build the plugin library, we need to use the -buildmode=plugin flag and
specify the result name:

  1. go build -buildmode=plugin -o plugins/middlewares/method.so plugins/middlewares/method.go
  2. go build -buildmode=plugin -o plugins/controllers/genearal.so plugins/controllers/genearal.go

Now we can put all together to work starting the web server and test our
service.

  1. go build -o start -v

N.B. it works only on Linux, but with container we can solve this issue

Usage

  • create middleware plugins under plugins/middlewares
  • create handler plugins under plugins/handlers

  • configure routes in the configurations/routes.json file

  • configure server in the configurations/server.json file

Tools

the create.sh script provide scaffold to make your middlewares and handlers.
executing command:

  1. $ ./create.sh handler mio

it will produce a plugins/handlers/mio.go file with the structure needed to use it in the server, as the same of

  1. $ ./create.sh middleware mio

that will create the plugins/middlewares/mio.go file.

Building

once you have finish configurations and created the handlers/middlewares plugins, in shell run the command:

  1. $ make build

if you want to remove all compiled files, run:

  1. $ make clean

Test

the project have a test that work for standard configuration and plugin, you need to edit this if you want to test your own implementation

TODO

Example

  • create a ipfilter middleware
  • create basic auth middleware
  • create a “only-admin-access” middleware

    Test

  • routing configuration test: searh for duplicated or wrong path, search for required plugins
  • performance test

    Desired features

  • server config to REDIRECT HTTP TO HTTPS
  • middleware: CLIENT AUTHENTICATION
  • server config to enable HTTPS: use crypto/tls package with ability to rotate TLS session ticket keys by default
  • JWT API auth for javascript frontend framework like angular
  • csrf token for request validation
ft_authoradmin  ft_create_time2019-03-30 21:41
 ft_update_time2019-03-30 21:57