diff --git a/_examples/shielding/fastly.toml b/_examples/shielding/fastly.toml new file mode 100644 index 0000000..a90b260 --- /dev/null +++ b/_examples/shielding/fastly.toml @@ -0,0 +1,13 @@ +# This file describes a Fastly Compute package. To learn more visit: +# https://developer.fastly.com/reference/fastly-toml/ + +authors = ["oss@fastly.com"] +description = "" +language = "go" +manifest_version = 2 +name = "shielding" + +[local_server.shielding_sites] +"pdx-or-us" = "Local" +"bfi-wa-us".unencrypted = "http://localhost" +"bfi-wa-us".encrypted = "https://localhost" diff --git a/_examples/shielding/main.go b/_examples/shielding/main.go new file mode 100644 index 0000000..07a1242 --- /dev/null +++ b/_examples/shielding/main.go @@ -0,0 +1,25 @@ +// Copyright 2022 Fastly, Inc. + +package main + +import ( + "context" + "fmt" + + "github.com/fastly/compute-sdk-go/fsthttp" + "github.com/fastly/compute-sdk-go/shielding" +) + +func main() { + fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { + name := r.URL.Query().Get("shield") + + shield, err := shielding.ShieldFromName(name) + if err != nil { + fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) + return + } + + fmt.Fprintf(w, "Shield Name=%v, RunningOn=%v\n", shield.Name(), shield.IsRunningOn()) + }) +} diff --git a/integration_tests/shielding/fastly.toml b/integration_tests/shielding/fastly.toml new file mode 100644 index 0000000..fd8ad11 --- /dev/null +++ b/integration_tests/shielding/fastly.toml @@ -0,0 +1,20 @@ +# This file describes a Fastly Compute package. To learn more visit: +# https://developer.fastly.com/reference/fastly-toml/ + +authors = ["oss@fastly.com"] +description = "" +language = "other" +manifest_version = 2 +name = "hello_world" +service_id = "" + +[local_server] + +[local_server.shielding_sites] +"pdx-or-us" = "Local" +"bfi-wa-us".unencrypted = "http://localhost" +"bfi-wa-us".encrypted = "https://localhost" + +[local_server.backends] +[local_server.backends.TheOrigin] + url = "https://compute-sdk-test-backend.edgecompute.app/" diff --git a/integration_tests/shielding/main_test.go b/integration_tests/shielding/main_test.go new file mode 100644 index 0000000..ade2421 --- /dev/null +++ b/integration_tests/shielding/main_test.go @@ -0,0 +1,56 @@ +//go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls + +// Copyright 2022 Fastly, Inc. + +package main + +import ( + "context" + "fmt" + "testing" + + "github.com/fastly/compute-sdk-go/fsthttp" + "github.com/fastly/compute-sdk-go/fsttest" + "github.com/fastly/compute-sdk-go/shielding" +) + +func TestShielding(t *testing.T) { + handler := func(_ context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { + name := r.URL.Query().Get("shield") + + shield, err := shielding.ShieldFromName(name) + if err != nil { + fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) + return + } + + fmt.Fprintf(w, "Name=%v RunningOn=%v", shield.Name(), shield.IsRunningOn()) + } + + var tests = []struct { + shield, want string + }{ + {"bfi-wa-us", "Name=bfi-wa-us RunningOn=false"}, + {"pdx-or-us", "Name=pdx-or-us RunningOn=true"}, + } + + for _, tt := range tests { + + r, err := fsthttp.NewRequest("GET", "/?shield="+tt.shield, nil) + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + w := fsttest.NewRecorder() + + handler(context.Background(), w, r) + + if got, want := w.Code, fsthttp.StatusOK; got != want { + t.Errorf("Code = %d, want %d", got, want) + } + + if got, want := w.Body.String(), tt.want; got != want { + t.Errorf("Body = %q, want %q", got, want) + } + + } +} diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index 0e1d515..8cb6677 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -727,3 +727,11 @@ func HTTPCacheGetSurrogateKeys(h *HTTPCacheHandle) (string, error) { func HTTPCacheGetVaryRule(h *HTTPCacheHandle) (string, error) { return "", fmt.Errorf("not implemented") } + +func ShieldingShieldInfo(name string) (*ShieldInfo, error) { + return nil, fmt.Errorf("not implemented") +} + +func ShieldingBackendForShield(name string, opts *ShieldingBackendOptions) (string, error) { + return "", fmt.Errorf("not implemented") +} diff --git a/internal/abi/fastly/shielding_guest.go b/internal/abi/fastly/shielding_guest.go new file mode 100644 index 0000000..9103242 --- /dev/null +++ b/internal/abi/fastly/shielding_guest.go @@ -0,0 +1,116 @@ +//go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls + +package fastly + +import ( + "bytes" + + "github.com/fastly/compute-sdk-go/internal/abi/prim" +) + +// (module $fastly_shielding + +// witx: +// +// (@interface func (export "shield_info") +// (param $name string) +// (param $info_block (@witx pointer (@witx char8))) +// (param $info_block_max_len (@witx usize)) +// (result $err (expected $num_bytes (error $fastly_status))) +// ) +// +//go:wasmimport fastly_shielding shield_info +//go:noescape +func fastlyShieldingShieldingInfo( + name prim.Pointer[prim.U8], nameLen prim.Usize, + bufPtr prim.Pointer[prim.Char8], bufLen prim.Usize, + bufLenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func ShieldingShieldInfo(name string) (*ShieldInfo, error) { + + n := prim.NewReadBufferFromString(name).Wstring() + buf := prim.NewWriteBuffer(DefaultMediumBufLen) + + if err := fastlyShieldingShieldingInfo( + n.Data, n.Len, + prim.ToPointer(buf.Char8Pointer()), buf.Cap(), + prim.ToPointer(buf.NPointer()), + ).toError(); err != nil { + return nil, err + } + + bufb := buf.AsBytes() + + if len(bufb) == 0 { + return nil, FastlyStatusBadAlign.toError() + } + + // block format: + // 0x1 (running on shield) + // 0x0 0x0 // no targets, but still have field separators) + // OR + // 0x0 (not running on shield) + // 0x0 + // 0x0 + + var info ShieldInfo + info.me = bufb[0] == 1 + + if !info.me { + // strip first null byte and extract targets + vals := bytes.Split(bufb[1:], []byte{0}) + + // ensure we got what we expected + if len(vals) != 3 { + return nil, FastlyStatusBadAlign.toError() + } + + info.target = string(vals[0]) + info.sslTarget = string(vals[1]) + } + + return &info, nil +} + +// witx: +// +// (@interface func (export "backend_for_shield") +// +// (param $shield_name string) +// (param $backend_config_mask $shield_backend_options) +// (param $backend_configuration (@witx pointer $shield_backend_config)) +// (param $backend_name_out (@witx pointer (@witx char8))) +// (param $backend_name_max_len (@witx usize)) +// (result $err (expected $num_bytes (error $fastly_status))) +// ) +// +//go:wasmimport fastly_shielding backend_for_shield +//go:noescape +func fastlyShieldingBackendForShield( + name prim.Pointer[prim.Char8], nameLen prim.Usize, + mask shieldingBackendOptionsMask, + opts prim.Pointer[shieldingBackendOptions], + bufPtr prim.Pointer[prim.Char8], bufLen prim.Usize, + bufLenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func ShieldingBackendForShield(name string, opts *ShieldingBackendOptions) (backend string, err error) { + + n := prim.NewReadBufferFromString(name) + + buf := prim.NewWriteBuffer(DefaultMediumBufLen) + + if err := fastlyShieldingBackendForShield( + prim.ToPointer(n.Char8Pointer()), n.Len(), + opts.mask, prim.ToPointer(&opts.opts), + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ).toError(); err != nil { + return "", err + + } + + return buf.ToString(), nil +} diff --git a/internal/abi/fastly/types.go b/internal/abi/fastly/types.go index 49bbe5e..5a9f253 100644 --- a/internal/abi/fastly/types.go +++ b/internal/abi/fastly/types.go @@ -1623,3 +1623,49 @@ const ( httpCacheWriteOptionsFlagLength httpCacheWriteOptionsMask = 1 << 5 httpCacheWriteOptionsFlagSensitiveData httpCacheWriteOptionsMask = 1 << 6 ) + +// shielding.witx + +type shieldingBackendOptionsMask prim.U32 + +const ( + shieldingBackendOptionsFlagReserved shieldingBackendOptionsMask = 1 << 0 + shieldingBackendOptionsFlagUseCacheKey shieldingBackendOptionsMask = 1 << 1 +) + +type shieldingBackendOptions struct { + // A list of surrogate keys that may be used to purge this response. + // + // The format is a string containing [valid surrogate + // keys](https://www.fastly.com/documentation/reference/http/http-headers/Surrogate-Key/) + // separated by spaces. + // + // If this field is not set, no surrogate keys will be associated with the response. This + // means that the response cannot be purged except via a purge-all operation. + cacheKeyPtr prim.Pointer[prim.Char8] + cacheKeyLen prim.Usize +} + +type ShieldingBackendOptions struct { + mask shieldingBackendOptionsMask + opts shieldingBackendOptions +} + +func (s *ShieldingBackendOptions) CacheKey(key string) { + s.mask |= shieldingBackendOptionsFlagUseCacheKey + buf := prim.NewReadBufferFromString(key) + s.opts.cacheKeyPtr = prim.ToPointer(buf.Char8Pointer()) + s.opts.cacheKeyLen = buf.Len() +} + +type ShieldInfo struct { + me bool + target string + sslTarget string +} + +func (s *ShieldInfo) RunningOn() bool { return s.me } + +func (s *ShieldInfo) Target() string { return s.target } + +func (s *ShieldInfo) SSLTarget() string { return s.sslTarget } diff --git a/shielding/doc.go b/shielding/doc.go new file mode 100644 index 0000000..76b9f89 --- /dev/null +++ b/shielding/doc.go @@ -0,0 +1,4 @@ +// Package shielding provides support for shielding for Compute services. Refer to +// https://www.fastly.com/documentation/guides/concepts/shielding/ for more information. + +package shielding diff --git a/shielding/shielding.go b/shielding/shielding.go new file mode 100644 index 0000000..ac95eec --- /dev/null +++ b/shielding/shielding.go @@ -0,0 +1,40 @@ +package shielding + +import ( + "github.com/fastly/compute-sdk-go/internal/abi/fastly" +) + +// Shield is a shielding site withing Fastly. +type Shield struct { + name string + runningOn bool +} + +// ShieldFromName returns information about a particular shield site. +func ShieldFromName(n string) (*Shield, error) { + info, err := fastly.ShieldingShieldInfo(n) + if err != nil { + return nil, err + } + + return &Shield{ + name: n, + runningOn: info.RunningOn(), + }, nil +} + +// Name returns the name of the shield site. +func (s *Shield) Name() string { return s.name } + +// IsRunningOn returns whether the Compute node is currently in the shielding site. +func (s *Shield) IsRunningOn() bool { return s.runningOn } + +// BackendOptions +type BackendOptions struct { +} + +// Backend returns a named backend for use with the fsthttp package. +func (s *Shield) Backend(opts *BackendOptions) (string, error) { + var abiOpts fastly.ShieldingBackendOptions + return fastly.ShieldingBackendForShield(s.name, &abiOpts) +}