Login 24/7 Support Community tyk.io

Golang plugins

This guide will help you to understand the idea behind Golang plugins in less than 5-10 minutes.

We will look at:

  • Some simple examples to see how to customise and extend Tyk with Golang plugins
  • Look at some code to get some ideas as to where to go next
  • Follow the process of how to build a Golang plugin and get it loaded by Tyk

Golang plugins are a very flexible and powerful way to extend the functionality of Tyk. It is based on utilising the native Golang plugins API (see https://golang.org/pkg/plugin for more details).

Every HTTP request (to your API, protected and managed by Tyk) gets passed through a chain of built-in middleware inside Tyk. This middleware performs tasks like authentication, rate limiting, white or black listing and many others - it depends on the particular API specification. In other words the chain of middleware is specific to an API and gets created at API re-load time. Golang plugins allow developers to create custom middleware in Golang and then add them to the chain of middleware. So when Tyk performs an API re-load it also loads the custom middleware and “injects” them into a chain to be called at different stages of the HTTP request life cycle.

It’s also possible to access the API definition data structure from within a plugin, this functionality is described in Accessing API definition from a plugin.

Table Of Contents

Plugin development flow

Initialize plugin

Create a new folder, and run the following command to initialize your plugin:

go mod init tyk_plugin
go mod edit -replace github.com/jensneuse/graphql-go-tools=github.com/TykTechnologies/graphql-go-tools@140640759f4b
go get github.com/TykTechnologies/tyk@bda54b0f790c9bc11297c96fe8f2a5b370f39e05

go mod init tyk_plugin
go mod edit -replace github.com/jensneuse/graphql-go-tools=github.com/TykTechnologies/graphql-go-tools@140640759f4b
go get github.com/TykTechnologies/tyk@b9315433e9477e2621c54c530327eb75f7e81ef5


Pick required version in the top of the side menu

This command will create go.mod file inside your folder, and will ensure that plugin depends on the right Tyk version.

Write the plugin

Let’s create a plugin with very basic functionality:

  • We will add a custom header "Foo: Bar" to a request.
  • This needs to happen right before the request is passed to an upstream target behind the Tyk API Gateway

Create a file plugin.go with the following content:

package main

import (
  "net/http"
)

// AddFooBarHeader adds custom "Foo: Bar" header to the request
func AddFooBarHeader(rw http.ResponseWriter, r *http.Request) {
  r.Header.Add("Foo", "Bar")
}

func main() {}

We see that the Golang plugin:

  • Is a Golang project with a main package
  • Has an empty func main()
  • Has one exported func AddFooBarHeader which must have the same method signature as type HandlerFunc func(ResponseWriter, *Request) from the standard "net/http" Golang package

Sync dependencies

go mod tidy
go mod vendor

Running command above will download required dependencies from the internet, and ensure that all plugin dependencies are resolved correctly. All dependencies are saved to the vendor folder.

Note

Run this command on initial plugin initialization, and every time you add a new third party library in your code.

Building the plugin

A specific of Golang plugins is that they need to be built using exactly the same Tyk binary as the one to be installed. In order to make it work, we provide a special Docker image, which we internally use for building our official binaries too.

docker run --rm -v `pwd`:/plugin-source tykio/tyk-plugin-compiler:v3.2.2 plugin.so

docker run --rm -v `pwd`:/plugin-source tykio/tyk-plugin-compiler:v3.2.1 plugin.so


Pick required version in the top of the side menu

Explanation to the command above:

  1. Mount your plugin directory to the /plugin-source image location
  2. Make sure to specify your Tyk version via a Docker tag. For example v3.2.1 .
  3. The final argument is the plugin name. For the example plugin.so

Note

When upgrading your Tyk Installation you need to re-compile your plugin with new version.

Loading the plugin

For the development purpose we going to load plugin from local files. For the production you can use bundles to deploy plugins to multiple gateways.

In API definition find custom_middleware section and make it look similar to snippet below. Tyk Dashboard users should use RAW API Editor to access this section.

"custom_middleware": {
  "pre": [],
  "post_key_auth": [],
  "auth_check": {},
  "post": [
    {
      "name": "AddFooBarHeader",
      "path": "<path>/plugin.so"
    }
  ],
  "driver": "goplugin"
}

Here we have:

  • "driver" - Set this to goplugin (no value created for this plugin) which says to Tyk that this custom middleware is a Golang native plugin.
  • "post" - This is the hook name. We use middleware with hook type post because we want this custom middleware to process the request right before it is passed to the upstream target (we will look at other types later).
  • post.name - is your function name from the go plugin project.
  • post.path - is the full or relative (to the Tyk binary) path to .so file with plugin implementation (make sure Tyk has read access to this file)

Also, let’s set fields "use_keyless": true and "target_url": "http://httpbin.org/" - for testing purposes (we need to see what request arrives to our upstream target and httpbin.org is a perfect fit for that).

The API needs to be reloaded after that change (this happens automatically when you save the updated API in the Dashboard).

Now your API with its Golang plugin is ready to process traffic:

curl http://localhost:8181/my_api_name/get   

{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip", 
    "Foo": "Bar", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.54.0"
  }, 
  "url": "https://httpbin.org/get"
} 

