Request filters

How to register web request filters to read custom request headers and how to relay that information to your nodes.

RESTCONF HTTP request access

This is useful for custom authorization or general need to access the HTTP request information

Steps:

1.) Register a request restconf.RequestFilter with RESTCONF restconf.Server instance

2.) Filter returns a context.Context that contains any custom data that you might extract from the HTTP request like HTTP header information, URL parameters or certificate information.

3.) Values from that context.Context will be made available to all your node.Node implementations

Example Code:

package demo

import (
	"context"
	"io"
	"net/http"
	"strings"
	"testing"

	"github.com/freeconf/restconf"
	"github.com/freeconf/restconf/device"
	"github.com/freeconf/yang/fc"
	"github.com/freeconf/yang/node"
	"github.com/freeconf/yang/nodeutil"
	"github.com/freeconf/yang/parser"
	"github.com/freeconf/yang/source"
	"github.com/freeconf/yang/val"
)

type App struct {
}

var module = `module x {
	namespace "freeconf.org";
	prefix "x";

	leaf messageFromRequest {
		config false;
		type string;
	}
}
`

type testContextKey string

const contextKey = testContextKey("requestKey")

func manageApp(a *App) node.Node {
	return &nodeutil.Basic{
		OnField: func(r node.FieldRequest, hnd *node.ValueHandle) error {
			switch r.Meta.Ident() {
			case "messageFromRequest":
				msg := r.Selection.Context.Value(contextKey).(string)
				hnd.Val = val.String(msg)
			}
			return nil
		},
	}
}

func startServer() *restconf.Server {
	ypath := source.Any(restconf.InternalYPath, source.Dir("../yang"))
	m, err := parser.LoadModuleFromString(ypath, module)
	if err != nil {
		panic(err)
	}
	app := &App{}
	b := node.NewBrowser(m, manageApp(app))
	d := device.New(ypath)
	d.AddBrowser(b)
	s := restconf.NewServer(d)

	// Register a filter to peek at request.  From here you can look at:
	//  * headers
	//  * certs
	//  * url params
	grabHeader := func(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, error) {
		msg := r.Header.Get("X-MESSAGE")

		// stuff your data into the context and it will be available to all node
		// navigation associated with this request.
		ctx = context.WithValue(ctx, contextKey, msg)
		return ctx, nil
	}
	s.Filters = append(s.Filters, grabHeader)

	d.ApplyStartupConfig(strings.NewReader(`{
		"fc-restconf" : {
			"web" : {
				"port" : ":9999"
			}
		}
	}
	`))

	return s
}

func TestRequestAccess(t *testing.T) {
	startServer()

	t.Run("request", func(t *testing.T) {

		req, err := http.NewRequest("GET", "http://localhost:9999/restconf/data/x:", nil)
		fc.AssertEqual(t, nil, err)
		req.Header.Add("X-MESSAGE", "hi")
		c := &http.Client{}
		resp, err := c.Do(req)
		fc.AssertEqual(t, nil, err)
		fc.AssertEqual(t, 200, resp.StatusCode)
		actual, err := io.ReadAll(resp.Body)
		fc.AssertEqual(t, nil, err)
		fc.AssertEqual(t, `{"messageFromRequest":"hi"}`, string(actual))
	})
}