negasus / haproxy-spoe-go

Haproxy SPOE (Stream Processing Offload Engine) Agent Golang implementation
MIT License
35 stars 12 forks source link
go golang haproxy middleware spoa spoe

Haproxy SPOE Golang Agent Library Go Report Card

Terms from Haproxy SPOE specification

* SPOE : Stream Processing Offload Engine.

    A SPOE is a filter talking to servers managed ba a SPOA to offload the
    stream processing. An engine is attached to a proxy. A proxy can have
    several engines. Each engine is linked to an agent and only one.

* SPOA : Stream Processing Offload Agent.

    A SPOA is a service that will receive info from a SPOE to offload the
    stream processing. An agent manages several servers. It uses a backend to
    reference all of them. By extension, these servers can also be called
    agents.

* SPOP : Stream Processing Offload Protocol, used by SPOEs to talk to SPOA
         servers.

    This protocol is used by engines to talk to agents. It is an in-house
    binary protocol described in this documentation.

This library implements SPOA for Golang applications

Install

go get -u github.com/negasus/haproxy-spoe-go

Example

Example from Section 2.5 SPOE specification describes a simple IP reputation service.

Here is a simple but complete example that sends client-ip address to a ip reputation service. This service can set the variable "ip_score" which is an integer between 0 and 100, indicating its reputation (100 means totally safe and 0 a blacklisted IP with no doubt).

Golang backend application for this example

package main

import (
    "github.com/negasus/haproxy-spoe-go/action"
    "github.com/negasus/haproxy-spoe-go/agent"
    "github.com/negasus/haproxy-spoe-go/request"
        "github.com/negasus/haproxy-spoe-go/logger"
    "log"
    "math/rand"
    "net"
    "os"
)

func main() {

    log.Print("listen 3000")

    listener, err := net.Listen("tcp4", "127.0.0.1:3000")
    if err != nil {
        log.Printf("error create listener, %v", err)
        os.Exit(1)
    }
    defer listener.Close()

    a := agent.New(handler, logger.NewDefaultLog())

    if err := a.Serve(listener); err != nil {
        log.Printf("error agent serve: %+v\n", err)
    }
}

func handler(req *request.Request) {

    log.Printf("handle request EngineID: '%s', StreamID: '%d', FrameID: '%d' with %d messages\n", req.EngineID, req.StreamID, req.FrameID, req.Messages.Len())

    messageName := "get-ip-reputation"

    mes, err := req.Messages.GetByName(messageName)
    if err != nil {
        log.Printf("message %s not found: %v", messageName, err)
        return
    }

    ipValue, ok := mes.KV.Get("ip")
    if !ok {
        log.Printf("var 'ip' not found in message")
        return
    }

    ip, ok := ipValue.(net.IP)
    if !ok {
        log.Printf("var 'ip' has wrong type. expect IP addr")
        return
    }

    ipScore := rand.Intn(100)

    log.Printf("IP: %s, send score '%d'", ip.String(), ipScore)

    req.Actions.SetVar(action.ScopeSession, "ip_score", ipScore)
}

API

Messages

Getting request data is possible through Request.Messages

Len() int

Returns count of messages in request

count := request.Messages.Len()

GetByName(name string) (*Message, error)

Returns message by name and error if not exists

mes, err := request.Messages.GetByName("get-ip-reputation")

GetByIndex(idx int) (*Message, error)

Returns message by index and error if not exists

mes, err := request.Messages.GetByIndex(0)

Message

Represents one message, sent by Haproxy

Message has two fields

KV contains key-value data of message

Actions

Actions is used for sending a response to Haproxy

SetVar(scope Scope, name string, value interface{})

Set variable with name to value in specific scope (see bellow)

request.Actions.SetVar(action.ScopeSession, "ip_score", 10)

UnsetVar(scope Scope, name string)

Unset variable with name in specific scope

request.Actions.UnsetVar(action.ScopeSession, "ip_score")

Actions Scopes

KV (key-value)

Contains message key-value data sent by Haproxy

Get(key string) (interface{}, bool)

Returns value by name. If key doesn't exist, last returned value will be set to False

ipValue, ok := message.KV.Get("ip")