We see that the upstream target has received the header "Foo": "Bar" which was added by our custom middleware implemented as a native Golang plugin in Tyk.

Updating the plugin

Loading an updated version of your plugin require one of the following actions:

  • An API reload with a NEW path or file name of your .so file with the plugin. You will need to update the API spec section "custom_middleware", specifying a new value for the "path" field of the plugin you need to reload.
  • Tyk main process reload. This will force a reload of all Golang plugins for all APIs.

If a plugin is loaded as a bundle and you need to update it you will need to update your API spec with new .zip file name in the "custom_middleware_bundle" field. Make sure the new .zip file is uploaded and available via the bundle HTTP endpoint before you update your API spec.

Plugin types

All types of custom middleware hooks are supported by Tyk Golang plugins. They represent different request stages where Golang plugins can be added as part of the middleware chain. Let’s recap the meaning of all these types:

  • "pre" - contains array of middlewares to be run before any others (i.e. before authentication).
  • "auth_check" - contains only one middleware info, his middleware performs custom authentication and adds API key session info into request context.
  • "post_auth_check" - contains array of middlewares to be run after authentication, at this point we have authenticated session API key for the given key (in request context) so we can perform any extra checks.
  • "post" - contains array of middlewares to be run at the very end of middleware chain, at this point Tyk is about to request a round-trip to the upstream target.
  • "response" - run only at the point the response has returned from a service upstream of the API Gateway. NOTE: The method signature for Go repsonse plugins varies from the other hook types

Custom Auth Hook

"auth_check" can be used only if both fields in the Tyk API definition are set: 1."use_keyless": false 2."use_go_plugin_auth": true

Post Authentication Hook

"post_auth_check" hook can only be use when:

  1. When the API is protected, the API spec has field set as "use_keyless": false
  2. With any auth method specified in API spec

Note

These fields are populated automatically with the correct values when you change the authentication method for the API in the Tyk Dashboard.

Examples

Sending HTTP-response from Tyk Golang plugin

It is possible to send a response from the Golang plugin custom middleware. So in the case that the HTTP response was sent:

  • The HTTP request processing is stopped and other middleware in the chain won’t be used.
  • The HTTP request round-trip to the upstream target won’t happen
  • Analytics records will still be created and sent to the analytics processing flow.

Let’s look at an example of how to send a HTTP response from the Tyk Golang plugin. Imagine that we need middleware which would send JSON with the current time if the request contains the parameter get_time=1 in the request query string:

package main

import (
  "encoding/json"
  "net/http"
  "time"
)

func SendCurrentTime(rw http.ResponseWriter, r *http.Request) {
  // check if we don't need to send reply
  if r.URL.Query().Get("get_time") != "1" {
    // allow request to be processed and sent to upstream
        return
  }

  // prepare data to send
  replyData := map[string]interface{}{
    "current_time": time.Now(),
  }

  jsonData, err := json.Marshal(replyData)
  if err != nil {
    rw.WriteHeader(http.StatusInternalServerError)
    return
  }

  // send HTTP response from Golang plugin
  rw.Header().Set("Content-Type", "application/json")
  rw.WriteHeader(http.StatusOK)
  rw.Write(jsonData)
}

func main() {}

Let’s build the plugin by running this command in the plugin project folder:

go build -buildmode=plugin -o /tmp/SendCurrentTime.so

Then let’s edit the API spec to use this custom middleware:

"custom_middleware": {
  "pre": [
    {
       "name": "SendCurrentTime",
       "path": "/tmp/SendCurrentTime.so"
    }
  ],
  "post_key_auth": [],
  "auth_check": {},
  "post": [],
  "driver": "goplugin"
}

Let’s check that we still perform a round trip to the upstream target if the request query string parameter get_time is not set:

curl http://localhost:8181/my_api_name/get
                 
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.54.0"
  }, 
  "url": "https://httpbin.org/get"
}

Now let’s check if our Golang plugin sends a HTTP 200 response (with JSON containing current time) when we set get_time=1 query string parameter:

curl -v http://localhost:8181/my_api_name/get?get_time=1
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8181 (#0)
> GET /my_api_name/get?get_time=1 HTTP/1.1
> Host: localhost:8181
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Wed, 11 Sep 2019 03:44:10 GMT
< Content-Length: 51
< 
* Connection #0 to host localhost left intact
{"current_time":"2019-09-11T23:44:10.040878-04:00"}

Here we see that:

  • We’ve got a HTTP 200 response code.
  • The response body has JSON payload with current time.
  • The upstream target was not reached. Our Tyk Golang plugin served this request and stopped processing after the response was sent.

Authentication with a Golang plugin

You can implement your own authentication method, using a Golang plugin and custom "auth_check" middleware. Ensure you set the two fields in Post Authentication Hook:

Let’s have a look at the code example. Imagine we need to implement a very trivial authentication method when only one key is supported (in the real world you would want to store your keys in some storage or have some more complex logic).

package main

import (
  "net/http"

  "github.com/TykTechnologies/tyk/ctx"
  "github.com/TykTechnologies/tyk/headers"
  "github.com/TykTechnologies/tyk/user"
)

func getSessionByKey(key string) *user.SessionState {
  // here goes our logic to check if passed API key is valid and appropriate key session can be retrieved

  // perform auth (only one token "abc" is allowed)
  if key != "abc" {
    return nil
  }

  // return session
  return &user.SessionState{
    OrgID: "default",
    Alias: "abc-session",
  }
}

func MyPluginAuthCheck(rw http.ResponseWriter, r *http.Request) {
  // try to get session by API key
  key := r.Header.Get(headers.Authorization)
  session := getSessionByKey(key)
  if session == nil {
    // auth failed, reply with 403
    rw.WriteHeader(http.StatusForbidden)
    return
  }

  // auth was successful, add session and key to request's context so other middlewares can use it
  ctx.SetSession(r, session, key, true)
}

func main() {}

A couple of notes about this code:

  • The package "github.com/TykTechnologies/tyk/ctx" is used to set a session in the request context - this is something "auth_check"-type custom middleware is responsible for.
  • The package "github.com/TykTechnologies/tyk/user" is used to operate with Tyk’s key session structure.
  • Our Golang plugin sends a 403 HTTP response if authentication fails.
  • Our Golang plugin just adds a session to the request context and returns if authentication was successful.

Let’s build the plugin by running the following command in the folder containing your plugin project:

go build -buildmode=plugin -o /tmp/MyPluginAuthCheck.so

Now let’s check if our custom authentication works as expected (only one key "abc" should work).

Authentication will fail with the wrong API key:

 curl -v -H "Authorization: xyz" http://localhost:8181/my_api_name/get
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8181 (#0)
> GET /my_api_name/get HTTP/1.1
> Host: localhost:8181
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: xyz
> 
< HTTP/1.1 403 Forbidden
< Date: Wed, 11 Sep 2019 04:31:34 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

Here we see that our custom middleware replied with a 403 response and request processing was stopped at this point.

Authentication successful with the right API key:

curl -v -H "Authorization: abc" http://localhost:8181/my_api_name/get
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8181 (#0)
> GET /my_api_name/get HTTP/1.1
> Host: localhost:8181
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: abc
> 
< HTTP/1.1 200 OK
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Origin: *
< Content-Type: application/json
< Date: Wed, 11 Sep 2019 04:31:39 GMT
< Referrer-Policy: no-referrer-when-downgrade
< Server: nginx
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-Ratelimit-Limit: 0
< X-Ratelimit-Remaining: 0
< X-Ratelimit-Reset: 0
< X-Xss-Protection: 1; mode=block
< Content-Length: 257
< 
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip", 
    "Authorization": "abc", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.54.0"
  }, 
  "url": "https://httpbin.org/get"
}
* Connection #0 to host localhost left intact

Here we see that our custom middleware successfully authenticated the request and we received a reply from the upstream target.

Logging from a Golang plugin

Your Golang plugin can write log entries as part of Tyk’s logging system.

To do so you just need to import the package "github.com/TykTechnologies/tyk/log" and use the exported public method Get():

package main

import (
  "net/http"

  "github.com/TykTechnologies/tyk/log"
)

var logger = log.Get()

// AddFooBarHeader adds custom "Foo: Bar" header to the request
func AddFooBarHeader(rw http.ResponseWriter, r *http.Request) {
  logger.Info("Processing HTTP request in Golang plugin!!")
  r.Header.Add("Foo", "Bar")
}

func main() {}

Monitoring instrumentation for Tyk Golang plugins

All custom middleware implemented as Golang plugins support Tyk’s current built in instrumentation.

The format for an event name with metadata is: "GoPluginMiddleware:" + Path + ":" + SymbolName, e.g., for our example the event name will be:

"GoPluginMiddleware:/tmp/AddFooBarHeader.so:AddFooBarHeader"

The format for metric with execution time (in nanoseconds) will have the same format but with the .exec_time suffix:

"GoPluginMiddleware:/tmp/AddFooBarHeader.so:AddFooBarHeader.exec_time"

Accessing internal state of a Tyk Golang plugin

A Golang plugin can be treated as normal Golang package but:

  • The package name is always "main" and this package cannot be imported.
  • This package loads at run-time by Tyk and loads after all other Golang packages.
  • This package has to have an empty func main() {}.

A Golang plugin as a package can have func init() and it gets called only once (when Tyk loads this plugin for the first time for an API).

It is possible to create structures or open connections to 3d party services/storage and then share them within every call and the export the function in your Golang plugin.

For example, here is an example of a Tyk Golang plugin with a simple hit-counter:

package main

import (
  "encoding/json"
  "net/http"
  "sync"

  "github.com/TykTechnologies/tyk/ctx"
  "github.com/TykTechnologies/tyk/log"
  "github.com/TykTechnologies/tyk/user"
)

var logger = log.Get()

// plugin exported functionality
func MyProcessRequest(rw http.ResponseWriter, r *http.Request) {
  endPoint := r.Method + " " + r.URL.Path
  logger.Info("Custom middleware, new hit:", endPoint)

  hitCounter := recordHit(endPoint)
  logger.Debug("New hit counter value:", hitCounter)

  if hitCounter > 100 {
    logger.Warning("Hit counter to high")
  }

  reply := myReply{
    Session:    ctx.GetSession(r),
    Endpoint:   endPoint,
    HitCounter: hitCounter,
  }

  jsonData, err := json.Marshal(reply)
  if err != nil {
    logger.Error(err.Error())
    rw.WriteHeader(http.StatusInternalServerError)
    return
  }

  rw.Header().Set("Content-Type", "application/json")
  rw.WriteHeader(http.StatusOK)
  rw.Write(jsonData)
}

// called once plugin is loaded, this is where we put all initialization work for plugin
// i.e. setting exported functions, setting up connection pool to storage and etc.
func init() {
  hitCounter = make(map[string]uint64)
}

// plugin internal state and implementation
var (
  hitCounter   map[string]uint64
  hitCounterMu sync.Mutex
)

func recordHit(endpoint string) uint64 {
  hitCounterMu.Lock()
  defer hitCounterMu.Unlock()
  hitCounter[endpoint]++
  return hitCounter[endpoint]
}

type myReply struct {
  Session    *user.SessionState `json:"session"`
  Endpoint   string             `json:"endpoint"`
  HitCounter uint64             `json:"hit_counter"`
}

func main() {}

Here we see how the internal state of the Golang plugin is used by the exported function MyProcessRequest (the one we set in the API spec in the "custom_middleware" section). The map hitCounter is used to send internal state and count hits to different endpoints. Then our exported Golang plugin function sends a HTTP reply with endpoint hit statistics.

Loading a Tyk Golang plugin from a bundle

So far we have loaded Golang plugins only directly from file system. However when you have multiple gateway instances, you need a more dynamic way to load plugins. Tyk offer bundle instrumentation Plugin Bundles. Using bundle command creates archive with your plugin, which you can deploy to HTTP-server (or AWS S3) and then your plugins will be fetched and loaded from that HTTP endpoint.

You will need to set in tyk.conf these two fields:

  • "enable_bundle_downloader": true - this enables plugin bundles downloader
  • "bundle_base_url": "http://mybundles:8000/abc" - this specifies the base URL with HTTP server where you place your bundles with Golang plugins (this endpoint has to be reachable from node with Tyk running)

Also, you will need to specify the following field in your API spec:

"custom_middleware_bundle" - here you place your filename with bundle (.zip archive) to be fetched from the HTTP endpoint you specified in your tyk.conf parameter "bundle_base_url"

So, your API spec will have this field:

"custom_middleware_bundle": "FooBarBundle.zip"

Let’s look at FooBarBundle.zip contents. It is just a ZIP-archive with two files archived inside:

  • AddFooBarHeader.so - this is our Golang plugin
  • manifest.json - this is special file with meta information used by Tyk’s bundle loader

The contents of manifest.json:

{
  "file_list": [
    "AddFooBarHeader.so"
  ],
  "custom_middleware": {
    "post": [
      {
        "name": "AddFooBarHeader",
        "path": "AddFooBarHeader.so"
      }
    ]
  },
  "driver": "goplugin",
  ...
}

Here we see:

  • field "custom_middleware" with exactly the same structure we used to specify "custom_middleware" in API spec without bundle
  • field "path" in section "post" now contains just a file name without any path. This field specifies .so filename placed in ZIP archive with bundle (remember how we specified "custom_middleware_bundle": "FooBarBundle.zip").

Accessing API definition from a Golang plugin

When Tyk passes a request to your plugin, the API definition is made available as part of the request context. This can be accessed as follows:

package main
import (
  "fmt"
  "net/http"
  "github.com/TykTechnologies/tyk/ctx"
)
func main() {}
func MyPluginFunction(w http.ResponseWriter, r *http.Request) {
  apidef := ctx.GetDefinition(r)
  fmt.Println("API name is", apidef.Name)
}

ctx.GetDefinition returns an APIDefinition object, the Go data structure can be found here

Accessing User session from a Golang plugin

When Tyk passes a request to your plugin, the User Sesssion object is made available as part of the request context. This can be accessed as follows:

package main
import (
  "fmt"
  "net/http"
  "github.com/TykTechnologies/tyk/ctx"
)
func main() {}
func MyPluginFunction(w http.ResponseWriter, r *http.Request) {
  session := ctx.GetSession(r)
  fmt.Println("Developer ID:", session.MetaData["tyk_developer_id"]
  fmt.Println("Developer Email:", session.MetaData["tyk_developer_email"]
}

ctx.GetSession returns an UserSession object, the Go data structure can be found here

Modifying a response

Now you need to instruct Tyk to load this shared library for an API so it will start processing traffic as part of the middleware chain. To do so you will need to edit your API spec using the raw JSON editor in the Tyk Dashboard or directly in the JSON file (in the case of the Open Source edition). This change needs to be done for the “custom_middleware” field and it looks like this:

"custom_middleware": {
  "pre": [],
  "post_key_auth": [],
  "auth_check": {},
  "post": [],
  "response": [
    {
      "name": "MyResponsePlugin",
      "path": "<path>/resplugin.so"
    }
  ],
  "driver": "goplugin"
}

Here you have:

"driver" - Set this to goplugin (no value created for this plugin) which says to Tyk that this custom middleware is a Golang native plugin. "response" - This is the hook name. You use middleware with a hook type response because you want this custom middleware to process the request on its return leg of a round trip. response.name - is your function name from the go plugin project. response.path - is the full or relative (to the Tyk binary) path to .so file with plugin implementation (make sure Tyk has read access to this file) Response plugin method signature

To write a response plugin in Go you need it to have a method signature as in the example below i.e. func MyResponseFunctionName(rw http.ResponseWriter, res *http.Response, req *http.Request). You can then access and modify any part of the request or response. User session and API definition data can be accessed as with other Go plugin hook types.

package main

import (
  "bytes"
  "encoding/json"
  "io/ioutil"
  "net/http"

)

// MyPluginResponse intercepts response from upstream 
func MyPluginResponse(rw http.ResponseWriter, res *http.Response, req *http.Request) {
        // add a header to our response object
  res.Header.Add("X-Response-Added", "resp-added")

        // overwrite our response body
  var buf bytes.Buffer
  buf.Write([]byte(`{"message":"Hi! I'm a response plugin"}`))
  res.Body = ioutil.NopCloser(&buf)

}

func main() {}

Building from Source

If you are building a plugin for a Gateway version compiled from the source, you can use the following command:

go build -buildmode=plugin -o plugin.so

As a result of this build command we get a shared library with the plugin implementation placed at plugin.so.

If your plugin depends on third party libraries, ensure to vendor them, before building. If you are using Go modules, it should be as simple as running go mod vendor command.

Known issues and Limitations

If a dependency that your plugin uses is also used by the gateway, the version used by the gateway will be used in your plugin. This may mask conflicts between transitive dependencies.

The plugin compiler does not support on Ubuntu 16.04 (Xenial Xerus) as it uses glibc 2.23 which is incompatible with our standard build environment. If you absolutely must have go plugin support on Xenial, please write to our support.