├── .github ├── actions │ └── install-tinygo │ │ └── action.yml └── workflows │ ├── build-examples.yml │ ├── integration-tests.yml │ └── validate.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _examples ├── README.md ├── acl │ ├── acls.json │ ├── fastly.toml │ └── main.go ├── add-or-remove-cookies │ ├── fastly.toml │ └── main.go ├── corecache │ ├── fastly.toml │ └── main.go ├── device-detection │ ├── fastly.toml │ └── main.go ├── geodata │ ├── fastly.toml │ └── main.go ├── hello-world │ ├── fastly.toml │ └── main.go ├── http-adapter │ ├── fastly.toml │ └── main.go ├── kvstore │ ├── fastly.toml │ └── main.go ├── limits │ ├── fastly.toml │ └── main.go ├── logging-and-env │ ├── fastly.toml │ └── main.go ├── middlewares │ ├── fastly.toml │ └── main.go ├── multiple-goroutines │ ├── fastly.toml │ └── main.go ├── parallel-requests │ ├── fastly.toml │ └── main.go ├── print-request │ ├── fastly.toml │ └── main.go ├── proxy-request-framing │ ├── fastly.toml │ └── main.go ├── proxy-request │ ├── fastly.toml │ └── main.go ├── secret-store │ ├── fastly.toml │ └── main.go ├── set-cookie │ ├── fastly.toml │ └── main.go ├── set-google-analytics │ ├── fastly.toml │ └── main.go ├── simplecache │ ├── fastly.toml │ └── main.go ├── stream-response │ ├── fastly.toml │ └── main.go └── with-timeout │ ├── fastly.toml │ └── main.go ├── acl └── acl.go ├── cache ├── core │ ├── core.go │ ├── core_test.go │ └── fastly.toml └── simple │ └── simple.go ├── compute ├── compute.go └── doc.go ├── configstore ├── configstore.go ├── configstore_test.go ├── doc.go └── fastly.toml ├── device └── device_detection.go ├── edgedict ├── dictionary.go ├── dictionary_test.go ├── doc.go └── fastly.toml ├── erl ├── erl.go ├── erl_test.go └── fastly.toml ├── fsthttp ├── adapter.go ├── adapter_test.go ├── backend.go ├── cache.go ├── cache_guest.go ├── cache_host.go ├── context.go ├── cookie.go ├── cookie_test.go ├── doc.go ├── fastly.toml ├── handle.go ├── headers.go ├── headers_test.go ├── helpers.go ├── internal.go ├── limits.go ├── methods.go ├── request.go ├── request_test.go ├── response.go ├── setparseuri.go ├── status.go └── transport.go ├── fsttest └── recorder.go ├── geo ├── doc.go ├── geodata.go └── testdata │ ├── geo_1.json │ └── geo_2.json ├── go.mod ├── go.sum ├── integration_tests ├── _version │ ├── fastly.toml │ └── main_test.go ├── acl │ ├── acls.json │ ├── fastly.toml │ └── main_test.go ├── async_select │ ├── fastly.toml │ └── main_test.go ├── byte_repeater │ ├── fastly.toml │ └── main_test.go ├── compute_runtime │ ├── fastly.toml │ └── main_test.go ├── config_store │ ├── configstore.json │ ├── fastly.toml │ └── main_test.go ├── device_detection │ ├── fastly.toml │ └── main_test.go ├── dynamic_backend │ ├── fastly.toml │ └── main_test.go ├── geolocation │ ├── fastly.toml │ └── main_test.go ├── hello_world │ ├── fastly.toml │ └── main_test.go ├── httpcache │ └── main.go ├── internal_debug │ ├── fastly.toml │ └── main_test.go ├── kvstore │ ├── fastly.toml │ └── main_test.go ├── request_downstream │ ├── fastly.toml │ └── main_test.go ├── request_upstream │ ├── fastly.toml │ └── main_test.go ├── secret_store │ ├── fastly.toml │ └── main_test.go ├── status │ ├── fastly.toml │ └── main_test.go └── streaming_close │ ├── fastly.toml │ └── main_test.go ├── internal └── abi │ ├── fastly │ ├── acl_guest.go │ ├── backends_guest.go │ ├── cache_guest.go │ ├── compute_runtime_guest.go │ ├── configstore_guest.go │ ├── device_detection_guest.go │ ├── dictionary_guest.go │ ├── doc.go │ ├── erl_guest.go │ ├── geo_guest.go │ ├── hostcalls_noguest.go │ ├── http_guest.go │ ├── httpcache_guest.go │ ├── init_guest.go │ ├── kvstore_guest.go │ ├── log_guest.go │ ├── secret_store_guest.go │ └── types.go │ ├── prim │ ├── doc.go │ └── prim.go │ └── wasi │ ├── doc.go │ └── types.go ├── kvstore └── kvstore.go ├── objectstore ├── doc.go └── objectstore.go ├── purge └── purge.go ├── rtlog ├── doc.go └── endpoint.go ├── secretstore └── secretstore.go ├── semgrep ├── README.txt ├── edgedict.yml └── objectstore.yml ├── targets ├── fastly-compute-wasi.json └── fastly-compute-wasip1.json └── x ├── doc.go ├── exp └── handoff │ └── handoff.go └── fstctx ├── ctxutil.go └── doc.go /.github/actions/install-tinygo/action.yml: -------------------------------------------------------------------------------- 1 | name: "Install TinyGo" 2 | description: "Installs TinyGo" 3 | inputs: 4 | tinygo-version: 5 | description: "The version of TinyGo to install" 6 | required: true 7 | default: "0.33.0" 8 | runs: 9 | using: "composite" 10 | steps: 11 | - run: | 12 | echo "Install TinyGo ${{ inputs.tinygo-version }}..." 13 | wget --no-verbose https://github.com/tinygo-org/tinygo/releases/download/v${{ inputs.tinygo-version }}/tinygo_${{ inputs.tinygo-version }}_amd64.deb 14 | sudo dpkg -i tinygo_${{ inputs.tinygo-version }}_amd64.deb 15 | echo "/usr/local/tinygo/bin" >> $GITHUB_PATH 16 | shell: "bash" 17 | -------------------------------------------------------------------------------- /.github/workflows/build-examples.yml: -------------------------------------------------------------------------------- 1 | name: Build Examples 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | build-examples: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | # Newest supported configuration 15 | - go-version: '1.23' # pairs with TinyGo 0.33.0 16 | tinygo-version: '0.33.0' 17 | # Oldest supported configuration 18 | - go-version: '1.21' # pairs with TinyGo 0.29.0 19 | tinygo-version: '0.29.0' 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | - uses: ./.github/actions/install-tinygo 27 | with: 28 | tinygo-version: ${{ matrix.tinygo-version }} 29 | 30 | - name: Build examples Go 31 | env: 32 | GOARCH: wasm 33 | GOOS: wasip1 34 | run: | 35 | for i in _examples/*/; do 36 | echo ${GITHUB_WORKSPACE}/$i 37 | cd ${GITHUB_WORKSPACE}/$i && go build -tags fastlyinternaldebug 38 | done 39 | 40 | - name: Build examples TinyGo 41 | run: | 42 | for i in _examples/*/; do 43 | echo ${GITHUB_WORKSPACE}/$i 44 | cd ${GITHUB_WORKSPACE}/$i && tinygo build -target=wasi -tags fastlyinternaldebug 45 | done 46 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | integration-tests: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | # Newest supported configuration 15 | - go-version: "1.23" # pairs with TinyGo 0.33.0+ 16 | tinygo-version: "0.35.0" 17 | tinygo-wasi-target: "wasip1" 18 | - go-version: "1.22" # pairs with TinyGo 0.31.2 19 | tinygo-version: "0.31.2" 20 | tinygo-wasi-target: "wasi" 21 | # Oldest supported configuration 22 | - go-version: "1.21" # pairs with TinyGo 0.29.0 23 | tinygo-version: "0.29.0" 24 | tinygo-wasi-target: "wasi" 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-go@v5 29 | with: 30 | go-version: ${{ matrix.go-version }} 31 | 32 | - uses: ./.github/actions/install-tinygo 33 | with: 34 | tinygo-version: ${{ matrix.tinygo-version }} 35 | 36 | - name: Setup Fastly CLI 37 | uses: fastly/compute-actions/setup@v7 38 | 39 | - name: Install Viceroy 40 | run: make tools/viceroy 41 | 42 | - name: Print dependency versions 43 | run: | 44 | go version 45 | tinygo version 46 | fastly version 47 | viceroy --version 48 | 49 | - name: Tests - Go 50 | run: make test-go 51 | 52 | - name: Integration Tests - Go 53 | run: make test-integration-go 54 | 55 | - name: Tests - TinyGo 56 | # The slightly different TINYGO_TARGET and GO_BUILD_FLAGS for older 57 | # versions of TinyGo can be removed once all versions are consistent. 58 | run: make test-tinygo TINYGO_TARGET=./targets/fastly-compute-${{ matrix.tinygo-wasi-target }}.json GO_BUILD_FLAGS="-tags='fastlyinternaldebug nofastlyhostcalls'" 59 | 60 | - name: Integration Tests - TinyGo 61 | # The slightly different TINYGO_TARGET for older versions of TinyGo can 62 | # be removed once all versions are consistent. 63 | run: make test-integration-tinygo TINYGO_TARGET=./targets/fastly-compute-${{ matrix.tinygo-wasi-target }}.json 64 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | validate: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version-file: go.mod 16 | 17 | - uses: ./.github/actions/install-tinygo 18 | with: 19 | tinygo-version: "0.35.0" 20 | 21 | - name: Print dependency versions 22 | run: | 23 | go version 24 | tinygo version 25 | 26 | - run: go vet ./... 27 | 28 | - name: staticcheck 29 | run: | 30 | go install honnef.co/go/tools/cmd/staticcheck@latest 31 | staticcheck ./... 32 | 33 | - name: nilness 34 | run: | 35 | go install golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@master 36 | nilness ./... 37 | 38 | - name: ineffassign 39 | run: | 40 | go install github.com/gordonklaus/ineffassign@latest 41 | ineffassign ./... 42 | 43 | - name: Install viceroy 44 | run: make tools/viceroy 45 | 46 | - name: Run Tests 47 | run: make test 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _examples/*/bin 2 | _examples/*/pkg 3 | tools/viceroy 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## 1.4.2 (2025-04-09) 4 | 5 | - fsthttp: update internal debug call for http cache refactoring change 6 | 7 | ## 1.4.1 (2025-04-09) 8 | 9 | - packaging: Retract (but do not revert) release 1.4.0 and 1.4.1. Builds will default to using 1.3.3. 10 | 11 | ## 1.4.0 (2025-04-07) 12 | 13 | ### Added 14 | 15 | - fsthttp: make response polling frequency configurable 16 | - fsthttp: add http cache API support 17 | - secretstore: add Plaintext toplevel convenience function 18 | - acl: add ACL hostcalls 19 | 20 | ## 1.3.3 (2024-09-12) 21 | 22 | ### Added 23 | 24 | - kvstore: add ErrTooManyRequests 25 | - fsthttp: add ServerAddr to Request 26 | - fsthttp: add pluggable URL parser 27 | - fsthttp: add TCP and HTTP keepalives configuration for backends 28 | - fsthttp: add RemoteAddr to Response 29 | - fsthttp: add pooling connection configuration for backends 30 | - compute: add GetVCPUMilliseconds 31 | - fsthttp: add client certificate configuration for backends 32 | - fsthttp: add grpc flag for backends 33 | 34 | ### Changed 35 | 36 | - configstore: switch to new configstore hostcalls 37 | 38 | ## 1.3.2 (2024-06-25) 39 | 40 | ### Added 41 | 42 | - configstore: add Store.Has() method 43 | - configstore: add Store.GetBytes() method 44 | - configstore: reduce memory usage with shared buffer 45 | 46 | ### Changed 47 | 48 | - fsthttp: make buffer sizes adaptable 49 | 50 | ## 1.3.1 (2024-04-25) 51 | 52 | ### Added 53 | 54 | - `kvstore`: add Store.Delete method 55 | 56 | ### Changed 57 | 58 | - Update Viceroy requirement to 0.9.6 59 | 60 | ## 1.3.0 (2024-02-21) 61 | 62 | ### Added 63 | 64 | - Add support for edge rate limiting (`erl`) 65 | 66 | ## 1.2.1 (2024-01-19) 67 | 68 | ### Added 69 | 70 | - Better error handling for geo data 71 | 72 | ### Changed 73 | 74 | - Copy, don't stream, in-memory io.Readers like bytes.Buffer, bytes.Reader and strings.Reader 75 | - Fix a bug where a panic under Go (but not TinyGo) would result in handlers returning 200 OK instead of 500 Internal Server Error by not deferring Close() on the response writer internally. 76 | 77 | ## 1.2.0 (2023-11-17) 78 | 79 | ### Added 80 | 81 | - Add support for device detection (`device`) 82 | 83 | ### Changed 84 | 85 | - Switch geolocation internals to use `encoding/json` from a custom built parser 86 | 87 | ## 1.1.0 (2023-10-31) 88 | 89 | ### Added 90 | 91 | - Improve error handling and documentation in `kvstore` package 92 | - Use new hostcalls for better error messages when sending requests to a backend 93 | - Add better unexpected error handling (`cache/core`, `configstore`, `secretstore`) 94 | 95 | ## 1.0.0 (2023-09-13) 96 | 97 | - Unchanged from 0.2.0 98 | 99 | ### Added 100 | 101 | - Tag version 1.0.0 102 | 103 | ## 0.2.0 (2023-08-11) 104 | 105 | ### Added 106 | 107 | - Add support for Go 1.21 WASI 108 | 109 | ### Changed 110 | 111 | - Remove support for TinyGo 0.28.0 and earlier 112 | 113 | ## 0.1.7 (2023-08-04) 114 | 115 | ### Added 116 | 117 | - Add Append method to ResponseWriter 118 | - Add SecretFromBytes to secretstore 119 | - Add support for HandoffWebsocket, HandoffFanout hostcalls (`exp/handoff`) 120 | - Add support for backend query API (`backend`) 121 | - Add support for testing via Viceroy with `go test` 122 | 123 | ### Changed 124 | 125 | - Improve returned errors 126 | 127 | ## 0.1.6 (2023-07-13) 128 | 129 | ### Added 130 | 131 | - Add Simple Cache API 132 | 133 | ## 0.1.5 (2023-06-23) 134 | 135 | ### Changed 136 | 137 | - Fix KV Store hostcalls 138 | 139 | ### Added 140 | 141 | - Add support for RegisterDynamicBackend 142 | 143 | ## 0.1.4 (2023-05-30) 144 | 145 | ### Changed 146 | 147 | - Send `Content-Length: 0` instead of `Transfer-Encoding: chunked` for requests without a body 148 | 149 | ### Added 150 | 151 | - Add Core Cache API 152 | - Add Purge API 153 | - Add package level documentation for Secret Store and KV Store APIs 154 | 155 | ## 0.1.3 (2023-05-15) 156 | 157 | ### Changed 158 | 159 | - Rename objectstore -> kvstore 160 | - Deprecate fstctx 161 | 162 | ### Added 163 | 164 | - Add fsthttp.RequestLimits 165 | 166 | ## 0.1.2 (2023-01-30) 167 | 168 | ### Changed 169 | 170 | - Renamed edgedict -> configstore. 171 | - Made HTTP Request/Response field size limit configurable 172 | 173 | ### Added 174 | 175 | - Add support for Object Store API 176 | - Add support for Secret Store API 177 | - Add adaptor for net/http.RoundTripper (for net/http.Client support) 178 | - Add adaptor for net/http.Handler 179 | - Add fsthttp.Error() and fsthttp.NotFound() helpers 180 | 181 | -- 182 | ## 0.1.1 (2022-06-14) 183 | 184 | ### Changed 185 | 186 | - Use Go 1.17 187 | 188 | -- 189 | ## 0.1.0 (2022-06-11) 190 | 191 | ### Added 192 | 193 | - Initial Release 194 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT: test 2 | 3 | test: test-go test-tinygo test-integration 4 | .PHONY: test 5 | 6 | # Makes tools/viceroy available as an executable within Makefile recipes. 7 | PATH := $(PWD)/tools:$(PATH) 8 | 9 | # Override these with environment variables or directly on the make command line. 10 | GO_BUILD_FLAGS := -tags=fastlyinternaldebug,nofastlyhostcalls 11 | GO_TEST_FLAGS := -v 12 | GO_PACKAGES := ./... 13 | 14 | test-go: 15 | go test $(GO_BUILD_FLAGS) $(GO_TEST_FLAGS) $(GO_PACKAGES) 16 | .PHONY: test-go 17 | 18 | # Using this target lets viceroy provide the wasm runtime, eliminating a dependency on wasmtime. 19 | TINYGO_TARGET := ./targets/fastly-compute-wasip1.json 20 | 21 | test-tinygo: 22 | tinygo test -target=$(TINYGO_TARGET) $(GO_BUILD_FLAGS) $(GO_TEST_FLAGS) $(GO_PACKAGES) 23 | .PHONY: test-tinygo 24 | 25 | # Integration tests use viceroy and override the default values for these variables. 26 | test-integration-%: GO_BUILD_FLAGS := -tags=fastlyinternaldebug 27 | test-integration-%: GO_PACKAGES := ./integration_tests/... 28 | 29 | test-integration: test-integration-go test-integration-tinygo 30 | .PHONY: test-integration 31 | 32 | test-integration-go: tools/viceroy 33 | GOARCH=wasm GOOS=wasip1 go test -exec "viceroy run -C fastly.toml" $(GO_BUILD_FLAGS) $(GO_TEST_FLAGS) $(GO_PACKAGES) 34 | .PHONY: test-integration-go 35 | 36 | test-integration-tinygo: tools/viceroy 37 | tinygo test -target=$(TINYGO_TARGET) $(GO_BUILD_FLAGS) $(GO_TEST_FLAGS) $(GO_PACKAGES) 38 | .PHONY: test-integration-tinygo 39 | 40 | tools/viceroy: | tools # Download latest version of Viceroy ./tools/viceroy; delete it if you'd like to upgrade 41 | @arch=$$(uname -m | sed 's/x86_64/amd64/'); \ 42 | os=$$(uname -s | tr '[:upper:]' '[:lower:]'); \ 43 | url=$$(curl -s https://api.github.com/repos/fastly/viceroy/releases/latest | jq --arg arch $$arch --arg os $$os -r '.assets[] | select((.name | contains($$arch)) and (.name | contains($$os))) | .browser_download_url'); \ 44 | filename=$$(basename $$url); \ 45 | curl -sSLO $$url && \ 46 | tar -xzf $$filename --directory ./tools/ && \ 47 | rm $$filename && \ 48 | ./tools/viceroy --version && \ 49 | touch ./tools/viceroy 50 | ifneq ($(strip $(GITHUB_PATH)),) 51 | @echo "$(PWD)/tools" >> "$(GITHUB_PATH)" 52 | endif 53 | 54 | tools: 55 | @mkdir -p tools 56 | 57 | viceroy-update: 58 | @rm -f tools/viceroy 59 | @$(MAKE) tools/viceroy 60 | .PHONY: viceroy-update 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compute-sdk-go 2 | 3 | Go SDK for building [Fastly Compute](https://www.fastly.com/products/edge-compute) applications with [Go](https://go.dev) (1.21+) and [TinyGo](https://tinygo.org/) (0.28.1+). 4 | 5 | ## Quick Start 6 | 7 | The Fastly Developer Hub has a great [Quick Start guide for Go](https://developer.fastly.com/learning/compute/go/). 8 | 9 | Alternatively, you can take a look at the [Go Starter Kit](https://github.com/fastly/compute-starter-kit-go-default). 10 | 11 | If you're using TinyGo, you'll also want to take a look at our [TinyGo Recommended Packages](#tinygo-recommended-packages) section, as this can help with the sharp edges of the SDK, like JSON support. 12 | 13 | ## Supported Toolchains 14 | 15 | Compute builds on top of WebAssembly and the [WebAssembly System Interface](https://wasi.dev/). 16 | 17 | TinyGo supports WASI as a target, and Go does as of its 1.21 release. 18 | 19 | Each toolchain has its own tradeoffs. Generally speaking, TinyGo produces smaller compiled artifacts and takes less RAM at runtime. Build times are generally longer, sometimes considerably. TinyGo does not support all of the Go standard library, and in particular support for the `reflect` package is incomplete. This means that some third-party packages may not work with TinyGo. 20 | 21 | Runtime performance is mixed, with TinyGo faster on some applications and Go faster on others. If you have a performance-critical application, we recommend benchmarking both toolchains to see which works best for you. 22 | 23 | To switch between TinyGo and Go, set the `build` command in the `[scripts]` section of your `fastly.toml` as follows: 24 | 25 | [scripts] 26 | build = "tinygo build -target=wasi -o bin/main.wasm ." 27 | 28 | or 29 | 30 | [scripts] 31 | build = "GOARCH=wasm GOOS=wasip1 go build -o bin/main.wasm ." 32 | 33 | You might need to adjust the actual build command depending on your project. 34 | 35 | ## Installation 36 | 37 | If you're using Go, download [the latest Go release](https://go.dev/dl/). For TinyGo, follow the [TinyGo Quick install guide](https://tinygo.org/getting-started/install/). 38 | 39 | Then, you can install `compute-sdk-go` in your project by running: 40 | 41 | `go get github.com/fastly/compute-sdk-go` 42 | 43 | ## Examples 44 | 45 | Examples can be found in the [`examples`](./_examples) directory. 46 | 47 | The Fastly Developer Hub has a collection of [common use cases in VCL ported to Go](https://developer.fastly.com/learning/compute/migrate/) which also acts as a great set of introductory examples of using Go on Compute. 48 | 49 | ## API Reference 50 | 51 | The API reference documentation can be found on [pkg.go.dev/github.com/fastly/compute-sdk-go](https://pkg.go.dev/github.com/fastly/compute-sdk-go). 52 | 53 | ## Testing 54 | 55 | Tests that rely on a Compute runtime use [Viceroy](https://github.com/fastly/Viceroy), our local development tool. 56 | 57 | The `Makefile` installs viceroy in ./tools/ and uses this version to run tests. 58 | 59 | Write your tests as ordinary Go tests. Viceroy provides the Compute APIs locally, although be aware that not all platform functionality is available. You can look at the `integration_tests` directory for examples. 60 | 61 | To run your tests: 62 | 63 | make test 64 | 65 | This target runs tests in both Go and TinyGo, and `integration_tests` in both Go and TinyGo in Viceroy. See additional targets in `Makefile` for running subsets of these tests. 66 | 67 | The `targets/fastly-compute-wasi{,p1}.json` files provide TinyGo targets to run Viceroy. 68 | 69 | ## Logging 70 | 71 | Logging can be done using a Fastly Compute Log Endpoint ([example](./_examples/logging-and-env/main.go)), or by using normal stdout like: 72 | 73 | ``` 74 | fmt.Printf("request received: %s\n", r.URL.String()) 75 | ``` 76 | 77 | ## Readthrough HTTP Cache Support 78 | 79 | Customizing cache behaviour with the readthrough cache is an opt-in feature; enable it by adding `-tags=fsthttp_guest_cache` to the build line of your `fastly.toml`. 80 | 81 | ``` 82 | [scripts] 83 | build = "tinygo build -target=wasip1 -tags=fsthttp_guest_cache -o bin/main.wasm ." 84 | ``` 85 | 86 | ## TinyGo Recommended Packages 87 | 88 | TinyGo is still a new project, which has yet to get a version `1.0.0`. Therefore, the project is incomplete, but in its current state can still handle a lot of tasks on Compute. However, [some languages features of Go are still missing](https://tinygo.org/docs/reference/lang-support/). 89 | 90 | To help with your adoption of `compute-sdk-go`, here are some recommended packages to help with some of the current missing language features: 91 | 92 | ### JSON Parsing 93 | 94 | TinyGo's `reflect` support (which is needed by `encoding/json` among other things) is still new. While most use cases should work, for performance or other compatibility reasons you might need to consider a third-party JSON package if the standard library doesn't meet your needs. 95 | 96 | * [valyala/fastjson](https://github.com/valyala/fastjson) 97 | * [mailru/easyjson](https://github.com/mailru/easyjson) 98 | * [buger/jsonparser](https://github.com/buger/jsonparser) 99 | 100 | ## Changelog 101 | 102 | The changelog can be found [here](./CHANGELOG.md). 103 | 104 | ## Security 105 | 106 | If you find any security issues, see the [Fastly Security Reporting Page](https://www.fastly.com/security/report-security-issue) or send an email to: `security@fastly.com` 107 | 108 | Note that communications related to security issues in Fastly-maintained OSS as described here are distinct from [Fastly security advisories](https://www.fastly.com/security-advisories). 109 | 110 | ## License 111 | 112 | [Apache-2.0 WITH LLVM-exception](./LICENSE) 113 | -------------------------------------------------------------------------------- /_examples/acl/acls.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { "op": "update", "prefix": "1.2.3.0/24", "action": "BLOCK" }, 4 | { "op": "create", "prefix": "192.168.0.0/16", "action": "BLOCK" }, 5 | { "op": "update", "prefix": "23.23.23.23/32", "action": "ALLOW" }, 6 | { "op": "create", "prefix": "1.2.3.4/32", "action": "ALLOW" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /_examples/acl/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "gacls" 9 | 10 | 11 | 12 | [scripts] 13 | build = "tinygo build -target=wasip1 -o bin/main.wasm ./" 14 | 15 | 16 | [local_server] 17 | acls.example = "./acls.json" 18 | -------------------------------------------------------------------------------- /_examples/acl/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net" 10 | 11 | "github.com/fastly/compute-sdk-go/acl" 12 | "github.com/fastly/compute-sdk-go/fsthttp" 13 | ) 14 | 15 | func main() { 16 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 17 | aclh, err := acl.Open("example") 18 | if err != nil { 19 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 20 | return 21 | } 22 | 23 | ip := r.URL.Query().Get("ip") 24 | if ip == "" { 25 | ip = r.RemoteAddr 26 | } 27 | 28 | netip := net.ParseIP(ip) 29 | aclr, err := aclh.Lookup(netip) 30 | if errors.Is(err, acl.ErrNoContent) { 31 | fmt.Fprintln(w, "IP:", ip, "No Match") 32 | return 33 | } 34 | if err != nil { 35 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 36 | return 37 | } 38 | 39 | fmt.Fprintln(w, "IP:", ip, "Prefix:", aclr.Prefix, "Action:", aclr.Action) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /_examples/add-or-remove-cookies/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "add-or-remove-cookies" 9 | 10 | [local_server] 11 | backends.backend.url = "https://http-me.glitch.me" 12 | -------------------------------------------------------------------------------- /_examples/add-or-remove-cookies/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/fastly/compute-sdk-go/fsthttp" 11 | "github.com/fastly/compute-sdk-go/rtlog" 12 | ) 13 | 14 | func main() { 15 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 16 | endpoint := rtlog.Open("mylogs") 17 | 18 | // Read a specific cookie 19 | c, err := r.Cookie("myCookie") 20 | if err == nil { 21 | fmt.Fprintf(endpoint, "The value of myCookie is %s\n", c.Value) 22 | } 23 | 24 | // Remove all cookies from the request 25 | r.Header.Del("Cookie") 26 | 27 | resp, err := r.Send(ctx, "backend") 28 | if err != nil { 29 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 30 | return 31 | } 32 | 33 | // Set a cookie in the response 34 | fsthttp.SetCookie(resp.Header, &fsthttp.Cookie{ 35 | Name: "myCookie", 36 | Value: "foo", 37 | Path: "/", 38 | MaxAge: 60, 39 | }) 40 | 41 | // You can set multiple cookies in one response 42 | resp.Header.Add("Set-Cookie", "mySecondCookie=bar; httpOnly") 43 | 44 | // It is usually a good idea to prevent downstream caching of 45 | // responses that set cookies 46 | resp.Header.Set("Cache-Control", "no-store, private") 47 | 48 | w.Header().Reset(resp.Header) 49 | w.WriteHeader(resp.StatusCode) 50 | io.Copy(w, resp.Body) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /_examples/corecache/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "corecache" 9 | 10 | # Note that this example doesn't currently work with Viceroy (`fastly 11 | # compute serve`) so you'll need to deploy it to Compute in order 12 | # to test it. You can do that with `fastly compute build` and `fastly 13 | # compute deploy`. 14 | -------------------------------------------------------------------------------- /_examples/corecache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | "time" 12 | 13 | "github.com/fastly/compute-sdk-go/cache/core" 14 | "github.com/fastly/compute-sdk-go/fsthttp" 15 | "github.com/fastly/compute-sdk-go/purge" 16 | ) 17 | 18 | func main() { 19 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 20 | key := keyForRequest(r) 21 | switch r.Method { 22 | 23 | // Fetch content from the cache. 24 | case "GET": 25 | tx, err := core.NewTransaction(key, core.LookupOptions{}) 26 | if err != nil { 27 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 28 | return 29 | } 30 | defer tx.Close() 31 | 32 | f, err := tx.Found() 33 | if errors.Is(err, core.ErrNotFound) { 34 | fsthttp.Error(w, err.Error(), fsthttp.StatusNotFound) 35 | return 36 | } 37 | if err != nil { 38 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 39 | return 40 | } 41 | defer f.Body.Close() 42 | 43 | msg, err := io.ReadAll(f.Body) 44 | if err != nil { 45 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 46 | return 47 | } 48 | 49 | w.Header().Set("Content-Type", "text/plain") 50 | fmt.Fprintf(w, "%s's message for %s is: %s\n", getPOP(), r.URL.Path, msg) 51 | 52 | // Write data to the cache and stream it back to the client. 53 | case "POST": 54 | if r.Header.Get("Content-Type") != "text/plain" && r.Header.Get("Content-Type") != "text/plain; charset=utf-8" { 55 | w.WriteHeader(fsthttp.StatusUnsupportedMediaType) 56 | return 57 | } 58 | 59 | msg, err := io.ReadAll(r.Body) 60 | if err != nil { 61 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 62 | return 63 | } 64 | 65 | tx, err := core.NewTransaction(key, core.LookupOptions{}) 66 | if err != nil { 67 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 68 | return 69 | } 70 | defer tx.Close() 71 | 72 | if !tx.MustInsert() { 73 | w.WriteHeader(fsthttp.StatusConflict) 74 | return 75 | } 76 | 77 | // We call InsertAndStreamBack to create both a handle to 78 | // write the content into the cache and a Found object to 79 | // stream the contents back out to the client. As soon as 80 | // data is written to the insert body, it is immediately 81 | // streamed to all clients waiting on this transaction, 82 | // including this one. 83 | // 84 | // This is preferable to using an io.MultiWriter because a 85 | // MultiWriter is constrained by the slowest writer. If 86 | // other transactions are waiting on the content to be 87 | // written to the cache and streamed, we don't want that 88 | // process to be delayed by a slow client for this request. 89 | insertBody, found, err := tx.InsertAndStreamBack(core.WriteOptions{ 90 | TTL: 600 * time.Second, 91 | Length: uint64(len(msg)), 92 | SurrogateKeys: []string{hex.EncodeToString(key)}, 93 | }) 94 | if err != nil { 95 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 96 | return 97 | } 98 | defer found.Body.Close() 99 | 100 | insertBody.Write(msg) 101 | 102 | if err := insertBody.Close(); err != nil { 103 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 104 | return 105 | } 106 | 107 | msg, err = io.ReadAll(found.Body) 108 | if err != nil { 109 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 110 | return 111 | } 112 | 113 | w.Header().Set("Content-Type", "text/plain") 114 | w.WriteHeader(fsthttp.StatusCreated) 115 | fmt.Fprintf(w, "%s's message for %s is: %s\n", getPOP(), r.URL.Path, msg) 116 | 117 | // Purge the key from the cache. 118 | case "DELETE": 119 | if err := purge.PurgeSurrogateKey(hex.EncodeToString(key), purge.PurgeOptions{}); err != nil { 120 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 121 | return 122 | } 123 | w.WriteHeader(fsthttp.StatusAccepted) 124 | 125 | default: 126 | w.WriteHeader(fsthttp.StatusMethodNotAllowed) 127 | } 128 | }) 129 | } 130 | 131 | func keyForRequest(r *fsthttp.Request) []byte { 132 | h := sha256.New() 133 | h.Write([]byte(r.URL.Path)) 134 | h.Write([]byte(getPOP())) 135 | return h.Sum(nil) 136 | } 137 | 138 | func getPOP() string { 139 | return os.Getenv("FASTLY_POP") 140 | } 141 | -------------------------------------------------------------------------------- /_examples/device-detection/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "device-detection" 9 | service_id = "" 10 | -------------------------------------------------------------------------------- /_examples/device-detection/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/fastly/compute-sdk-go/device" 10 | "github.com/fastly/compute-sdk-go/fsthttp" 11 | ) 12 | 13 | func main() { 14 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 15 | d, err := device.Lookup(r.Header.Get("User-Agent")) 16 | if err != nil { 17 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 18 | return 19 | } 20 | 21 | fmt.Fprintf(w, "%+v\n", d) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /_examples/geodata/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "geodata" 9 | 10 | # You can customize the geodata that is returned. See the developer 11 | # documentation linked above for more info. 12 | -------------------------------------------------------------------------------- /_examples/geodata/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net" 9 | 10 | "github.com/fastly/compute-sdk-go/fsthttp" 11 | "github.com/fastly/compute-sdk-go/geo" 12 | ) 13 | 14 | func main() { 15 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 16 | ip := net.ParseIP(r.RemoteAddr) 17 | g, err := geo.Lookup(ip) 18 | if err != nil { 19 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 20 | return 21 | } 22 | 23 | fmt.Fprintf(w, "AsName: %q\n", g.AsName) 24 | fmt.Fprintf(w, "AsNumber: %d\n", g.AsNumber) 25 | fmt.Fprintf(w, "AreaCode: %d\n", g.AreaCode) 26 | fmt.Fprintf(w, "City: %q\n", g.City) 27 | fmt.Fprintf(w, "ConnSpeed: %q\n", g.ConnSpeed) 28 | fmt.Fprintf(w, "ConnType: %q\n", g.ConnType) 29 | fmt.Fprintf(w, "ContinentCode: %q\n", g.ContinentCode) 30 | fmt.Fprintf(w, "CountryCode: %q\n", g.CountryCode) 31 | fmt.Fprintf(w, "CountryCode3: %q\n", g.CountryCode3) 32 | fmt.Fprintf(w, "CountryName: %q\n", g.CountryName) 33 | fmt.Fprintf(w, "Latitude: %f\n", g.Latitude) 34 | fmt.Fprintf(w, "Longitude: %f\n", g.Longitude) 35 | fmt.Fprintf(w, "MetroCode: %d\n", g.MetroCode) 36 | fmt.Fprintf(w, "PostalCode: %q\n", g.PostalCode) 37 | fmt.Fprintf(w, "ProxyDescription: %q\n", g.ProxyDescription) 38 | fmt.Fprintf(w, "ProxyType: %q\n", g.ProxyType) 39 | fmt.Fprintf(w, "Region: %q\n", g.Region) 40 | fmt.Fprintf(w, "UTCOffset: %d\n", g.UTCOffset) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /_examples/hello-world/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "hello-world" 9 | -------------------------------------------------------------------------------- /_examples/hello-world/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/fastly/compute-sdk-go/fsthttp" 10 | ) 11 | 12 | func main() { 13 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 14 | fmt.Fprintf(w, "Hello, %s!\n", r.RemoteAddr) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /_examples/http-adapter/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "example" 9 | 10 | [local_server] 11 | backends.httpme.url = "https://http-me.glitch.me" 12 | -------------------------------------------------------------------------------- /_examples/http-adapter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/fastly/compute-sdk-go/fsthttp" 11 | ) 12 | 13 | const backend = "httpme" 14 | 15 | func main() { 16 | // http.ServeMux is an http.Handler implementation. You can use any 17 | // one here, including chi, gorilla/mux, etc. 18 | mux := http.NewServeMux() 19 | 20 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 21 | w.Write([]byte("Hello World!")) 22 | }) 23 | 24 | mux.HandleFunc("/ip", func(w http.ResponseWriter, r *http.Request) { 25 | req, err := fsthttp.NewRequest("GET", "https://http-me.glitch.me/ip", nil) 26 | if err != nil { 27 | w.WriteHeader(fsthttp.StatusInternalServerError) 28 | w.Write([]byte(err.Error())) 29 | return 30 | } 31 | 32 | req.Header.Set("Fastly-Debug", "1") 33 | 34 | resp, err := req.Send(r.Context(), backend) 35 | if err != nil { 36 | w.WriteHeader(fsthttp.StatusBadGateway) 37 | w.Write([]byte(err.Error())) 38 | return 39 | } 40 | 41 | for k, v := range resp.Header { 42 | w.Header()[k] = v 43 | } 44 | 45 | w.WriteHeader(resp.StatusCode) 46 | io.Copy(w, resp.Body) 47 | fmt.Fprintf(w, "\n---\n") 48 | 49 | ofr := fsthttp.RequestFromContext(r.Context()) 50 | fmt.Fprintf(w, "%s\n", ofr.Host) 51 | }) 52 | 53 | mux.HandleFunc("/long", func(w http.ResponseWriter, r *http.Request) { 54 | rand.Seed(time.Now().UnixNano()) 55 | 56 | ctx := r.Context() 57 | processTime := time.Duration(rand.Intn(10)+1) * time.Second 58 | 59 | select { 60 | case <-ctx.Done(): 61 | return 62 | 63 | case <-time.After(processTime): 64 | // The above channel simulates some hard work. 65 | } 66 | 67 | w.Write([]byte("done")) 68 | }) 69 | 70 | fsthttp.Serve(fsthttp.Adapt(mux)) 71 | } 72 | -------------------------------------------------------------------------------- /_examples/kvstore/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "kvstore" 9 | 10 | [local_server] 11 | kv_stores.example_kvstore = [{key = "foo", data = "Hello from KV Store!\n"}] 12 | -------------------------------------------------------------------------------- /_examples/kvstore/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "strings" 9 | 10 | "github.com/fastly/compute-sdk-go/fsthttp" 11 | "github.com/fastly/compute-sdk-go/kvstore" 12 | ) 13 | 14 | func main() { 15 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 16 | o, err := kvstore.Open("example_kvstore") 17 | if err != nil { 18 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 19 | return 20 | } 21 | 22 | v, err := o.Lookup("foo") 23 | if err != nil { 24 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 25 | return 26 | } 27 | 28 | w.WriteHeader(fsthttp.StatusOK) 29 | io.Copy(w, v) 30 | 31 | // We can detect when a key does not exist and supply a default value instead. 32 | var reader io.Reader 33 | v, err = o.Lookup("might-not-exist") 34 | if err != nil && err == kvstore.ErrKeyNotFound { 35 | reader = strings.NewReader("default value") 36 | } else if err != nil { 37 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 38 | return 39 | } else { 40 | reader = v 41 | } 42 | 43 | io.Copy(w, reader) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /_examples/limits/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "limits" 9 | -------------------------------------------------------------------------------- /_examples/limits/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/fastly/compute-sdk-go/fsthttp" 10 | ) 11 | 12 | func main() { 13 | // Increase URL length limit to 16K 14 | fsthttp.RequestLimits.SetMaxURLLen(16 * 1024) 15 | 16 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 17 | fmt.Fprintf( 18 | w, 19 | "The length of the URL is %d and the maximum is %d\n", 20 | len(r.URL.String()), 21 | fsthttp.RequestLimits.MaxURLLen(), 22 | ) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /_examples/logging-and-env/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "logging-and-env" 9 | -------------------------------------------------------------------------------- /_examples/logging-and-env/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | 12 | "github.com/fastly/compute-sdk-go/fsthttp" 13 | "github.com/fastly/compute-sdk-go/rtlog" 14 | ) 15 | 16 | func main() { 17 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 18 | fmt.Fprintln(os.Stdout, "os.Stdout can be streamed via `fastly logs tail`") 19 | fmt.Fprintln(os.Stderr, "os.Stderr can be streamed via `fastly logs tail`") 20 | 21 | endpoint := rtlog.Open("my-logging-endpoint") 22 | fmt.Fprintln(endpoint, "Real-time logging is available via `package rtlog`") 23 | 24 | mw := io.MultiWriter(os.Stdout, endpoint, w) 25 | fmt.Fprintln(mw, "Mix-and-match destinations with helpers like io.MultiWriter") 26 | 27 | fmt.Fprintln(mw, "Several environment variables are defined by default...") 28 | for _, key := range []string{ 29 | "FASTLY_CACHE_GENERATION", 30 | "FASTLY_CUSTOMER_ID", 31 | "FASTLY_HOSTNAME", 32 | "FASTLY_POP", 33 | "FASTLY_REGION", 34 | "FASTLY_SERVICE_ID", 35 | "FASTLY_SERVICE_VERSION", 36 | "FASTLY_TRACE_ID", 37 | } { 38 | fmt.Fprintf(mw, "%s=%s\n", key, os.Getenv(key)) 39 | } 40 | 41 | prefix := fmt.Sprintf("%s | %s | ", os.Getenv("FASTLY_SERVICE_VERSION"), r.RemoteAddr) 42 | logger := log.New(os.Stdout, prefix, log.LstdFlags|log.LUTC) 43 | logger.Printf("It can be useful to create a logger with request-specific metadata built in") 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /_examples/middlewares/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "middlewares" 9 | -------------------------------------------------------------------------------- /_examples/middlewares/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/fastly/compute-sdk-go/fsthttp" 13 | ) 14 | 15 | func main() { 16 | var h fsthttp.Handler 17 | h = newHandler(fmt.Sprintf("Hello from %s!", os.Getenv("FASTLY_POP"))) 18 | h = newLoggingMiddleware(h) 19 | fsthttp.Serve(h) 20 | } 21 | 22 | // 23 | // 24 | // 25 | 26 | type handler struct { 27 | greeting string 28 | } 29 | 30 | func newHandler(greeting string) *handler { 31 | return &handler{ 32 | greeting: greeting, 33 | } 34 | } 35 | 36 | func (h *handler) ServeHTTP(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 37 | fmt.Fprintln(w, h.greeting) 38 | } 39 | 40 | // 41 | // 42 | // 43 | 44 | type loggingMiddleware struct { 45 | next fsthttp.Handler 46 | } 47 | 48 | func newLoggingMiddleware(next fsthttp.Handler) *loggingMiddleware { 49 | return &loggingMiddleware{ 50 | next: next, 51 | } 52 | } 53 | 54 | func (mw *loggingMiddleware) ServeHTTP(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 55 | irw := &interceptingResponseWriter{ResponseWriter: w, code: fsthttp.StatusOK} 56 | 57 | defer func(begin time.Time) { 58 | log.Printf("%s: %s %s %s: %d (%s)", r.RemoteAddr, r.Proto, r.Method, r.URL, irw.code, time.Since(begin)) 59 | }(time.Now()) 60 | 61 | mw.next.ServeHTTP(ctx, irw, r) 62 | } 63 | 64 | // 65 | // 66 | // 67 | 68 | type interceptingResponseWriter struct { 69 | fsthttp.ResponseWriter 70 | code int 71 | } 72 | 73 | func (irw *interceptingResponseWriter) WriteHeader(code int) { 74 | irw.code = code 75 | irw.ResponseWriter.WriteHeader(code) 76 | } 77 | -------------------------------------------------------------------------------- /_examples/multiple-goroutines/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "multiple-goroutines" 9 | -------------------------------------------------------------------------------- /_examples/multiple-goroutines/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "math/rand" 9 | "time" 10 | 11 | "github.com/fastly/compute-sdk-go/fsthttp" 12 | ) 13 | 14 | func main() { 15 | begin := time.Now() 16 | rand.Seed(begin.UnixNano()) 17 | 18 | c := make(chan string, 5) 19 | for i := 0; i < cap(c); i++ { 20 | go func(i int) { 21 | r := 1 + rand.Intn(99) 22 | d := time.Duration(r) * time.Millisecond 23 | time.Sleep(d) 24 | c <- fmt.Sprintf("goroutine %d took %s", i, d) 25 | }(i) 26 | } 27 | 28 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 29 | for i := 0; i < cap(c); i++ { 30 | fmt.Fprintln(w, <-c) 31 | } 32 | fmt.Fprintln(w, "overall", time.Since(begin)) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /_examples/parallel-requests/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "parallel-requests" 9 | 10 | [local_server] 11 | backends.httpme.url = "https://http-me.glitch.me" 12 | -------------------------------------------------------------------------------- /_examples/parallel-requests/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "log" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/fastly/compute-sdk-go/fsthttp" 14 | ) 15 | 16 | func main() { 17 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 18 | // Log to the console (`fastly logs tail`) and the client. 19 | log := log.New(io.MultiWriter(os.Stdout, w), "", log.Ltime) 20 | log.Printf("Starting") 21 | begin := time.Now() 22 | 23 | // Send several requests in parallel. 24 | var wg sync.WaitGroup 25 | for _, url := range []string{ 26 | "https://http-me.glitch.me/drip=2?wait=3000", // delay 3s + stream body 2s = 5s 27 | "https://http-me.glitch.me/drip=2?wait=2000", // delay 2s + stream body 2s = 4s 28 | "https://http-me.glitch.me/wait=3000", // delay 3s + stream body 0s = 3s 29 | } { 30 | wg.Add(1) 31 | go func(url string) { 32 | defer wg.Done() 33 | log.Printf("Starting %s", url) 34 | 35 | req, err := fsthttp.NewRequest(fsthttp.MethodGet, url, nil) 36 | if err != nil { 37 | log.Printf("%s: create request: %v", url, err) 38 | return 39 | } 40 | req.CacheOptions.Pass = true 41 | 42 | // Sending HTTP requests in separate goroutines is both 43 | // concurrent and parallel. For example, 3 requests that each 44 | // take 3s to return a response will take about 3s in total. 45 | resp, err := req.Send(ctx, "httpme") 46 | if err != nil { 47 | log.Printf("%s: send request: %v", url, err) 48 | return 49 | } 50 | 51 | // All other code run in separate goroutines is concurrent but 52 | // not parallel. For example, reading 3 response bodies that 53 | // each take 3s will take about 9s in total. 54 | _, err = io.Copy(io.Discard, resp.Body) 55 | if err != nil { 56 | log.Printf("%s: stream response body: %v", url, err) 57 | return 58 | } 59 | 60 | log.Printf("Finished %s", url) 61 | }(url) 62 | } 63 | wg.Wait() 64 | 65 | // All requests should finish in about as long as the longest individual 66 | // request took. That is, about 5s, rather than 5s+4s+3s=12s. 67 | log.Printf("Finished after %s", time.Since(begin)) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /_examples/print-request/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "print-request" 9 | -------------------------------------------------------------------------------- /_examples/print-request/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/fastly/compute-sdk-go/fsthttp" 11 | ) 12 | 13 | func main() { 14 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 15 | fmt.Fprintf(w, "Method: %q\n", r.Method) 16 | fmt.Fprintf(w, "URL: %v\n", r.URL) 17 | fmt.Fprintf(w, "Proto: %q\n", r.Proto) 18 | fmt.Fprintf(w, "ProtoMajor: %d\n", r.ProtoMajor) 19 | fmt.Fprintf(w, "ProtoMinor: %d\n", r.ProtoMinor) 20 | fmt.Fprintf(w, "RemoteAddr: %q\n", r.RemoteAddr) 21 | fmt.Fprintf(w, "TLSInfo:\n") 22 | fmt.Fprintf(w, " Protocol: %s\n", r.TLSInfo.Protocol) 23 | fmt.Fprintf(w, " CipherOpenSSLName: %s\n", r.TLSInfo.CipherOpenSSLName) 24 | fmt.Fprintf(w, " JA3MD5: %#x\n", r.TLSInfo.JA3MD5) 25 | fmt.Fprintf(w, " ClientHello: %#x\n", r.TLSInfo.ClientHello) 26 | 27 | fmt.Fprintf(w, "\n") 28 | 29 | for _, k := range r.Header.Keys() { 30 | fmt.Fprintf(w, "%s: %v\n", k, r.Header.Get(k)) 31 | } 32 | 33 | fmt.Fprintf(w, "\n") 34 | 35 | io.Copy(w, r.Body) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /_examples/proxy-request-framing/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "proxy-request-framing" 9 | 10 | [local_server] 11 | backends.httpme.url = "https://http-me.glitch.me" 12 | -------------------------------------------------------------------------------- /_examples/proxy-request-framing/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "io" 8 | 9 | "github.com/fastly/compute-sdk-go/fsthttp" 10 | ) 11 | 12 | func main() { 13 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 14 | // Reset the URI and Host header, so the request is 15 | // recognized and routed correctly at the origin. 16 | r.URL.Scheme, r.URL.Host = "https", "http-me.glitch.me" 17 | r.Header.Set("host", "http-me.glitch.me") 18 | 19 | // Determine the framing headers (Content-Length/Transfer-Encoding) 20 | // based on the message body (default) 21 | r.ManualFramingMode = false 22 | 23 | // Make sure the response isn't cached. 24 | r.CacheOptions.Pass = true 25 | 26 | // This requires your service to be configured with a backend 27 | // named "httpme" and pointing to "https://http-me.glitch.me". 28 | resp, err := r.Send(ctx, "httpme") 29 | if err != nil { 30 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 31 | return 32 | } 33 | 34 | w.Header().Reset(resp.Header) 35 | 36 | // Use the framing headers set in the message. 37 | w.SetManualFramingMode(true) 38 | 39 | w.WriteHeader(resp.StatusCode) 40 | io.Copy(w, resp.Body) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /_examples/proxy-request/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "proxy-request" 9 | 10 | [local_server] 11 | backends.httpme.url = "https://http-me.glitch.me" 12 | -------------------------------------------------------------------------------- /_examples/proxy-request/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "io" 8 | 9 | "github.com/fastly/compute-sdk-go/fsthttp" 10 | ) 11 | 12 | func main() { 13 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 14 | // Reset the URI and Host header, so the request is 15 | // recognized and routed correctly at the origin. 16 | r.URL.Scheme, r.URL.Host = "https", "http-me.glitch.me" 17 | r.Header.Set("host", "http-me.glitch.me") 18 | 19 | // Make sure the response isn't cached. 20 | r.CacheOptions.Pass = true 21 | 22 | // This requires your service to be configured with a backend 23 | // named "httpme" and pointing to "https://http-me.glitch.me". 24 | resp, err := r.Send(ctx, "httpme") 25 | if err != nil { 26 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 27 | return 28 | } 29 | 30 | w.Header().Reset(resp.Header) 31 | w.WriteHeader(resp.StatusCode) 32 | io.Copy(w, resp.Body) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /_examples/secret-store/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "secret-store" 9 | 10 | [local_server] 11 | secret_stores.example_secretstore = [{key = "my_secret", data = "Open sesame!"}] 12 | -------------------------------------------------------------------------------- /_examples/secret-store/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/fastly/compute-sdk-go/fsthttp" 9 | "github.com/fastly/compute-sdk-go/secretstore" 10 | ) 11 | 12 | func main() { 13 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 14 | v, err := secretstore.Plaintext("example_secretstore", "my_secret") 15 | switch { 16 | case errors.Is(err, secretstore.ErrSecretStoreNotFound) || errors.Is(err, secretstore.ErrSecretNotFound): 17 | fsthttp.Error(w, err.Error(), fsthttp.StatusNotFound) 18 | return 19 | case err != nil: 20 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 21 | return 22 | } 23 | 24 | fmt.Fprintf(w, "secret value: %q", v) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /_examples/set-cookie/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "set-cookie" 9 | -------------------------------------------------------------------------------- /_examples/set-cookie/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/fastly/compute-sdk-go/fsthttp" 10 | ) 11 | 12 | func main() { 13 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 14 | cookie := &fsthttp.Cookie{ 15 | Name: "Hello", 16 | Value: r.RemoteAddr, 17 | Secure: true, 18 | HttpOnly: true, 19 | SameSite: fsthttp.SameSiteStrictMode, 20 | } 21 | fsthttp.SetCookie(w.Header(), cookie) 22 | 23 | fmt.Fprintln(w, "Your cookie has been set!") 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /_examples/set-google-analytics/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "set-google-analytics" 9 | 10 | [local_server] 11 | backends.backend.url = "https://http-me.glitch.me" 12 | -------------------------------------------------------------------------------- /_examples/set-google-analytics/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | "math/rand" 10 | "strings" 11 | "time" 12 | 13 | "github.com/fastly/compute-sdk-go/fsthttp" 14 | ) 15 | 16 | func main() { 17 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 18 | resp, err := r.Send(ctx, "backend") 19 | if err != nil { 20 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 21 | return 22 | } 23 | c, err := r.Cookie("_ga") 24 | if r.Header.Get("Fastly-FF") == "" && (err != nil || !strings.HasPrefix(c.Value, "GA")) { 25 | now := time.Now() 26 | rand.Seed(now.UnixNano()) 27 | 28 | // The _ga cookie is made up of four fields: 29 | // 30 | // 1. Version number (GA1). 31 | // 2. Number of components in the domain separated by dot. 32 | // 3. Random unique ID. 33 | // 4. Time stamp. 34 | 35 | host := r.Header.Get("Host") 36 | numSegs := strings.Count(host, ".") + 1 37 | i := rand.Intn(2147483647-1000000000) + 1000000000 38 | value := fmt.Sprintf("GA1.%d.%d.%d", numSegs, i, now.Unix()) 39 | 40 | cookie := &fsthttp.Cookie{ 41 | Name: "_ga", 42 | Value: value, 43 | Domain: "." + host, 44 | MaxAge: 3600 * 24 * 365 * 2, // two years in seconds 45 | } 46 | 47 | fsthttp.SetCookie(resp.Header, cookie) 48 | 49 | // Prevent browser from caching a set-cookie 50 | resp.Header.Set("Cache-Control", "no-store, private") 51 | } 52 | 53 | w.Header().Reset(resp.Header) 54 | w.WriteHeader(resp.StatusCode) 55 | io.Copy(w, resp.Body) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /_examples/simplecache/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "simplecache" 9 | 10 | # Note that this example doesn't currently work with Viceroy (`fastly 11 | # compute serve`) so you'll need to deploy it to Compute in order 12 | # to test it. You can do that with `fastly compute build` and `fastly 13 | # compute deploy`. 14 | -------------------------------------------------------------------------------- /_examples/simplecache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | "io" 8 | "os" 9 | "time" 10 | 11 | "github.com/fastly/compute-sdk-go/cache/simple" 12 | "github.com/fastly/compute-sdk-go/fsthttp" 13 | ) 14 | 15 | func main() { 16 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 17 | w.Header().Set("Service-Version", os.Getenv("FASTLY_SERVICE_VERSION")) 18 | 19 | key := keyForRequest(r) 20 | switch r.Method { 21 | 22 | // Fetch content from the cache. 23 | case "GET": 24 | rc, err := simple.Get(key) 25 | if err != nil { 26 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 27 | return 28 | } 29 | defer rc.Close() 30 | 31 | msg, err := io.ReadAll(rc) 32 | if err != nil { 33 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 34 | return 35 | } 36 | 37 | w.Header().Set("Content-Type", "text/plain") 38 | fmt.Fprintf(w, "%s's message for %s is: %s\n", getPOP(), r.URL.Path, msg) 39 | 40 | // Write data to the cache (if there's nothing there) and stream it back to the client. 41 | case "POST": 42 | if r.Header.Get("Content-Type") != "text/plain" && r.Header.Get("Content-Type") != "text/plain; charset=utf-8" { 43 | w.WriteHeader(fsthttp.StatusUnsupportedMediaType) 44 | return 45 | } 46 | 47 | var set bool 48 | rc, err := simple.GetOrSet(key, func() (simple.CacheEntry, error) { 49 | set = true 50 | return simple.CacheEntry{ 51 | Body: r.Body, 52 | TTL: 3 * time.Minute, 53 | }, nil 54 | }) 55 | if err != nil { 56 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 57 | return 58 | } 59 | defer rc.Close() 60 | 61 | if !set { 62 | w.WriteHeader(fsthttp.StatusConflict) 63 | return 64 | } 65 | 66 | msg, err := io.ReadAll(rc) 67 | if err != nil { 68 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 69 | return 70 | } 71 | 72 | w.Header().Set("Content-Type", "text/plain") 73 | w.WriteHeader(fsthttp.StatusOK) 74 | fmt.Fprintf(w, "%s's message for %s is: %s\n", getPOP(), r.URL.Path, msg) 75 | 76 | // Purge the key from the cache. 77 | case "DELETE": 78 | if err := simple.Purge(key, simple.PurgeOptions{}); err != nil { 79 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 80 | return 81 | } 82 | w.WriteHeader(fsthttp.StatusAccepted) 83 | 84 | default: 85 | w.WriteHeader(fsthttp.StatusMethodNotAllowed) 86 | } 87 | }) 88 | } 89 | 90 | func keyForRequest(r *fsthttp.Request) []byte { 91 | h := sha256.New() 92 | h.Write([]byte(r.URL.Path)) 93 | return h.Sum(nil) 94 | } 95 | 96 | func getPOP() string { 97 | return os.Getenv("FASTLY_POP") 98 | } 99 | -------------------------------------------------------------------------------- /_examples/stream-response/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "stream-response" 9 | -------------------------------------------------------------------------------- /_examples/stream-response/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/fastly/compute-sdk-go/fsthttp" 12 | ) 13 | 14 | func main() { 15 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 16 | n := getQueryInt(r, "n", 5) 17 | d := getQueryDuration(r, "d", 250*time.Millisecond) 18 | 19 | // If you're using cURL, be sure to use `-N, --no-buffer`. 20 | fmt.Fprintf(w, "n=%d, d=%s\n", n, d) 21 | for i := 1; i <= n; i++ { 22 | time.Sleep(d) 23 | fmt.Fprintf(w, " ʕ◔ϖ◔ʔ") 24 | } 25 | fmt.Fprintln(w) 26 | }) 27 | } 28 | 29 | func getQueryInt(r *fsthttp.Request, key string, def int) int { 30 | i, err := strconv.Atoi(r.URL.Query().Get(key)) 31 | if err != nil { 32 | return def 33 | } 34 | return i 35 | } 36 | 37 | func getQueryDuration(r *fsthttp.Request, key string, def time.Duration) time.Duration { 38 | d, err := time.ParseDuration(r.URL.Query().Get(key)) 39 | if err != nil { 40 | return def 41 | } 42 | return d 43 | } 44 | -------------------------------------------------------------------------------- /_examples/with-timeout/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "with-timeout" 9 | 10 | [local_server] 11 | backends.httpme.url = "https://http-me.glitch.me" 12 | -------------------------------------------------------------------------------- /_examples/with-timeout/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "log" 8 | "time" 9 | 10 | "github.com/fastly/compute-sdk-go/fsthttp" 11 | ) 12 | 13 | func main() { 14 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 15 | begin := time.Now() 16 | 17 | // Create a context with a 1-second timeout. 18 | ctx, cancel := context.WithTimeout(ctx, 1*time.Second) 19 | defer cancel() 20 | 21 | // Create the request, and set pass to true, to avoid caching. 22 | req, err := fsthttp.NewRequest(fsthttp.MethodGet, "https://http-me.glitch.me/wait=3000", nil) 23 | if err != nil { 24 | log.Printf("create request: %v", err) 25 | return 26 | } 27 | req.CacheOptions.Pass = true 28 | 29 | // This request takes 3 seconds to complete but should error after 1 30 | // second. It also requires your service to be configured with a backend 31 | // named "httpme" and pointing to "https://http-me.glitch.me". 32 | _, err = req.Send(ctx, "httpme") 33 | if err != nil { 34 | log.Printf("send request errored after %s: %v", time.Since(begin), err) 35 | return 36 | } 37 | 38 | // This line should not print because the request should have errored 39 | // before it completed. 40 | log.Printf("Finished after %s", time.Since(begin)) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /acl/acl.go: -------------------------------------------------------------------------------- 1 | // Package acl provides access to Fastly ACLs. 2 | // 3 | // See the [Fastly ACL documentation] for details. 4 | // 5 | // [Fastly ACL documentation]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#access-control-lists 6 | package acl 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "net" 13 | 14 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 15 | ) 16 | 17 | var ( 18 | // ErrNotFound indicates the requested ACL was not found. 19 | ErrNotFound = errors.New("acl: not found") 20 | 21 | // ErrInvalidHandle indicatest the ACL handle was invalid. 22 | ErrInvalidHandle = errors.New("acl: invalid handle") 23 | 24 | // ErrInvalidResponseBody indicates the looup response body was invalid. 25 | ErrInvalidResponseBody = errors.New("acl: invalid response body") 26 | 27 | // ErrInvalidArgument indicates the IP address was invalid. 28 | ErrInvalidArgument = errors.New("acl: invalid argument") 29 | 30 | // ErrNoContent indicates there was no entry for the provided IP address. 31 | ErrNoContent = errors.New("acl: no content") 32 | 33 | // ErrTooManyRequests indicates too many requests were made. 34 | ErrTooManyRequests = errors.New("acl: too many requests") 35 | 36 | // ErrUnexpected indicates an unexpected error occurred. 37 | ErrUnexpected = errors.New("acl: unexepected error") 38 | ) 39 | 40 | // Handle is a handle for an ACL 41 | type Handle struct { 42 | h *fastly.ACLHandle 43 | } 44 | 45 | // Response is an ACL lookup response 46 | type Response struct { 47 | Prefix string // Matching prefix in CIDR notation 48 | Action string // Associated prefix's action 49 | } 50 | 51 | // Open returns a handle to the named ACL. 52 | func Open(name string) (*Handle, error) { 53 | a, err := fastly.OpenACL(name) 54 | if err != nil { 55 | status, ok := fastly.IsFastlyError(err) 56 | switch { 57 | case ok && status == fastly.FastlyStatusNone: 58 | return nil, ErrNotFound 59 | case ok: 60 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 61 | default: 62 | return nil, err 63 | } 64 | } 65 | 66 | return &Handle{h: a}, nil 67 | 68 | } 69 | 70 | // Lookup the given IP in the ACL and returns the response. If no match was found, returns ErrNoContent. 71 | func (h *Handle) Lookup(ip net.IP) (Response, error) { 72 | body, err := h.h.Lookup(ip) 73 | if err != nil { 74 | return Response{}, mapFastlyErr(err) 75 | } 76 | 77 | var r Response 78 | dec := json.NewDecoder(body) 79 | if err := dec.Decode(&r); err != nil { 80 | return Response{}, err 81 | } 82 | return r, nil 83 | } 84 | 85 | func mapFastlyErr(err error) error { 86 | // Is it a acl-specific error? 87 | if aclErr, ok := err.(fastly.ACLError); ok { 88 | switch aclErr { 89 | case fastly.ACLErrorUninitialized: // we really shouldn't be returning this 90 | return ErrUnexpected 91 | case fastly.ACLErrorOK: 92 | // Not an error; we shouldn't get here 93 | return fmt.Errorf("%w (%s)", ErrUnexpected, err) 94 | case fastly.ACLErrorNoContent: 95 | return ErrNoContent 96 | case fastly.ACLErrorTooManyRequests: 97 | return ErrTooManyRequests 98 | } 99 | return fmt.Errorf("%w (%s)", ErrUnexpected, err) 100 | } 101 | 102 | // Maybe it was a fastly error? 103 | status, ok := fastly.IsFastlyError(err) 104 | switch { 105 | case ok && status == fastly.FastlyStatusBadf: 106 | return ErrInvalidHandle 107 | case ok && status == fastly.FastlyStatusInval: 108 | return ErrInvalidArgument 109 | case ok: 110 | return fmt.Errorf("%w (%s)", ErrUnexpected, status) 111 | } 112 | 113 | // No idea; just return what we have. 114 | return err 115 | } 116 | -------------------------------------------------------------------------------- /cache/core/core_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/fastly/compute-sdk-go/cache/core" 9 | ) 10 | 11 | func ExampleLookup() { 12 | // f is a core.Found value, representing a found cache item. 13 | // core.ErrNotFound is returned if the item is not cached. 14 | f, err := core.Lookup([]byte("my_key"), core.LookupOptions{}) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer f.Body.Close() 19 | 20 | // The contents of the cached item are in the Found's Body field. 21 | cachedStr, err := io.ReadAll(f.Body) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | fmt.Printf("The cached value was: %s", cachedStr) 27 | } 28 | 29 | func ExampleInsert() { 30 | const ( 31 | key = "my_key" 32 | contents = "my cached object" 33 | ) 34 | 35 | // w is a core.WriteCloseAbandoner, a superset of io.WriteCloser. 36 | // Data written to this handle is streamed into the Fastly cache. 37 | w, err := core.Insert([]byte(key), core.WriteOptions{ 38 | TTL: time.Hour, 39 | SurrogateKeys: []string{key}, 40 | Length: uint64(len(contents)), 41 | }) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | if _, err := io.WriteString(w, contents); err != nil { 47 | panic(err) 48 | } 49 | 50 | // The writer must be closed to complete the cache operation. 51 | // Always check for errors from Close. 52 | if err := w.Close(); err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | func ExampleTransaction() { 58 | // Users of the transactional API should at a minimum anticipate 59 | // lookups that are obligated to insert an object into the cache, 60 | // and lookups which are not. If the stale-while-revalidate 61 | // parameter is set for a cached object, the user should also 62 | // distinguish between the insertion and revalidation cases. 63 | 64 | useFoundItem := func(f *core.Found) { 65 | // Do something with the found item 66 | } 67 | 68 | buildContents := func() []byte { 69 | // Build the contents of the cached item 70 | return []byte("hello world!") 71 | } 72 | 73 | shouldReplace := func(f *core.Found, contents []byte) bool { 74 | // Determine whether the cached item should be replaced with 75 | // the new contents 76 | return true 77 | } 78 | 79 | tx, err := core.NewTransaction([]byte("my_key"), core.LookupOptions{}) 80 | if err != nil { 81 | panic(err) 82 | } 83 | defer tx.Close() 84 | 85 | // f is a core.Found value, representing a found cache item. 86 | // core.ErrNotFound is returned if the item is not cached. 87 | f, err := tx.Found() 88 | switch err { 89 | case nil: 90 | // A cached item was found, though it might be stale. 91 | useFoundItem(f) 92 | 93 | // Perform revalidation, if necessary. 94 | if tx.MustInsertOrUpdate() { 95 | contents := buildContents() 96 | if shouldReplace(f, contents) { 97 | // Use Insert to replace the previous object 98 | w, err := tx.Insert(core.WriteOptions{ 99 | TTL: time.Hour, 100 | SurrogateKeys: []string{"my_key"}, 101 | Length: uint64(len(contents)), 102 | }) 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | if _, err := w.Write(contents); err != nil { 108 | panic(err) 109 | } 110 | 111 | if err := w.Close(); err != nil { 112 | panic(err) 113 | } 114 | } else { 115 | // Otherwise update the stale object's metadata 116 | if err := tx.Update(core.WriteOptions{ 117 | TTL: time.Hour, 118 | SurrogateKeys: []string{"my_key"}, 119 | }); err != nil { 120 | panic(err) 121 | } 122 | } 123 | } 124 | 125 | case core.ErrNotFound: 126 | // The item was not found. 127 | if tx.MustInsert() { 128 | // We've been chosen to insert the object. 129 | contents := buildContents() 130 | w, f, err := tx.InsertAndStreamBack(core.WriteOptions{ 131 | TTL: time.Hour, 132 | SurrogateKeys: []string{"my_key"}, 133 | Length: uint64(len(contents)), 134 | }) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | if _, err := w.Write(contents); err != nil { 140 | panic(err) 141 | } 142 | 143 | if err := w.Close(); err != nil { 144 | panic(err) 145 | } 146 | 147 | useFoundItem(f) 148 | } else { 149 | panic(err) 150 | } 151 | 152 | default: 153 | // An unexpected error 154 | panic(err) 155 | } 156 | } 157 | 158 | func ExampleFound_GetRange() { 159 | const ( 160 | key = "my_key" 161 | contents = "my cached object" 162 | ) 163 | 164 | // Start by filling the cache... 165 | w, err := core.Insert([]byte(key), core.WriteOptions{ 166 | TTL: time.Hour, 167 | SurrogateKeys: []string{key}, 168 | Length: uint64(len(contents)), 169 | }) 170 | if err != nil { 171 | panic(err) 172 | } 173 | if _, err := io.WriteString(w, contents); err != nil { 174 | panic(err) 175 | } 176 | if err := w.Close(); err != nil { 177 | panic(err) 178 | } 179 | 180 | // We get a response... 181 | f, err := core.Lookup([]byte("my_key"), core.LookupOptions{}) 182 | if err != nil { 183 | panic(err) 184 | } 185 | // ...then discard the body, so we can re-open a new body reading a subset of the bytes. 186 | if err := f.Body.Close(); err != nil { 187 | panic(err) 188 | } 189 | 190 | // If we try to read an invalid range (from > to), we get an error: 191 | _, err = f.GetRange(3, 1) 192 | if err == nil { 193 | panic("accepted invalid range") 194 | } 195 | 196 | // We can use "0" as a signal value to say "everything to the end": 197 | body, err := f.GetRange(3, 0) 198 | if err == nil { 199 | panic("accepted invalid range") 200 | } 201 | cachedStr, err := io.ReadAll(body) 202 | if err != nil { 203 | panic(err) 204 | } 205 | if string(cachedStr) != "cached object" { 206 | panic(fmt.Sprintf("got: %q, want: %q", cachedStr, "cached object")) 207 | } 208 | 209 | fmt.Printf("The cached value was: %s", cachedStr) 210 | } 211 | -------------------------------------------------------------------------------- /cache/core/fastly.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/compute-sdk-go/11fceb8997c83b7f827666b999056e6a92e01bf8/cache/core/fastly.toml -------------------------------------------------------------------------------- /cache/simple/simple.go: -------------------------------------------------------------------------------- 1 | // Package simple provides the Simple Cache API, a simplified interface 2 | // to inserting and retrieving entries from Fastly's cache. 3 | // 4 | // Cache operations are local to the Fastly POP serving the request. 5 | // Purging can also be performed globally. 6 | // 7 | // For more advanced uses, see the Core Cache API in the 8 | // [github.com/fastly/compute-sdk-go/cache/core] package. 9 | package simple 10 | 11 | import ( 12 | "crypto/sha256" 13 | "encoding/hex" 14 | "io" 15 | "os" 16 | "strings" 17 | "time" 18 | 19 | "github.com/fastly/compute-sdk-go/cache/core" 20 | "github.com/fastly/compute-sdk-go/purge" 21 | ) 22 | 23 | // Get retrieves the object stored in the cache for the given key. If 24 | // the key is not cached, [core.ErrNotFound] is returned. Keys can be 25 | // up to 4096 bytes in length. 26 | // 27 | // The returned [io.ReadCloser] must be closed by the caller when 28 | // finished. 29 | func Get(key []byte) (io.ReadCloser, error) { 30 | f, err := core.Lookup(key, core.LookupOptions{}) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return f.Body, nil 35 | } 36 | 37 | // CacheEntry contains the contents and TTL (time-to-live) for an item 38 | // to be added to the cache via [GetOrSet] or [GetOrSetContents]. 39 | type CacheEntry struct { 40 | // The contents of the cached object. 41 | Body io.Reader 42 | 43 | // The time-to-live for the cached object. 44 | TTL time.Duration 45 | } 46 | 47 | // GetOrSet retrieves the object stored in the cache for the given key 48 | // if it exists, or inserts and returns the contents by running the 49 | // provided setFn function. 50 | // 51 | // The setFn function is only run when no value is present for the key, 52 | // and no other client is in the process of setting it. The function 53 | // should return a populated [CacheEntry] or an error. 54 | // 55 | // If the setFn function returns an error, nothing will be saved to the 56 | // cache and the error will be returned from the GetOrSet function. 57 | // Other concurrent readers will also see an error while reading. 58 | // 59 | // The returned [io.ReadCloser] must be closed by the caller when 60 | // finished. 61 | func GetOrSet(key []byte, setFn func() (CacheEntry, error)) (io.ReadCloser, error) { 62 | tx, err := core.NewTransaction(key, core.LookupOptions{}) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer tx.Close() 67 | 68 | if tx.MustInsertOrUpdate() { 69 | e, err := setFn() 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | w, f, err := tx.InsertAndStreamBack(core.WriteOptions{ 75 | TTL: e.TTL, 76 | SurrogateKeys: []string{ 77 | SurrogateKeyForCacheKey(key, PurgeScopePOP), 78 | SurrogateKeyForCacheKey(key, PurgeScopeGlobal), 79 | }, 80 | }) 81 | if err != nil { 82 | return nil, err 83 | } 84 | defer w.Close() 85 | 86 | if _, err := io.Copy(w, e.Body); err != nil { 87 | w.Abandon() 88 | return nil, err 89 | } 90 | 91 | if err := w.Close(); err != nil { 92 | w.Abandon() 93 | return nil, err 94 | } 95 | 96 | return f.Body, nil 97 | } 98 | 99 | f, err := tx.Found() 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return f.Body, nil 105 | } 106 | 107 | // GetOrSetEntry retrieves the object stored in the cache for the given 108 | // key if it exists, or inserts and returns the contents provided in the 109 | // [CacheEntry]. 110 | // 111 | // The cache entry is only inserted when no value is present for the 112 | // key, and no other client is in the process of setting it. 113 | // 114 | // If the cache entry body content is costly to compute, consider using 115 | // [GetOrSet] instead to avoid creating its [io.Reader] in the case 116 | // where the value is already present. 117 | // 118 | // The returned [io.ReadCloser] must be closed by the caller when 119 | // finished. 120 | func GetOrSetEntry(key []byte, entry CacheEntry) (io.ReadCloser, error) { 121 | return GetOrSet(key, func() (CacheEntry, error) { 122 | return entry, nil 123 | }) 124 | } 125 | 126 | // PurgeScope controls the scope of a purge operation. It is used in 127 | // the [PurgeOptions] struct. 128 | type PurgeScope uint32 129 | 130 | const ( 131 | // PurgeScopePOP purges the entry only from the local POP cache. 132 | PurgeScopePOP PurgeScope = iota 133 | // PurgeScopeGlobal purges the entry from all POP caches. 134 | PurgeScopeGlobal 135 | ) 136 | 137 | // PurgeOptions controls the behavior of the [Purge] function. 138 | type PurgeOptions struct { 139 | Scope PurgeScope 140 | } 141 | 142 | // Purge removes the entry associated with the given cache key, if one 143 | // exists. 144 | // 145 | // The scope of the purge can be controlled with the PurgeOptions. 146 | // 147 | // Purges are handled asynchronously, and the cached object may persist 148 | // in cache for a short time (~150ms or less) after this function 149 | // returns. 150 | func Purge(key []byte, opts PurgeOptions) error { 151 | sk := SurrogateKeyForCacheKey(key, opts.Scope) 152 | return purge.PurgeSurrogateKey(sk, purge.PurgeOptions{}) 153 | } 154 | 155 | // SurrogateKeyForCacheKey creates a surrogate key for the given cache 156 | // key and purge scope that is compatible with the Simple Cache API. 157 | // Each cache entry for the Simple Cache API is configured with 158 | // surrogate keys from this function. 159 | // 160 | // This function is provided as a convenience for implementors wishing 161 | // to add a Simple Cache-compatible surrogate key manually via the Core 162 | // Cache API ([github.com/fastly/compute-sdk-go/cache/core]) for 163 | // interoperability with [Purge]. 164 | func SurrogateKeyForCacheKey(cacheKey []byte, scope PurgeScope) string { 165 | // The values are SHA-256 digests of the cache key (plus the local POP 166 | // for the local surrogate key), converted to uppercase hexadecimal. 167 | // This scheme must be kept consistent across all compute SDKs. 168 | h := sha256.New() 169 | h.Write(cacheKey) 170 | if scope == PurgeScopePOP { 171 | h.Write([]byte(os.Getenv("FASTLY_POP"))) 172 | } 173 | 174 | return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) 175 | } 176 | -------------------------------------------------------------------------------- /compute/compute.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Fastly, Inc. 2 | 3 | // Useful functions for interacting with the compute instance runtime. 4 | package compute 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 10 | ) 11 | 12 | // Get the amount of time taken on the vCPU. 13 | // 14 | // The resulting time is millisecond-accurate, but we recommend against 15 | // comparing the absolute values returned across different runs (or builds) 16 | // of the program. 17 | // 18 | // Because compute guests can run on a variety of different platforms, 19 | // you should not necessarily expect these values to converge across 20 | // different sessions. Instead, we strongly recommend using this value 21 | // to look at the relative cost of various operations in your code base, 22 | // by taking the time before and after a particular operation and then 23 | // dividing this by the total amount of vCPU time your program takes. 24 | // The resulting percentage should be relatively stable across different 25 | // platforms, and useful in doing A/B testing. 26 | func GetVCPUTime() (time.Duration, error) { 27 | milliseconds, err := fastly.GetVCPUMilliseconds() 28 | 29 | if err != nil { 30 | return 0, err 31 | } 32 | 33 | result := time.Duration(milliseconds) * time.Millisecond 34 | 35 | return result, nil 36 | } 37 | -------------------------------------------------------------------------------- /compute/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Fastly, Inc. 2 | 3 | // Package compute_runtime provides ways to reflect on the operation of the 4 | // compute runtime in which your application is operating. 5 | package compute 6 | -------------------------------------------------------------------------------- /configstore/configstore.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package configstore 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 10 | ) 11 | 12 | var ( 13 | // ErrStoreNotFound indicates the named config store doesn't exist. 14 | ErrStoreNotFound = errors.New("config store not found") 15 | 16 | // ErrStoreNameEmpty indicates the given config store name 17 | // was empty. 18 | ErrStoreNameEmpty = errors.New("config store name was empty") 19 | 20 | // ErrStoreNameInvalid indicates the given config store name 21 | // was invalid. 22 | ErrStoreNameInvalid = errors.New("config store name contained invalid characters") 23 | 24 | // ErrStoreNameTooLong indicates the given config store name 25 | // was too long. 26 | ErrStoreNameTooLong = errors.New("config store name too long") 27 | 28 | // ErrKeyNotFound indicates a key isn't in a config store. 29 | ErrKeyNotFound = errors.New("key not found") 30 | 31 | // ErrUnexpected indicates an unexpected error occurred. 32 | ErrUnexpected = errors.New("unexpected error") 33 | ) 34 | 35 | // Store is a read-only representation of a config store. 36 | type Store struct { 37 | abiDict *fastly.ConfigStore 38 | } 39 | 40 | // Open returns a config store with the given name. Names are case 41 | // sensitive. 42 | func Open(name string) (*Store, error) { 43 | d, err := fastly.OpenConfigStore(name) 44 | if err != nil { 45 | status, ok := fastly.IsFastlyError(err) 46 | switch { 47 | case ok && status == fastly.FastlyStatusBadf: 48 | return nil, ErrStoreNotFound 49 | case ok && status == fastly.FastlyStatusNone: 50 | return nil, ErrStoreNameEmpty 51 | case ok && status == fastly.FastlyStatusUnsupported: 52 | return nil, ErrStoreNameTooLong 53 | case ok && status == fastly.FastlyStatusInval: 54 | return nil, ErrStoreNameInvalid 55 | case ok: 56 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 57 | default: 58 | return nil, err 59 | } 60 | } 61 | return &Store{d}, nil 62 | } 63 | 64 | // Has returns true if the key exists in the config store, without allocating 65 | // space to read a value. 66 | func (s *Store) Has(key string) (bool, error) { 67 | if s == nil { 68 | return false, ErrKeyNotFound 69 | } 70 | 71 | v, err := s.abiDict.Has(key) 72 | if err != nil { 73 | status, ok := fastly.IsFastlyError(err) 74 | switch { 75 | case ok && status == fastly.FastlyStatusBadf: 76 | return false, ErrStoreNotFound 77 | case ok && status == fastly.FastlyStatusNone: 78 | return false, ErrKeyNotFound 79 | case ok: 80 | return false, fmt.Errorf("%w (%s)", ErrUnexpected, status) 81 | default: 82 | return false, err 83 | } 84 | } 85 | 86 | return v, nil 87 | } 88 | 89 | // GetBytes returns the value in the config store for the given key, if it exists, as a byte slice. 90 | func (s *Store) GetBytes(key string) ([]byte, error) { 91 | if s == nil { 92 | return nil, ErrKeyNotFound 93 | } 94 | 95 | v, err := s.abiDict.GetBytes(key) 96 | if err != nil { 97 | status, ok := fastly.IsFastlyError(err) 98 | switch { 99 | case ok && status == fastly.FastlyStatusBadf: 100 | return nil, ErrStoreNotFound 101 | case ok && status == fastly.FastlyStatusNone: 102 | return nil, ErrKeyNotFound 103 | case ok: 104 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 105 | default: 106 | return nil, err 107 | } 108 | } 109 | return v, nil 110 | } 111 | 112 | // Get returns the value in the config store with the given key, if it exists. 113 | func (s *Store) Get(key string) (string, error) { 114 | buf, err := s.GetBytes(key) 115 | if err != nil { 116 | return "", err 117 | } 118 | return string(buf), nil 119 | } 120 | -------------------------------------------------------------------------------- /configstore/configstore_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package configstore 4 | 5 | import "testing" 6 | 7 | func TestStore(t *testing.T) { 8 | var c *Store 9 | val, err := c.Get("xyzzy") 10 | if err != ErrKeyNotFound { 11 | t.Errorf("Expected get on nil configstore to return ErrKeyNotFound") 12 | } 13 | // check val despite err being non-nil 14 | if val != "" { 15 | t.Errorf("Expected get on nil configstore to return empty string") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /configstore/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package configstore provides a read-only interface to config stores. Refer 4 | // to https://developer.fastly.com/reference/api/services/resources/config-store/ 5 | // for more information. 6 | package configstore 7 | -------------------------------------------------------------------------------- /configstore/fastly.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/compute-sdk-go/11fceb8997c83b7f827666b999056e6a92e01bf8/configstore/fastly.toml -------------------------------------------------------------------------------- /device/device_detection.go: -------------------------------------------------------------------------------- 1 | // Package device provides device dection based on the User-Agent 2 | // header. 3 | package device 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 11 | ) 12 | 13 | var ( 14 | // ErrDeviceNotFound is returned when the device is not found. 15 | ErrDeviceNotFound = errors.New("device not found") 16 | 17 | // ErrUnexpected indicates that an unexpected error occurred. 18 | ErrUnexpected = errors.New("unexpected error") 19 | ) 20 | 21 | type Device struct { 22 | info deviceInfo 23 | } 24 | 25 | type deviceInfo struct { 26 | Device struct { 27 | Name string `json:"name"` 28 | Brand string `json:"brand"` 29 | Model string `json:"model"` 30 | HWType string `json:"hwtype"` 31 | IsEReader bool `json:"is_ereader"` 32 | IsGameConsole bool `json:"is_gameconsole"` 33 | IsMediaPlayer bool `json:"is_mediaplayer"` 34 | IsMobile bool `json:"is_mobile"` 35 | IsSmartTV bool `json:"is_smarttv"` 36 | IsTablet bool `json:"is_tablet"` 37 | IsTVPlayer bool `json:"is_tvplayer"` 38 | IsDesktop bool `json:"is_desktop"` 39 | IsTouchscreen bool `json:"is_touchscreen"` 40 | } `json:"device"` 41 | } 42 | 43 | func Lookup(userAgent string) (Device, error) { 44 | var d Device 45 | 46 | raw, err := fastly.DeviceLookup(userAgent) 47 | if err != nil { 48 | status, ok := fastly.IsFastlyError(err) 49 | switch { 50 | case ok && status == fastly.FastlyStatusNone: 51 | return d, ErrDeviceNotFound 52 | case ok: 53 | return d, fmt.Errorf("%w (%s)", ErrUnexpected, status) 54 | default: 55 | return d, err 56 | } 57 | } 58 | 59 | if err := json.Unmarshal(raw, &d.info); err != nil { 60 | return d, err 61 | } 62 | 63 | return d, nil 64 | } 65 | 66 | // Name returns the name of the client device. 67 | func (d *Device) Name() string { 68 | return d.info.Device.Name 69 | } 70 | 71 | // Brand returns the brand of the client device, possibly different from 72 | // the manufacturer of that device. 73 | func (d *Device) Brand() string { 74 | return d.info.Device.Brand 75 | } 76 | 77 | // Model returns the model of the client device. 78 | func (d *Device) Model() string { 79 | return d.info.Device.Model 80 | } 81 | 82 | // HWType returns a string representation of the primary client platform 83 | // hardware. The most commonly used device types are also identified 84 | // via boolean variables. Because a device may have multiple device 85 | // types and this variable only has the primary type, we recommend using 86 | // the boolean variables for logic and using this string representation 87 | // for logging. 88 | func (d *Device) HWType() string { 89 | return d.info.Device.HWType 90 | } 91 | 92 | // IsEReader returns true if the client device is a reading device (like 93 | // a Kindle). 94 | func (d *Device) IsEReader() bool { 95 | return d.info.Device.IsEReader 96 | } 97 | 98 | // IsGameConsole returns true if the client device is a video game 99 | // console (like a PlayStation or Xbox). 100 | func (d *Device) IsGameConsole() bool { 101 | return d.info.Device.IsGameConsole 102 | } 103 | 104 | // IsMediaPlayer returns true if the client device is a media player 105 | // (like Blu-ray players, iPod devices, and smart speakers such as 106 | // Amazon Echo). 107 | func (d *Device) IsMediaPlayer() bool { 108 | return d.info.Device.IsMediaPlayer 109 | } 110 | 111 | // IsMobile returns true if the client device is a mobile phone. 112 | func (d *Device) IsMobile() bool { 113 | return d.info.Device.IsMobile 114 | } 115 | 116 | // IsSmartTV returns true if the client device is a smart TV. 117 | func (d *Device) IsSmartTV() bool { 118 | return d.info.Device.IsSmartTV 119 | } 120 | 121 | // IsTablet returns true if the client device is a tablet (like an 122 | // iPad). 123 | func (d *Device) IsTablet() bool { 124 | return d.info.Device.IsTablet 125 | } 126 | 127 | // IsTVPlayer returns true if the client device is a set-top box or 128 | // other TV player (like a Roku or Apple TV). 129 | func (d *Device) IsTVPlayer() bool { 130 | return d.info.Device.IsTVPlayer 131 | } 132 | 133 | // IsDesktop returns true if the client device is a desktop web browser. 134 | func (d *Device) IsDesktop() bool { 135 | return d.info.Device.IsDesktop 136 | } 137 | 138 | // IsTouchscreen returns true if the client device's screen is touch 139 | // sensitive. 140 | func (d *Device) IsTouchscreen() bool { 141 | return d.info.Device.IsTouchscreen 142 | } 143 | -------------------------------------------------------------------------------- /edgedict/dictionary.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package edgedict 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 10 | ) 11 | 12 | var ( 13 | // ErrDictionaryNotFound indicates the named dictionary doesn't exist. 14 | ErrDictionaryNotFound = errors.New("dictionary not found") 15 | 16 | // ErrDictionaryNameEmpty indicates the given dictionary name 17 | // was empty. 18 | ErrDictionaryNameEmpty = errors.New("dictionary name was empty") 19 | 20 | // ErrDictionaryNameInvalid indicates the given dictionary name 21 | // was invalid. 22 | ErrDictionaryNameInvalid = errors.New("dictionary name contained invalid characters") 23 | 24 | // ErrDictionaryNameTooLong indicates the given dictionary name 25 | // was too long. 26 | ErrDictionaryNameTooLong = errors.New("dictionary name too long") 27 | 28 | // ErrKeyNotFound indicates a key isn't in a dictionary. 29 | ErrKeyNotFound = errors.New("key not found") 30 | 31 | // ErrUnexpected indicates an unexpected error occurred. 32 | ErrUnexpected = errors.New("unexpected error") 33 | ) 34 | 35 | // Dictionary is a read-only representation of an edge dictionary. 36 | // 37 | // Deprecated: Use the configstore package instead. 38 | type Dictionary struct { 39 | abiDict *fastly.Dictionary 40 | } 41 | 42 | // Open returns an edge dictionary with the given name. Names are case 43 | // sensitive. 44 | // 45 | // Deprecated: Use configstore.Open() instead. 46 | func Open(name string) (*Dictionary, error) { 47 | d, err := fastly.OpenDictionary(name) 48 | if err != nil { 49 | status, ok := fastly.IsFastlyError(err) 50 | switch { 51 | case ok && status == fastly.FastlyStatusBadf: 52 | return nil, ErrDictionaryNotFound 53 | case ok && status == fastly.FastlyStatusNone: 54 | return nil, ErrDictionaryNameEmpty 55 | case ok && status == fastly.FastlyStatusUnsupported: 56 | return nil, ErrDictionaryNameTooLong 57 | case ok && status == fastly.FastlyStatusInval: 58 | return nil, ErrDictionaryNameInvalid 59 | default: 60 | return nil, err 61 | } 62 | } 63 | return &Dictionary{d}, nil 64 | } 65 | 66 | // GetBytes returns the value in the dictionary for the given key, if it exists, as a byte slice. 67 | func (d *Dictionary) GetBytes(key string) ([]byte, error) { 68 | if d == nil { 69 | return nil, ErrKeyNotFound 70 | } 71 | 72 | v, err := d.abiDict.GetBytes(key) 73 | if err != nil { 74 | status, ok := fastly.IsFastlyError(err) 75 | switch { 76 | case ok && status == fastly.FastlyStatusBadf: 77 | return nil, ErrDictionaryNotFound 78 | case ok && status == fastly.FastlyStatusNone: 79 | return nil, ErrKeyNotFound 80 | case ok: 81 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 82 | default: 83 | return nil, err 84 | } 85 | } 86 | return v, nil 87 | } 88 | 89 | // Get returns the value in the dictionary with the given key, if it exists. 90 | func (d *Dictionary) Get(key string) (string, error) { 91 | buf, err := d.GetBytes(key) 92 | if err != nil { 93 | return "", err 94 | } 95 | return string(buf), nil 96 | } 97 | 98 | // Has returns true if the key exists in the dictionary, without allocating 99 | // space to read a value. 100 | func (d *Dictionary) Has(key string) (bool, error) { 101 | if d == nil { 102 | return false, ErrKeyNotFound 103 | } 104 | 105 | v, err := d.abiDict.Has(key) 106 | if err != nil { 107 | status, ok := fastly.IsFastlyError(err) 108 | switch { 109 | case ok && status == fastly.FastlyStatusBadf: 110 | return false, ErrDictionaryNotFound 111 | case ok && status == fastly.FastlyStatusNone: 112 | return false, ErrKeyNotFound 113 | case ok: 114 | return false, fmt.Errorf("%w (%s)", ErrUnexpected, status) 115 | default: 116 | return false, err 117 | } 118 | } 119 | 120 | return v, nil 121 | } 122 | -------------------------------------------------------------------------------- /edgedict/dictionary_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package edgedict 4 | 5 | import "testing" 6 | 7 | func TestDictionary(t *testing.T) { 8 | var d *Dictionary 9 | val, err := d.Get("xyzzy") 10 | if err != ErrKeyNotFound { 11 | t.Errorf("Expected get on nil dictionary to return ErrKeyNotFound") 12 | } 13 | // check val despite err being non-nil 14 | if val != "" { 15 | t.Errorf("Expected get on nil dictionary to return empty string") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /edgedict/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package edgedict provides a read-only interface to edge dictionaries. Refer 4 | // to https://developer.fastly.com/reference/api/dictionaries/ for more 5 | // information. 6 | // 7 | // Deprecated: Use the configstore package instead. 8 | package edgedict 9 | -------------------------------------------------------------------------------- /edgedict/fastly.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/compute-sdk-go/11fceb8997c83b7f827666b999056e6a92e01bf8/edgedict/fastly.toml -------------------------------------------------------------------------------- /erl/erl_test.go: -------------------------------------------------------------------------------- 1 | package erl_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/fastly/compute-sdk-go/erl" 9 | "github.com/fastly/compute-sdk-go/fsthttp" 10 | ) 11 | 12 | func ExampleRateLimiter_CheckRate() { 13 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 14 | limiter := erl.NewRateLimiter( 15 | erl.OpenRateCounter("requests"), 16 | erl.OpenPenaltyBox("bad_ips"), 17 | ) 18 | 19 | block, err := limiter.CheckRate( 20 | r.RemoteAddr, // Use the IP address of the client as the entry 21 | 1, // Increment the request counter by 1 22 | &erl.Policy{ 23 | erl.RateWindow10s, // Check the rate of requests per second over the past 10 seconds 24 | 100, // Allow up to 100 requests per second 25 | time.Minute, // Put offenders into the penalty box for 1 minute 26 | }, 27 | ) 28 | if err != nil { 29 | // It's probably better to fail open. Consider logging the 30 | // error but continuing to handle the request. 31 | } else if block { 32 | // The rate limit has been exceeded. Return a 429 Too Many 33 | // Requests response. 34 | w.WriteHeader(fsthttp.StatusTooManyRequests) 35 | return 36 | } 37 | 38 | // Otherwise, continue processing the request. 39 | }) 40 | } 41 | 42 | func ExampleRateCounter_LookupRate() { 43 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 44 | rc := erl.OpenRateCounter("requests") 45 | 46 | // Increment the request counter by 1 47 | rc.Increment(r.RemoteAddr, 1) 48 | 49 | // Get the current rate of requests per second over the past 60 50 | // seconds 51 | rate, err := rc.LookupRate(r.RemoteAddr, erl.RateWindow60s) 52 | if err != nil { 53 | w.WriteHeader(fsthttp.StatusInternalServerError) 54 | return 55 | } 56 | 57 | fmt.Fprintf(w, "Rate over the past 60 seconds: %d requests per second\n", rate) 58 | }) 59 | } 60 | 61 | func ExampleRateCounter_LookupCount() { 62 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 63 | rc := erl.OpenRateCounter("requests") 64 | 65 | // Increment the request counter by 1 66 | rc.Increment(r.RemoteAddr, 1) 67 | 68 | // Get an estimated count of total number of requests over the 69 | // past 60 seconds 70 | count, err := rc.LookupCount(r.RemoteAddr, erl.CounterDuration60s) 71 | if err != nil { 72 | w.WriteHeader(fsthttp.StatusInternalServerError) 73 | return 74 | } 75 | 76 | fmt.Fprintf(w, "Estimated count over the past 60 seconds: %d requests\n", count) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /erl/fastly.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/compute-sdk-go/11fceb8997c83b7f827666b999056e6a92e01bf8/erl/fastly.toml -------------------------------------------------------------------------------- /fsthttp/adapter.go: -------------------------------------------------------------------------------- 1 | package fsthttp 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // responseWriterAdapter is an implementation of http.ResponseWriter on 9 | // top of fsthttp.ResponseWriter. It is necessary because the Header 10 | // types are different, despite being otherwise compatible. 11 | type responseWriterAdapter struct { 12 | w ResponseWriter 13 | } 14 | 15 | func (w *responseWriterAdapter) Header() http.Header { 16 | return http.Header(w.w.Header()) 17 | } 18 | 19 | func (w *responseWriterAdapter) Write(b []byte) (int, error) { 20 | return w.w.Write(b) 21 | } 22 | 23 | func (w *responseWriterAdapter) WriteHeader(status int) { 24 | w.w.WriteHeader(status) 25 | } 26 | 27 | // Adapt allows an http.Handler to be used as an fsthttp.Handler. 28 | // 29 | // Because the Request and ResponseWriter types are not exactly the same 30 | // as ones in net/http, helper accessor functions exist to extract the 31 | // fsthttp values from the request context. 32 | func Adapt(h http.Handler) Handler { 33 | return HandlerFunc(func(ctx context.Context, w ResponseWriter, r *Request) { 34 | ctx = contextWithRequest(ctx, r) 35 | ctx = contextWithResponseWriter(ctx, w) 36 | 37 | hw := &responseWriterAdapter{w: w} 38 | 39 | hr, err := http.NewRequestWithContext(ctx, r.Method, r.URL.String(), r.Body) 40 | if err != nil { 41 | w.WriteHeader(StatusInternalServerError) 42 | return 43 | } 44 | hr.Proto = r.Proto 45 | hr.ProtoMajor = r.ProtoMajor 46 | hr.ProtoMinor = r.ProtoMinor 47 | hr.Header = http.Header(r.Header.Clone()) 48 | hr.Host = r.Host 49 | hr.RemoteAddr = r.RemoteAddr 50 | hr.ContentLength = -1 51 | 52 | // TODO: Translate some of fsthttp.TLSInfo into 53 | // tls.ConnectionState. 54 | // 55 | // The protocol version and chosen cipher are available but 56 | // provided via the ABI as strings, which we would need to 57 | // convert back into integer values. 58 | // 59 | // The raw ClientHello is provided, so we could use 60 | // golang.org/x/crypto/cryptobyte to parse it. But 61 | // server-chosen properties of the connection (cipher, ALPN, 62 | // client certificate, etc.) would need to be provided by the 63 | // ABI. 64 | 65 | h.ServeHTTP(hw, hr) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /fsthttp/adapter_test.go: -------------------------------------------------------------------------------- 1 | // This test file is in its own test package to avoid a circular 2 | // dependency between fsthttp and fsttest. 3 | 4 | package fsthttp_test 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/fastly/compute-sdk-go/fsthttp" 13 | "github.com/fastly/compute-sdk-go/fsttest" 14 | ) 15 | 16 | func TestAdapter(t *testing.T) { 17 | t.Parallel() 18 | 19 | hh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | fr := fsthttp.RequestFromContext(r.Context()) 21 | if fr == nil { 22 | http.Error(w, "no fsthttp.Request in context", http.StatusInternalServerError) 23 | return 24 | } 25 | 26 | fw := fsthttp.ResponseWriterFromContext(r.Context()) 27 | if fw == nil { 28 | http.Error(w, "no fsthttp.ResponseWriter in context", http.StatusInternalServerError) 29 | return 30 | } 31 | 32 | w.WriteHeader(http.StatusTeapot) 33 | fmt.Fprintln(w, "Hello, client") 34 | }) 35 | 36 | r, err := fsthttp.NewRequest(http.MethodGet, "http://example.com", nil) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | w := fsttest.NewRecorder() 42 | 43 | fsthttp.Adapt(hh).ServeHTTP(context.Background(), w, r) 44 | 45 | if want, got := fsthttp.StatusTeapot, w.Code; want != got { 46 | t.Errorf("want code %d, got %d", want, got) 47 | } 48 | 49 | if want, got := "Hello, client\n", w.Body.String(); want != got { 50 | t.Errorf("want body %q, got %q", want, got) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /fsthttp/cache_guest.go: -------------------------------------------------------------------------------- 1 | //go:build fsthttp_guest_cache 2 | 3 | package fsthttp 4 | 5 | var useGuestCaching = true 6 | -------------------------------------------------------------------------------- /fsthttp/cache_host.go: -------------------------------------------------------------------------------- 1 | //go:build !fsthttp_guest_cache 2 | 3 | package fsthttp 4 | 5 | var useGuestCaching = false 6 | -------------------------------------------------------------------------------- /fsthttp/context.go: -------------------------------------------------------------------------------- 1 | package fsthttp 2 | 3 | import "context" 4 | 5 | type ( 6 | requestContextKey struct{} 7 | responseWriterContextKey struct{} 8 | responseContextKey struct{} 9 | ) 10 | 11 | // RequestFromContext returns the fsthttp.Request associated with the 12 | // context, if any. 13 | func RequestFromContext(ctx context.Context) *Request { 14 | req, _ := ctx.Value(requestContextKey{}).(*Request) 15 | return req 16 | } 17 | 18 | func contextWithRequest(ctx context.Context, req *Request) context.Context { 19 | return context.WithValue(ctx, requestContextKey{}, req) 20 | } 21 | 22 | // ResponseWriterFromContext returns the fsthttp.ResponseWriter associated 23 | // with the context, if any. 24 | func ResponseWriterFromContext(ctx context.Context) ResponseWriter { 25 | w, _ := ctx.Value(responseWriterContextKey{}).(ResponseWriter) 26 | return w 27 | } 28 | 29 | func contextWithResponseWriter(ctx context.Context, w ResponseWriter) context.Context { 30 | return context.WithValue(ctx, responseWriterContextKey{}, w) 31 | } 32 | 33 | // ResponseFromContext returns the fsthttp.Response associated with the 34 | // context, if any. 35 | func ResponseFromContext(ctx context.Context) *Response { 36 | resp, _ := ctx.Value(responseContextKey{}).(*Response) 37 | return resp 38 | } 39 | 40 | func contextWithResponse(ctx context.Context, resp *Response) context.Context { 41 | return context.WithValue(ctx, responseContextKey{}, resp) 42 | } 43 | -------------------------------------------------------------------------------- /fsthttp/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package fsthttp provides HTTP functionality for Fastly's Compute 4 | // environment. 5 | // 6 | // A Compute program can be thought of as an HTTP request handler. Each 7 | // execution is triggered by an incoming request from a client, and is expected 8 | // to respond to that request before terminating. The Serve function provides a 9 | // Handler-style interface to that Request and its ResponseWriter. 10 | // 11 | // The types in this package are similar to, but not the same as, corresponding 12 | // types in the standard library's package net/http. Refer to the documentation 13 | // for important caveats about usage. 14 | package fsthttp 15 | -------------------------------------------------------------------------------- /fsthttp/fastly.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/compute-sdk-go/11fceb8997c83b7f827666b999056e6a92e01bf8/fsthttp/fastly.toml -------------------------------------------------------------------------------- /fsthttp/handle.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package fsthttp 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "sync" 9 | ) 10 | 11 | var ( 12 | serveOnce sync.Once 13 | clientRequest *Request 14 | clientResponseWriter ResponseWriter 15 | ) 16 | 17 | // Serve calls h, providing it with a context that will be canceled when Serve 18 | // returns, a Request representing the incoming client request that initiated 19 | // this execution, and a ResponseWriter that can be used to respond to that 20 | // request. Serve will ensure the ResponseWriter has been closed before 21 | // returning, and so should only be called once per execution. 22 | func Serve(h Handler) { 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | defer cancel() 25 | 26 | serveOnce.Do(func() { 27 | var err error 28 | clientRequest, err = newClientRequest() 29 | if err != nil { 30 | panic(fmt.Errorf("create client Request: %w", err)) 31 | } 32 | clientResponseWriter, err = newResponseWriter() 33 | if err != nil { 34 | panic(fmt.Errorf("create client ResponseWriter: %w", err)) 35 | } 36 | }) 37 | 38 | h.ServeHTTP(ctx, clientResponseWriter, clientRequest) 39 | clientResponseWriter.Close() 40 | } 41 | 42 | // ServeFunc is sugar for Serve(HandlerFunc(f)). 43 | func ServeFunc(f HandlerFunc) { 44 | Serve(f) 45 | } 46 | 47 | // Handler describes anything which can handle, or respond to, an HTTP request. 48 | // It has the same semantics as net/http.Handler, but operates on the Request 49 | // and ResponseWriter types defined in this package. 50 | type Handler interface { 51 | ServeHTTP(ctx context.Context, w ResponseWriter, r *Request) 52 | } 53 | 54 | // HandlerFunc adapts a function to a Handler. 55 | type HandlerFunc func(ctx context.Context, w ResponseWriter, r *Request) 56 | 57 | // ServeHTTP implements Handler by calling f(ctx, w, r). 58 | func (f HandlerFunc) ServeHTTP(ctx context.Context, w ResponseWriter, r *Request) { 59 | f(ctx, w, r) 60 | } 61 | -------------------------------------------------------------------------------- /fsthttp/headers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package fsthttp 4 | 5 | import ( 6 | "net/textproto" 7 | ) 8 | 9 | // Header represents the key-value pairs in a set of HTTP headers. Unlike 10 | // net/http, keys are canonicalized to their lowercase form. 11 | type Header map[string][]string 12 | 13 | // NewHeader returns an initialized and empty set of headers. 14 | func NewHeader() Header { 15 | return map[string][]string{} 16 | } 17 | 18 | // Add adds the key, value pair to the headers. It appends to any existing 19 | // values associated with key. The key is case insensitive; it is canonicalized 20 | // by CanonicalHeaderKey. 21 | func (h Header) Add(key, value string) { 22 | key = CanonicalHeaderKey(key) 23 | h[key] = append(h[key], value) 24 | } 25 | 26 | // Del deletes the values associated with key. The key is case insensitive; it 27 | // is canonicalized by CanonicalHeaderKey. 28 | func (h Header) Del(key string) { 29 | key = CanonicalHeaderKey(key) 30 | delete(h, key) 31 | } 32 | 33 | // Get gets the first value associated with the given key. It is case 34 | // insensitive; CanonicalHeaderKey is used to canonicalize the provided key. If 35 | // there are no values associated with the key, Get returns "". 36 | func (h Header) Get(key string) string { 37 | key = CanonicalHeaderKey(key) 38 | if values := h[key]; len(values) > 0 { 39 | return values[0] 40 | } 41 | return "" 42 | } 43 | 44 | // Set sets the header entries associated with key to the single element value. 45 | // It replaces any existing values associated with key. The key is case 46 | // insensitive; it is canonicalized by CanonicalHeaderKey. 47 | func (h Header) Set(key, value string) { 48 | key = CanonicalHeaderKey(key) 49 | h[key] = []string{value} 50 | } 51 | 52 | // Keys returns all keys in the header collection. 53 | func (h Header) Keys() []string { 54 | keys := make([]string, 0, len(h)) 55 | for key := range h { 56 | keys = append(keys, key) 57 | } 58 | return keys 59 | } 60 | 61 | // Values returns all values associated with the given key. It is case 62 | // insensitive; CanonicalHeaderKey is used to canonicalize the provided key. The 63 | // returned slice is not a copy. 64 | func (h Header) Values(key string) []string { 65 | key = CanonicalHeaderKey(key) 66 | return h[key] 67 | } 68 | 69 | // Clone returns a copy of the headers. 70 | func (h Header) Clone() Header { 71 | clone := NewHeader() 72 | clone.Apply(h) 73 | return clone 74 | } 75 | 76 | // Reset deletes all existing headers, and adds all of the headers in hs. 77 | func (h Header) Reset(hs Header) { 78 | for key := range h { 79 | h.Del(key) 80 | } 81 | h.Apply(hs) 82 | } 83 | 84 | // Apply adds all of the headers in hs. In the case of key conflict, 85 | // values from hs totally overwrite existing values in h. 86 | func (h Header) Apply(hs Header) { 87 | for _, key := range hs.Keys() { 88 | for _, value := range hs.Values(key) { 89 | h.Add(key, value) 90 | } 91 | } 92 | } 93 | 94 | // CanonicalHeaderKey returns the canonical format of the header key s. The 95 | // canonicalization converts the first letter and any letter following a hyphen 96 | // to upper case; the rest are converted to lowercase. For example, the 97 | // canonical key for "accept-encoding" is "Accept-Encoding". If s contains a 98 | // space or invalid header field bytes, it is returned without modifications. 99 | func CanonicalHeaderKey(s string) string { 100 | return textproto.CanonicalMIMEHeaderKey(s) 101 | } 102 | -------------------------------------------------------------------------------- /fsthttp/headers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package fsthttp 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestHeaderBasics(t *testing.T) { 10 | t.Parallel() 11 | 12 | h := NewHeader() 13 | 14 | h.Add("Host", "zombo.com") 15 | if want, have := "zombo.com", h.Get("host"); want != have { 16 | t.Errorf("Host: want %q, have %q", want, have) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fsthttp/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package fsthttp 6 | 7 | import "fmt" 8 | 9 | // Error replies to the request with the specified error message and HTTP code. 10 | // It does not otherwise end the request; the caller should ensure no further 11 | // writes are done to w. 12 | // The error message should be plain text. 13 | func Error(w ResponseWriter, error string, code int) { 14 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 15 | w.Header().Set("X-Content-Type-Options", "nosniff") 16 | w.WriteHeader(code) 17 | fmt.Fprintln(w, error) 18 | } 19 | 20 | // NotFound replies to the request with an HTTP 404 not found error. 21 | func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) } 22 | -------------------------------------------------------------------------------- /fsthttp/internal.go: -------------------------------------------------------------------------------- 1 | //go:build fastlyinternaldebug 2 | 3 | package fsthttp 4 | 5 | import "github.com/fastly/compute-sdk-go/internal/abi/fastly" 6 | 7 | func (req *Request) ConstructABIRequest() error { 8 | if err := req.constructABIRequest(); err != nil { 9 | return err 10 | } 11 | 12 | return req.setABIRequestOptions() 13 | } 14 | 15 | func (req *Request) ABI() (*fastly.HTTPRequest, *fastly.HTTPBody) { 16 | return req.abi.req, req.abi.body 17 | } 18 | -------------------------------------------------------------------------------- /fsthttp/limits.go: -------------------------------------------------------------------------------- 1 | package fsthttp 2 | 3 | // Limits handles HTTP limits 4 | type Limits struct { 5 | maxHeaderNameLen int 6 | maxHeaderValueLen int 7 | maxMethodLen int 8 | maxURLLen int 9 | } 10 | 11 | // MaxHeaderNameLen gets the header name limit 12 | func (limits *Limits) MaxHeaderNameLen() int { 13 | return limits.maxHeaderNameLen 14 | } 15 | 16 | // SetMaxHeaderNameLen sets the header name limit 17 | func (limits *Limits) SetMaxHeaderNameLen(len int) { 18 | limits.maxHeaderNameLen = len 19 | } 20 | 21 | // MaxHeaderValueLen gets the header value limit 22 | func (limits *Limits) MaxHeaderValueLen() int { 23 | return limits.maxHeaderValueLen 24 | } 25 | 26 | // SetMaxHeaderValueLen sets the header value limit 27 | func (limits *Limits) SetMaxHeaderValueLen(len int) { 28 | limits.maxHeaderValueLen = len 29 | } 30 | 31 | // MaxMethodLen gets the request method limit 32 | // Deprecated: the limit is not enforced, buffer sizing is adaptive. 33 | func (limits *Limits) MaxMethodLen() int { 34 | return limits.maxMethodLen 35 | } 36 | 37 | // SetMaxMethodLen sets the request method limit 38 | // Deprecated: the limit is not reset, buffer sizing is adaptive. 39 | func (limits *Limits) SetMaxMethodLen(_ int) { 40 | } 41 | 42 | // MaxURLLen gets the request URL limit 43 | // Deprecated: the limit is not enforced, buffer sizing is adaptive. 44 | func (limits *Limits) MaxURLLen() int { 45 | return limits.maxURLLen 46 | } 47 | 48 | // SetMaxURLLen sets the request URL limit 49 | // Deprecated: the limit is not reset, buffer sizing is adaptive. 50 | func (limits *Limits) SetMaxURLLen(_ int) { 51 | } 52 | -------------------------------------------------------------------------------- /fsthttp/methods.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package fsthttp 6 | 7 | // Common HTTP methods. 8 | // 9 | // Unless otherwise noted, these are defined in RFC 7231 section 4.3. 10 | const ( 11 | MethodGet = "GET" 12 | MethodHead = "HEAD" 13 | MethodPost = "POST" 14 | MethodPut = "PUT" 15 | MethodPatch = "PATCH" // RFC 5789 16 | MethodDelete = "DELETE" 17 | MethodConnect = "CONNECT" 18 | MethodOptions = "OPTIONS" 19 | MethodTrace = "TRACE" 20 | ) 21 | -------------------------------------------------------------------------------- /fsthttp/request_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package fsthttp 4 | 5 | import "testing" 6 | 7 | // TestRequestHost validates a Host field is set on the Request type. 8 | func TestRequestHost(t *testing.T) { 9 | t.Parallel() 10 | 11 | uri := "http://example.com:8080/" 12 | want := "example.com:8080" 13 | 14 | r, err := NewRequest("GET", uri, nil) 15 | if err != nil { 16 | t.Errorf("unexpected error: %s", err) 17 | } 18 | 19 | if want, have := want, r.Host; want != have { 20 | t.Errorf("Host: want %q, have %q", want, have) 21 | } 22 | 23 | if want, have := want, r.URL.Host; want != have { 24 | t.Errorf("Host: want %q, have %q", want, have) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fsthttp/setparseuri.go: -------------------------------------------------------------------------------- 1 | //go:build fastlyinternalsetparseuri 2 | 3 | package fsthttp 4 | 5 | import ( 6 | "net/url" 7 | ) 8 | 9 | // SetParseRequestURI takes a function like url.ParseRequestURI to use when parsing incoming requests 10 | // It is an experimental interface for applications that want to relax restrictions on url parsing 11 | // It should generally not be needed, and is likely to change, so please avoid unless absolutely necessary 12 | func SetParseRequestURI(parseRequestURI func(string)(*url.URL, error)) { 13 | _parseRequestURI = parseRequestURI 14 | } 15 | -------------------------------------------------------------------------------- /fsthttp/transport.go: -------------------------------------------------------------------------------- 1 | package fsthttp 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // Transport is an http.RoundTripper implementation for backend requests 9 | // on Compute. 10 | // 11 | // Compute requests must be made to a pre-configured named backend. 12 | // Transport provides a mechanism for mapping hostnames to backend 13 | // names. A default catchall backend is set when the Transport is 14 | // created, but additional host-to-backend mappings can be added with 15 | // the AddHostBackend method. 16 | // 17 | // Transport is provided primarily to adapt existing code which uses 18 | // http.Client instances to work on Compute. Using an http.Client 19 | // pulls in substantially more code, resulting in slower compile times 20 | // and larger binaries. For this reason, we recommend new code use the 21 | // fsthttp.Request type and its Send() method directly whenever 22 | // possible. 23 | type Transport struct { 24 | defaultBackend string 25 | backends map[string]string 26 | 27 | // Request is an optional callback invoked before the request is 28 | // sent to the backend. It allows callers to set 29 | // fsthttp.Request-specific fields, such as cache control options. 30 | Request func(req *Request) error 31 | } 32 | 33 | // NewTransport creates a new Transport instance with the given default 34 | // backend. Any request made to a host not explicitly mapped to a 35 | // backend using the AddHostBackend method will be sent to the default 36 | // backend. 37 | func NewTransport(backend string) *Transport { 38 | return &Transport{ 39 | defaultBackend: backend, 40 | backends: make(map[string]string), 41 | } 42 | } 43 | 44 | // AddHostBackend adds a new host-to-backend mapping. Multiple hosts 45 | // may be mapped to the same backend. 46 | func (t *Transport) AddHostBackend(host, backend string) { 47 | t.backends[strings.ToLower(host)] = backend 48 | } 49 | 50 | func (t *Transport) getBackend(host string) string { 51 | if backend, ok := t.backends[strings.ToLower(host)]; ok { 52 | return backend 53 | } 54 | return t.defaultBackend 55 | } 56 | 57 | // RoundTrip implements the http.RoundTripper interface. 58 | // 59 | // The provided http.Request is adapted into an fsthttp.Request. If the 60 | // Transport's Request callback field is set, it is invoked so that the 61 | // fsthttp.Request can be modified before it is sent. The request is 62 | // then sent to the backend matching the host in the URL. The resulting 63 | // fsthttp.Response is adapted into an http.Response and returned. 64 | // 65 | // The http.Response's Request field contains a context from which the 66 | // original fsthttp.Request and fsthttp.Response can be extracted using 67 | // fsthttp.RequestFromContext and fsthttp.ResponseFromContext. 68 | func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { 69 | freq, err := NewRequest(req.Method, req.URL.String(), req.Body) 70 | if err != nil { 71 | return nil, err 72 | } 73 | freq.Header = Header(req.Header.Clone()) 74 | 75 | if t.Request != nil { 76 | if err := t.Request(freq); err != nil { 77 | return nil, err 78 | } 79 | } 80 | 81 | fresp, err := freq.Send(req.Context(), t.getBackend(req.URL.Host)) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | ctx := contextWithRequest(req.Context(), freq) 87 | ctx = contextWithResponse(ctx, fresp) 88 | 89 | resp := &http.Response{ 90 | Request: req.WithContext(ctx), 91 | StatusCode: fresp.StatusCode, 92 | Header: http.Header(fresp.Header.Clone()), 93 | Body: fresp.Body, 94 | } 95 | 96 | return resp, nil 97 | } 98 | -------------------------------------------------------------------------------- /fsttest/recorder.go: -------------------------------------------------------------------------------- 1 | package fsttest 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/fastly/compute-sdk-go/fsthttp" 9 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 10 | ) 11 | 12 | // ResponseRecorder is an implementation of fsthttp.ResponseWriter that 13 | // records its mutations for later inspection in tests. 14 | type ResponseRecorder struct { 15 | Code int 16 | HeaderMap fsthttp.Header 17 | Body *bytes.Buffer 18 | headersSent bool 19 | } 20 | 21 | // NewRecorder returns an initialized ResponseRecorder. 22 | func NewRecorder() *ResponseRecorder { 23 | return &ResponseRecorder{ 24 | Code: fsthttp.StatusOK, 25 | HeaderMap: make(fsthttp.Header), 26 | Body: &bytes.Buffer{}, 27 | } 28 | } 29 | 30 | // Header returns the response headers to mutate within a handler. 31 | func (r *ResponseRecorder) Header() fsthttp.Header { 32 | if !r.headersSent { 33 | return r.HeaderMap 34 | } 35 | // Once sent, return a copy so any changes are discarded. 36 | return r.HeaderMap.Clone() 37 | } 38 | 39 | // WriteHeader records the response code. 40 | func (r *ResponseRecorder) WriteHeader(code int) { 41 | if !r.headersSent { 42 | r.Code = code 43 | r.headersSent = true 44 | } 45 | } 46 | 47 | // Write records the response body. The data is written to the Body 48 | // field of the ResponseRecorder. 49 | func (r *ResponseRecorder) Write(b []byte) (int, error) { 50 | return r.Body.Write(b) 51 | } 52 | 53 | // Close is a no-op on ResponseRecorder. It exists to satisfy the 54 | // fsthttp.ResponseWriter interface. 55 | func (r *ResponseRecorder) Close() error { 56 | return nil 57 | } 58 | 59 | // SetManualFramingMode is a no-op on ResponseRecorder. It exists to 60 | // satisfy the fsthttp.ResponseWriter interface. 61 | func (r *ResponseRecorder) SetManualFramingMode(v bool) {} 62 | 63 | // Append records the response body. The data is written to the Body 64 | // field of the ResponseRecorder. 65 | func (r *ResponseRecorder) Append(other io.ReadCloser) error { 66 | // do the same type check as the real implementation 67 | _, ok := other.(*fastly.HTTPBody) 68 | if !ok { 69 | return fmt.Errorf("non-Response Body passed to ResponseWriter.Append") 70 | } 71 | // the real implementation makes a host call to do the body append 72 | // without a real body handle to write to, we'll just use io.Copy to the same 73 | // effect 74 | _, err := io.Copy(r, other) 75 | return err 76 | } 77 | -------------------------------------------------------------------------------- /geo/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package geo provides access to the geographic data for IP addresses. 4 | package geo 5 | -------------------------------------------------------------------------------- /geo/geodata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package geo 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net" 10 | 11 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 12 | ) 13 | 14 | var ( 15 | // ErrInvalidIP indicates the input IP was invalid. 16 | ErrInvalidIP = errors.New("geo: invalid IP") 17 | 18 | // ErrUnexpected indicates an unexpected error occurred. 19 | ErrUnexpected = errors.New("geo: unexpected error") 20 | ) 21 | 22 | // Geo represents the geographic data for an IP address. 23 | type Geo struct { 24 | AsName string `json:"as_name"` // The name of the organization associated with AsNumber 25 | AsNumber int `json:"as_number"` // Autonomous system (AS) number 26 | AreaCode int `json:"area_code"` // The telephone area code associated with an IP address 27 | City string `json:"city"` // City or town name 28 | ConnSpeed string `json:"conn_speed"` // Connection speed 29 | ConnType string `json:"conn_type"` // Connection type 30 | ContinentCode string `json:"continent"` // A two-character UN M.49 continent code 31 | CountryCode string `json:"country_code"` // A two-character ISO 3166-1 country code for the country associated with an IP address 32 | CountryCode3 string `json:"country_code3"` // A three-character ISO 3166-1 alpha-3 country code for the country associated with the IP address 33 | CountryName string `json:"country_name"` // Country name 34 | Latitude float64 `json:"latitude"` // Latitude, in units of degrees from the equator 35 | Longitude float64 `json:"longitude"` // Longitude, in units of degrees from the IERS Reference Meridian 36 | MetroCode int `json:"metro_code"` // Metro code, representing designated market areas (DMAs) in the United States 37 | PostalCode string `json:"postal_code"` // The postal code associated with the IP address 38 | ProxyDescription string `json:"proxy_description"` // Client proxy description 39 | ProxyType string `json:"proxy_type"` // Client proxy type 40 | Region string `json:"region"` // ISO 3166-2 country subdivision code 41 | UTCOffset int `json:"utc_offset"` // Time zone offset from coordinated universal time (UTC) for city 42 | } 43 | 44 | // Lookup returns the geographic data associated with a particular IP address. 45 | func Lookup(ip net.IP) (*Geo, error) { 46 | buf, err := fastly.GeoLookup(ip) 47 | if err != nil { 48 | status, ok := fastly.IsFastlyError(err) 49 | switch { 50 | case ok && status == fastly.FastlyStatusNone: 51 | // Viceroy <= 0.9.3 returns fastly.FastlyStatusNone when no geolocation 52 | // data is available. The Compute production environment instead returns 53 | // empty data, which is handled by falling through to code below this switch. 54 | 55 | // TODO: potential breaking change if bumping major version 56 | // return nil, ErrNotFound 57 | case ok && status == fastly.FastlyStatusInval: 58 | return nil, ErrInvalidIP 59 | case ok: 60 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 61 | default: 62 | return nil, err 63 | } 64 | } 65 | 66 | var g Geo 67 | 68 | // Check if there is geographic data for this IP address. 69 | if len(buf) == 0 { 70 | return &g, nil 71 | } 72 | 73 | if err := json.Unmarshal(buf, &g); err != nil { 74 | return nil, err 75 | } 76 | return &g, nil 77 | } 78 | -------------------------------------------------------------------------------- /geo/testdata/geo_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "as_name": "?", 3 | "as_number": 0, 4 | "area_code": 0, 5 | "city": "贵州", 6 | "conn_speed": "broadband", 7 | "conn_type": "wired", 8 | "continent": "AS", 9 | "country_code": "CN", 10 | "country_code3": "CHN", 11 | "country_name": "china", 12 | "gmt_offset": 800, 13 | "latitude": 23.13, 14 | "longitude": 113.24, 15 | "metro_code": 156196, 16 | "postal_code": "510000", 17 | "proxy_description": "?", 18 | "proxy_type": "?", 19 | "region": "GD", 20 | "utc_offset": 800 21 | } 22 | -------------------------------------------------------------------------------- /geo/testdata/geo_2.json: -------------------------------------------------------------------------------- 1 | {"as_name":"att services inc.","as_number":7018,"area_code":213,"city":"los angeles","conn_speed":"broadband","conn_type":"wired","continent":"NA","country_code":"US","country_code3":"USA","country_name":"united states","gmt_offset":-700,"latitude":34.01,"longitude":-118.26,"metro_code":803,"postal_code":"90011","proxy_description":"?","proxy_type":"?","region":"CA","utc_offset":-700} -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fastly/compute-sdk-go 2 | 3 | go 1.21 4 | 5 | retract ( 6 | v1.4.0 // Observed errors after rollout 7 | v1.4.1 // Contains retractions only 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/compute-sdk-go/11fceb8997c83b7f827666b999056e6a92e01bf8/go.sum -------------------------------------------------------------------------------- /integration_tests/_version/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://www.fastly.com/documentation/reference/compute/fastly-toml 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "print-request" 9 | 10 | [scripts] 11 | # build = "GOARCH=wasm GOOS=wasip1 go build -o bin/main.wasm ." 12 | # build = "tinygo build -target=wasip1 -gc=conservative -o ./bin/main.wasm ./" 13 | -------------------------------------------------------------------------------- /integration_tests/_version/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Fastly, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "runtime/debug" 7 | "testing" 8 | ) 9 | 10 | func TestGoVersion(t *testing.T) { 11 | bi, _ := debug.ReadBuildInfo() 12 | t.Log(bi.GoVersion) 13 | } 14 | -------------------------------------------------------------------------------- /integration_tests/acl/acls.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { "op": "update", "prefix": "1.2.3.0/24", "action": "BLOCK" }, 4 | { "op": "create", "prefix": "192.168.0.0/16", "action": "BLOCK" }, 5 | { "op": "update", "prefix": "23.23.23.23/32", "action": "ALLOW" }, 6 | { "op": "create", "prefix": "1.2.3.4/32", "action": "ALLOW" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /integration_tests/acl/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "gacls" 9 | 10 | 11 | 12 | [scripts] 13 | build = "tinygo build -target=wasip1 -o bin/main.wasm ./" 14 | 15 | 16 | [local_server] 17 | acls.example = "./acls.json" 18 | -------------------------------------------------------------------------------- /integration_tests/acl/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2023 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | "testing" 11 | 12 | "github.com/fastly/compute-sdk-go/acl" 13 | ) 14 | 15 | func TestACL(t *testing.T) { 16 | store, err := acl.Open("example") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | var tests = []struct { 22 | ip string 23 | r acl.Response 24 | err error 25 | }{ 26 | {"1.2.3.4", acl.Response{Prefix: "1.2.3.4/32", Action: "ALLOW"}, nil}, 27 | {"1.1.1.1", acl.Response{}, acl.ErrNoContent}, 28 | {"1.1.1", acl.Response{}, acl.ErrInvalidArgument}, 29 | } 30 | 31 | for _, tt := range tests { 32 | 33 | lookup, err := store.Lookup(net.ParseIP(tt.ip)) 34 | if (tt.err == nil && err != nil) || (tt.err != nil && !errors.Is(err, tt.err)) { 35 | t.Errorf("Lookup(%v) error mismatch: got %v, want %v", tt.ip, err, tt.err) 36 | continue 37 | } 38 | 39 | if lookup.Prefix != tt.r.Prefix || lookup.Action != tt.r.Action { 40 | t.Errorf("Lookup(%v) mismatch: got %#v, want %#v\n", tt.ip, lookup, tt.r) 41 | } 42 | 43 | } 44 | 45 | store, err = acl.Open("does-not-exist") 46 | if err != acl.ErrNotFound { 47 | t.Errorf("Open(does-not-exist) err = %v, want %v\n", err, acl.ErrNotFound) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /integration_tests/async_select/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "async_select" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | 18 | [local_server.backends.TheOrigin2] 19 | url = "https://compute-sdk-test-backend.edgecompute.app/" 20 | -------------------------------------------------------------------------------- /integration_tests/async_select/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "sync" 12 | "testing" 13 | 14 | "github.com/fastly/compute-sdk-go/fsthttp" 15 | "github.com/fastly/compute-sdk-go/fsttest" 16 | ) 17 | 18 | func TestAsyncSelect(t *testing.T) { 19 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 20 | type requestInfo struct { 21 | url string 22 | backend string 23 | header string 24 | } 25 | 26 | // Send several requests in parallel. 27 | var wg sync.WaitGroup 28 | for _, ri := range []requestInfo{ 29 | {"https://compute-sdk-test-backend.edgecompute.app/async_select_1", "TheOrigin", "fooname"}, 30 | {"https://compute-sdk-test-backend.edgecompute.app/async_select_2", "TheOrigin2", "barname"}, 31 | } { 32 | wg.Add(1) 33 | go func(ri requestInfo) { 34 | defer wg.Done() 35 | 36 | req, err := fsthttp.NewRequest("GET", ri.url, nil) 37 | if err != nil { 38 | t.Errorf("%s: create request: %v", ri.url, err) 39 | return 40 | } 41 | req.CacheOptions.Pass = true 42 | 43 | resp, err := req.Send(ctx, ri.backend) 44 | if err != nil { 45 | t.Errorf("%s: send request: %v", ri.url, err) 46 | return 47 | } 48 | 49 | io.Copy(io.Discard, resp.Body) 50 | resp.Body.Close() 51 | 52 | w.Header().Set(ri.header, resp.Header.Get(ri.header)) 53 | }(ri) 54 | } 55 | wg.Wait() 56 | 57 | fmt.Fprintf(w, "pong") 58 | } 59 | 60 | r, err := fsthttp.NewRequest("POST", "/hello", nil) 61 | if err != nil { 62 | t.Fatalf("NewRequest: %v", err) 63 | } 64 | w := fsttest.NewRecorder() 65 | 66 | handler(context.Background(), w, r) 67 | 68 | if got, want := w.Code, fsthttp.StatusOK; got != want { 69 | t.Errorf("Code = %d, want %d", got, want) 70 | } 71 | 72 | if got, want := w.Header().Get("FooName"), "FooValue"; got != want { 73 | t.Errorf("Header[FooName] = %q, want %q", got, want) 74 | } 75 | 76 | if got, want := w.Header().Get("BarName"), "BarValue"; got != want { 77 | t.Errorf("Header[BarName] = %q, want %q", got, want) 78 | } 79 | 80 | if got, want := w.Body.String(), "pong"; got != want { 81 | t.Errorf("Body = %q, want %q", got, want) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /integration_tests/byte_repeater/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "byte_repeater" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | -------------------------------------------------------------------------------- /integration_tests/byte_repeater/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "context" 10 | "io" 11 | "testing" 12 | 13 | "github.com/fastly/compute-sdk-go/fsthttp" 14 | "github.com/fastly/compute-sdk-go/fsttest" 15 | ) 16 | 17 | func TestByteRepeater(t *testing.T) { 18 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 19 | req, err := fsthttp.NewRequest("GET", "https://compute-sdk-test-backend.edgecompute.app/byte_repeater", nil) 20 | if err != nil { 21 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 22 | t.Errorf("NewRequest: %v", err) 23 | return 24 | } 25 | req.CacheOptions.Pass = true 26 | 27 | resp, err := req.Send(ctx, "TheOrigin") 28 | if err != nil { 29 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 30 | t.Errorf("Send: %v", err) 31 | return 32 | } 33 | 34 | br := bufio.NewReader(resp.Body) 35 | for { 36 | b, err := br.ReadByte() 37 | switch { 38 | case err == nil: // normal case 39 | w.Write([]byte{b, b}) 40 | case err == io.EOF: // done 41 | return 42 | case err != nil: // error 43 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 44 | t.Errorf("ReadByte: %v", err) 45 | return 46 | } 47 | } 48 | } 49 | 50 | r, err := fsthttp.NewRequest("GET", "/", nil) 51 | if err != nil { 52 | t.Fatalf("NewRequest: %v", err) 53 | } 54 | w := fsttest.NewRecorder() 55 | 56 | handler(context.Background(), w, r) 57 | 58 | if got, want := w.Code, fsthttp.StatusOK; got != want { 59 | t.Errorf("Code = %v; want %v", got, want) 60 | } 61 | 62 | if got, want := w.Body.String(), "112233445566778899001122\n\n"; got != want { 63 | t.Errorf("Body = %q; want %q", got, want) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /integration_tests/compute_runtime/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "compute_runtime" 9 | service_id = "" 10 | 11 | [local_server] 12 | -------------------------------------------------------------------------------- /integration_tests/compute_runtime/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2024 Fastly, Inc. 4 | package main 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/fastly/compute-sdk-go/compute" 11 | ) 12 | 13 | func TestGetVcpuMs(t *testing.T) { 14 | start, err := compute.GetVCPUTime() 15 | if err != nil { 16 | t.Errorf("Couldn't get starting vcpu time") 17 | } 18 | 19 | time.Sleep(1 * time.Second) 20 | 21 | end, err := compute.GetVCPUTime() 22 | if err != nil { 23 | t.Errorf("Couldn't get ending vcpu time") 24 | } 25 | 26 | if end - start > (200 * time.Millisecond) { 27 | t.Errorf("Sleeping shouldn't count as vcpu time!") 28 | } 29 | 30 | now, err := compute.GetVCPUTime() 31 | if err != nil { 32 | t.Errorf("Couldn't get starting vcpu time (part 2)") 33 | } 34 | 35 | var counter uint64 36 | 37 | counter = 0 38 | next := now 39 | for now == next { 40 | new_next, err := compute.GetVCPUTime() 41 | if err != nil { 42 | t.Errorf("Couldn't get part 2's recheck of vcpu time") 43 | } 44 | next = new_next 45 | counter += 1 46 | } 47 | 48 | if counter == 0 { 49 | t.Errorf("It should take at least one loop to advance vcpu time") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /integration_tests/config_store/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "configstore" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | 18 | [local_server.backends.TheOrigin2] 19 | url = "https://compute-sdk-test-backend.edgecompute.app/" 20 | 21 | [local_server.backends.example_backend] 22 | url = "https://example.org/" 23 | 24 | [local_server.dictionaries] 25 | 26 | [local_server.dictionaries.configstore] 27 | file = "configstore.json" 28 | format = "json" 29 | -------------------------------------------------------------------------------- /integration_tests/device_detection/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "geolocation" 9 | service_id = "" 10 | 11 | 12 | [local_server] 13 | [local_server.device_detection] 14 | format = "inline-toml" 15 | 16 | [local_server.device_detection.user_agents] 17 | [local_server.device_detection.user_agents."Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]"] 18 | device = {name = "iPhone", brand = "Apple", model = "iPhone4,1", hwtype = "Mobile Phone", is_ereader = false, is_gameconsole = false, is_mediaplayer = false, is_mobile = true, is_smarttv = false, is_tablet = false, is_tvplayer = false, is_desktop = false, is_touchscreen = true} 19 | 20 | [local_server.device_detection.user_agents."ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)"] 21 | device = {name = "Asus TeK", brand = "Asus", model = "TeK", is_desktop = false} 22 | -------------------------------------------------------------------------------- /integration_tests/device_detection/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2023 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "testing" 11 | 12 | "github.com/fastly/compute-sdk-go/device" 13 | "github.com/fastly/compute-sdk-go/fsthttp" 14 | "github.com/fastly/compute-sdk-go/fsttest" 15 | ) 16 | 17 | func assert[T comparable](res fsthttp.ResponseWriter, field string, got, want T) { 18 | if got != want { 19 | fsthttp.Error(res, fmt.Sprintf("%s: got %v, want %v", field, got, want), fsthttp.StatusInternalServerError) 20 | } 21 | } 22 | 23 | func TestDeviceDetection(t *testing.T) { 24 | handler := func(ctx context.Context, res fsthttp.ResponseWriter, req *fsthttp.Request) { 25 | d, err := device.Lookup(req.Header.Get("User-Agent")) 26 | 27 | switch req.URL.Path { 28 | case "/iPhone": 29 | if err != nil { 30 | fsthttp.Error(res, err.Error(), fsthttp.StatusInternalServerError) 31 | return 32 | } 33 | 34 | assert(res, "Name", d.Name(), "iPhone") 35 | assert(res, "Brand", d.Brand(), "Apple") 36 | assert(res, "Model", d.Model(), "iPhone4,1") 37 | assert(res, "HWType", d.HWType(), "Mobile Phone") 38 | assert(res, "IsMobile", d.IsMobile(), true) 39 | assert(res, "IsTouchscreen", d.IsTouchscreen(), true) 40 | 41 | case "/AsusTeK": 42 | if err != nil { 43 | fsthttp.Error(res, err.Error(), fsthttp.StatusInternalServerError) 44 | return 45 | } 46 | 47 | assert(res, "Name", d.Name(), "Asus TeK") 48 | assert(res, "Brand", d.Brand(), "Asus") 49 | assert(res, "Model", d.Model(), "TeK") 50 | 51 | case "/unknown": 52 | if err != device.ErrDeviceNotFound { 53 | fsthttp.Error(res, err.Error(), fsthttp.StatusInternalServerError) 54 | return 55 | } 56 | 57 | default: 58 | fsthttp.Error(res, "not found", fsthttp.StatusNotFound) 59 | } 60 | } 61 | 62 | testcases := []struct { 63 | name string 64 | userAgent string 65 | }{ 66 | { 67 | name: "iPhone", 68 | userAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]", 69 | }, 70 | 71 | { 72 | name: "AsusTeK", 73 | userAgent: "ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)", 74 | }, 75 | 76 | { 77 | name: "unknown", 78 | userAgent: "whoopty doopty doo", 79 | }, 80 | } 81 | 82 | for _, tc := range testcases { 83 | t.Run(tc.name, func(t *testing.T) { 84 | r, err := fsthttp.NewRequest("GET", "/"+tc.name, nil) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | r.Header.Set("User-Agent", tc.userAgent) 89 | w := fsttest.NewRecorder() 90 | 91 | handler(context.Background(), w, r) 92 | 93 | if got, want := w.Code, fsthttp.StatusOK; got != want { 94 | t.Errorf("got %v, want %v", got, want) 95 | t.Error(w.Body.String()) 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /integration_tests/dynamic_backend/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "dynamic_backend" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | -------------------------------------------------------------------------------- /integration_tests/dynamic_backend/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "testing" 12 | 13 | "github.com/fastly/compute-sdk-go/fsthttp" 14 | "github.com/fastly/compute-sdk-go/fsttest" 15 | ) 16 | 17 | func NewBackendOptions() *fsthttp.BackendOptions { 18 | return &fsthttp.BackendOptions{} 19 | } 20 | 21 | func TestDynamicBackend(t *testing.T) { 22 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 23 | b, err := fsthttp.RegisterDynamicBackend( 24 | "dynamic", 25 | "compute-sdk-test-backend.edgecompute.app", 26 | NewBackendOptions().UseSSL(true), 27 | ) 28 | if err != nil { 29 | t.Errorf("RegisterDynamicBackend: %v", err) 30 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 31 | return 32 | } 33 | 34 | if !b.IsDynamic() { 35 | t.Errorf("IsDynamic() = false, want true") 36 | fsthttp.Error(w, "IsDynamic() = false, want true", fsthttp.StatusInternalServerError) 37 | return 38 | } 39 | 40 | if !b.IsSSL() { 41 | t.Errorf("IsSSL() = false, want true") 42 | fsthttp.Error(w, "IsSSL() = false, want true", fsthttp.StatusInternalServerError) 43 | return 44 | } 45 | 46 | health, err := b.Health() 47 | if err != nil { 48 | t.Errorf("Health: %v", err) 49 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 50 | return 51 | } 52 | 53 | // Viceroy doesn't support health checks, so the status will always be unknown 54 | if health != fsthttp.BackendHealthUnknown { 55 | t.Errorf("Health = %v, want %v", health, fsthttp.BackendHealthUnknown) 56 | fsthttp.Error(w, fmt.Sprintf("Health = %v, want %v", health, fsthttp.BackendHealthUnknown), fsthttp.StatusInternalServerError) 57 | return 58 | } 59 | 60 | req, err := fsthttp.NewRequest("GET", "https://compute-sdk-test-backend.edgecompute.app/", nil) 61 | if err != nil { 62 | t.Errorf("NewRequest: %v", err) 63 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 64 | return 65 | } 66 | 67 | req.CacheOptions.Pass = true 68 | 69 | // Send to our newly-registered dynamic backend 70 | resp, err := req.Send(ctx, "dynamic") 71 | if err != nil { 72 | t.Errorf("Send: %v", err) 73 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 74 | return 75 | } 76 | 77 | w.Header().Reset(resp.Header.Clone()) 78 | w.WriteHeader(resp.StatusCode) 79 | if _, err := io.Copy(w, resp.Body); err != nil { 80 | t.Errorf("Copy: %v", err) 81 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 82 | return 83 | } 84 | } 85 | 86 | r, err := fsthttp.NewRequest("GET", "/", nil) 87 | if err != nil { 88 | t.Fatalf("NewRequest: %v", err) 89 | } 90 | w := fsttest.NewRecorder() 91 | 92 | handler(context.Background(), w, r) 93 | 94 | if got, want := w.Code, fsthttp.StatusOK; got != want { 95 | t.Errorf("Code = %d, want %d", got, want) 96 | } 97 | 98 | if got, want := w.Body.String(), "Compute SDK Test Backend"; got != want { 99 | t.Errorf("Body = %q, want %q", got, want) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /integration_tests/geolocation/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "geolocation" 9 | service_id = "" 10 | 11 | [local_server] 12 | [local_server.geolocation] 13 | use_default_loopback = false 14 | format = "inline-toml" 15 | 16 | [local_server.geolocation.addresses] 17 | [local_server.geolocation.addresses."127.0.0.1"] 18 | as_name = "Fastly Test" 19 | as_number = 12345 20 | area_code = 123 21 | city = "Test City" 22 | conn_speed = "broadband" 23 | conn_type = "wired" 24 | continent = "NA" 25 | country_code = "CA" 26 | country_code3 = "CAN" 27 | country_name = "Canada" 28 | latitude = 12.345 29 | longitude = 54.321 30 | metro_code = 1 31 | postal_code = "12345" 32 | proxy_description = "?" 33 | proxy_type = "?" 34 | region = "BC" 35 | utc_offset = -700 36 | -------------------------------------------------------------------------------- /integration_tests/geolocation/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2023 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/fastly/compute-sdk-go/geo" 12 | ) 13 | 14 | func assert[T comparable](t *testing.T, field string, got, want T) { 15 | t.Helper() 16 | if got != want { 17 | t.Errorf("%s: got %v, want %v", field, got, want) 18 | } 19 | } 20 | 21 | func TestGeolocation(t *testing.T) { 22 | t.Run("found", func(t *testing.T) { 23 | g, err := geo.Lookup(net.ParseIP("127.0.0.1")) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | assert(t, "AsName", g.AsName, "Fastly Test") 29 | assert(t, "AsNumber", g.AsNumber, 12345) 30 | assert(t, "AreaCode", g.AreaCode, 123) 31 | assert(t, "City", g.City, "Test City") 32 | assert(t, "ConnSpeed", g.ConnSpeed, "broadband") 33 | assert(t, "ConnType", g.ConnType, "wired") 34 | assert(t, "ContinentCode", g.ContinentCode, "NA") 35 | assert(t, "CountryCode", g.CountryCode, "CA") 36 | assert(t, "CountryCode3", g.CountryCode3, "CAN") 37 | assert(t, "CountryName", g.CountryName, "Canada") 38 | assert(t, "Latitude", g.Latitude, 12.345) 39 | assert(t, "Longitude", g.Longitude, 54.321) 40 | assert(t, "MetroCode", g.MetroCode, 1) 41 | assert(t, "PostalCode", g.PostalCode, "12345") 42 | assert(t, "ProxyDescription", g.ProxyDescription, "?") 43 | assert(t, "ProxyType", g.ProxyType, "?") 44 | assert(t, "Region", g.Region, "BC") 45 | assert(t, "UTCOffset", g.UTCOffset, -700) 46 | }) 47 | 48 | t.Run("not found", func(t *testing.T) { 49 | g, err := geo.Lookup(net.ParseIP("127.0.0.9")) 50 | assert(t, "Geo{}", *g, geo.Geo{}) 51 | assert(t, "err", err, nil) 52 | }) 53 | 54 | t.Run("invalid", func(t *testing.T) { 55 | g, err := geo.Lookup(net.IP("127.0.0.9")) // Note: don't do this, net.IP is actually []byte() 56 | assert(t, "Geo{}", g, nil) 57 | assert(t, "err", err, geo.ErrInvalidIP) 58 | }) 59 | } 60 | 61 | func BenchmarkGeo(b *testing.B) { 62 | ip := net.ParseIP("127.0.0.1") 63 | var ( 64 | g *geo.Geo 65 | err error 66 | ) 67 | 68 | b.ReportAllocs() 69 | b.ResetTimer() 70 | for i := 0; i < b.N; i++ { 71 | g, err = geo.Lookup(ip) 72 | if err != nil { 73 | b.Fatal(err) 74 | } 75 | } 76 | if g.AsName != "Fastly Test" { 77 | b.Fatalf("AsName: got %v, want %v", g.AsName, "Fastly Test") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /integration_tests/hello_world/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "hello_world" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | -------------------------------------------------------------------------------- /integration_tests/hello_world/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "testing" 11 | 12 | "github.com/fastly/compute-sdk-go/fsthttp" 13 | "github.com/fastly/compute-sdk-go/fsttest" 14 | ) 15 | 16 | func TestHelloWorld(t *testing.T) { 17 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 18 | fmt.Fprintf(w, "Hello, TinyGo!") 19 | } 20 | 21 | r, err := fsthttp.NewRequest("GET", "/", nil) 22 | if err != nil { 23 | t.Fatalf("NewRequest: %v", err) 24 | } 25 | w := fsttest.NewRecorder() 26 | 27 | handler(context.Background(), w, r) 28 | 29 | if got, want := w.Code, fsthttp.StatusOK; got != want { 30 | t.Errorf("Code = %d, want %d", got, want) 31 | } 32 | 33 | if got, want := w.Body.String(), "Hello, TinyGo!"; got != want { 34 | t.Errorf("Body = %q, want %q", got, want) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /integration_tests/internal_debug/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "internal_debug" 9 | service_id = "" 10 | 11 | 12 | [local_server.backends.httpme] 13 | url = "https://http-me.glitch.me" 14 | -------------------------------------------------------------------------------- /integration_tests/internal_debug/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "io" 11 | "math/rand" 12 | "strconv" 13 | "testing" 14 | 15 | "github.com/fastly/compute-sdk-go/fsthttp" 16 | ) 17 | 18 | const backend = "httpme" 19 | 20 | func TestInternalDebug(t *testing.T) { 21 | 22 | uri := "https://http-me.glitch.me/anything/" + strconv.Itoa(rand.Int()) + "/" 23 | req, err := fsthttp.NewRequest("GET", uri, nil) 24 | if err != nil { 25 | t.Errorf("error during NewRequest: uri=%v err=%v", uri, err) 26 | return 27 | } 28 | 29 | ctx := context.Background() 30 | 31 | req.Header.Add("foobar", "quxzot") 32 | 33 | req.ConstructABIRequest() 34 | 35 | resp, err := req.Send(ctx, backend) 36 | if err != nil { 37 | t.Errorf("error during Send: %v", err) 38 | return 39 | } 40 | 41 | body, err := io.ReadAll(resp.Body) 42 | if err != nil { 43 | t.Errorf("readall(body)=%v, want %v", err, nil) 44 | return 45 | } 46 | 47 | if !bytes.Contains(body, []byte(`"foobar": "quxzot",`)) { 48 | t.Errorf("body missing foobar/quzot header: got %v", string(body)) 49 | return 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /integration_tests/kvstore/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["jchampion@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 3 8 | name = "kvstore" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.kv_stores] 14 | 15 | [[local_server.kv_stores.example-test-kv-store]] 16 | key = "hello" 17 | data = "world" 18 | -------------------------------------------------------------------------------- /integration_tests/kvstore/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2023 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | 11 | "github.com/fastly/compute-sdk-go/kvstore" 12 | ) 13 | 14 | func TestKVStore(t *testing.T) { 15 | store, err := kvstore.Open("example-test-kv-store") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | hello, err := store.Lookup("hello") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if got, want := hello.String(), "world"; got != want { 26 | t.Errorf("Lookup: got %q, want %q", got, want) 27 | } 28 | 29 | _, err = store.Lookup("animal") 30 | if err == nil { 31 | t.Error("expected Lookup failure before insert") 32 | } 33 | 34 | err = store.Insert("animal", strings.NewReader("cat")) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | animal, err := store.Lookup("animal") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if got, want := animal.String(), "cat"; got != want { 45 | t.Errorf("Insert: got %q, want %q", got, want) 46 | } 47 | 48 | if err = store.Delete("animal"); err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | _, err = store.Lookup("animal") 53 | if err == nil { 54 | t.Error("expected Lookup failure after delete") 55 | } 56 | 57 | /* 58 | // TODO(athomason) address inconsistent behavior in viceroy and production 59 | if err = store.Delete("nonexistent"); err != nil { 60 | t.Fatal(err) 61 | } 62 | */ 63 | } 64 | -------------------------------------------------------------------------------- /integration_tests/request_downstream/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "request_downstream" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | 18 | [local_server.backends.TheOrigin2] 19 | url = "https://compute-sdk-test-backend.edgecompute.app/" 20 | -------------------------------------------------------------------------------- /integration_tests/request_downstream/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/fastly/compute-sdk-go/fsthttp" 15 | "github.com/fastly/compute-sdk-go/fsttest" 16 | ) 17 | 18 | func TestDownstreamRequest(t *testing.T) { 19 | // This uses fsthttp.ServeFunc() to test an incoming request. 20 | // Viceroy constructs a simple GET http://example.com request with 21 | // the remote address being 127.0.0.1, so that's what we check for 22 | // here. 23 | fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 24 | if r.Method != "GET" { 25 | t.Errorf("Method = %s, want GET", r.Method) 26 | return 27 | } 28 | 29 | url := "http://example.com/" 30 | if r.URL.String() != url { 31 | t.Errorf("URL = %s, want %s", r.URL.String(), url) 32 | return 33 | } 34 | 35 | localhost := "127.0.0.1" 36 | 37 | if r.RemoteAddr != localhost { 38 | t.Errorf("RemoteAddr = %s, want %s", r.RemoteAddr, localhost) 39 | return 40 | } 41 | 42 | if r.ServerAddr != localhost { 43 | t.Errorf("ServerAddr = %s, want %s", r.ServerAddr, localhost) 44 | return 45 | } 46 | }) 47 | } 48 | 49 | func TestDownstreamResponse(t *testing.T) { 50 | // In this test we construct our own request and response recorder 51 | // to test that the headers and body on the response are sent 52 | // properly. 53 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 54 | if r.Method != "POST" { 55 | fsthttp.Error(w, fmt.Sprintf("Method = %s, want POST", r.Method), fsthttp.StatusMethodNotAllowed) 56 | return 57 | } 58 | 59 | w.Header().Apply(r.Header.Clone()) 60 | io.Copy(w, r.Body) 61 | } 62 | 63 | const body = "downstream requeest!" 64 | r, err := fsthttp.NewRequest("POST", "/", strings.NewReader(body)) 65 | if err != nil { 66 | t.Fatalf("NewRequest: %v", err) 67 | } 68 | 69 | r.Header.Set("DownstreamName", "DownstreamValue") 70 | 71 | w := fsttest.NewRecorder() 72 | 73 | handler(context.Background(), w, r) 74 | 75 | if got, want := w.Code, fsthttp.StatusOK; got != want { 76 | t.Errorf("Code = %d, want %d", got, want) 77 | } 78 | 79 | if got, want := w.Header().Get("DownstreamName"), "DownstreamValue"; got != want { 80 | t.Errorf("Header[DownstreamName] = %q, want %q", got, want) 81 | } 82 | 83 | if got, want := w.Body.String(), body; got != want { 84 | t.Errorf("Body = %q, want %q", got, want) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /integration_tests/request_upstream/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "request_upstream" 9 | service_id = "" 10 | 11 | 12 | [local_server.backends.TheOrigin] 13 | url = "https://compute-sdk-test-backend.edgecompute.app/" 14 | 15 | [local_server.backends.TheOrigin2] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | 18 | [local_server.backends.example_backend] 19 | url = "https://example.org/" 20 | 21 | [local_server.backends.httpedge] 22 | url = "https://http.edgecompute.app/anything/" 23 | -------------------------------------------------------------------------------- /integration_tests/request_upstream/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "encoding/json" 11 | "io" 12 | "strconv" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/fastly/compute-sdk-go/fsthttp" 17 | "github.com/fastly/compute-sdk-go/fsttest" 18 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 19 | ) 20 | 21 | func TestRequestUpstream(t *testing.T) { 22 | t.Run("useAppend=false", func(t *testing.T) { requestUpstream(false, t) }) 23 | t.Run("useAppend=true", func(t *testing.T) { requestUpstream(true, t) }) 24 | } 25 | 26 | func requestUpstream(useAppend bool, t *testing.T) { 27 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, _ *fsthttp.Request) { 28 | // Create our upstream request 29 | req, err := fsthttp.NewRequest("GET", "https://compute-sdk-test-backend.edgecompute.app/request_upstream", nil) 30 | if err != nil { 31 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 32 | t.Errorf("NewRequest: %v", err) 33 | return 34 | } 35 | 36 | req.Header.Set("UpstreamHeader", "UpstreamValue") 37 | 38 | // Make sure the response isn't cached. 39 | req.CacheOptions.Pass = true 40 | 41 | // This requires your service to be configured with a backend 42 | // named "TheOrigin" and pointing to "http://provider.org/TheURL". 43 | resp, err := req.Send(ctx, "TheOrigin") 44 | if err != nil { 45 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 46 | t.Errorf("Send: %v", err) 47 | return 48 | } 49 | 50 | w.Header().Reset(resp.Header.Clone()) 51 | w.WriteHeader(resp.StatusCode) 52 | if useAppend { 53 | w.Append(resp.Body) 54 | } else { 55 | io.Copy(w, resp.Body) 56 | } 57 | } 58 | 59 | r, err := fsthttp.NewRequest("GET", "/", nil) 60 | if err != nil { 61 | t.Fatalf("NewRequest: %v", err) 62 | } 63 | 64 | w := fsttest.NewRecorder() 65 | 66 | handler(context.Background(), w, r) 67 | 68 | if got, want := w.Code, fsthttp.StatusOK; got != want { 69 | t.Errorf("Code = %d, want %d", got, want) 70 | } 71 | 72 | if got, want := w.Header().Get("OriginHeader"), "OriginValue"; got != want { 73 | t.Errorf("Header[OriginHeader] = %q, want %q", got, want) 74 | } 75 | 76 | if got, want := w.Header().Get("x-cat"), "meow, nyan, mrrow, miau"; got != want { 77 | t.Errorf("Header[x-cat] = %q, want %q", got, want) 78 | } 79 | 80 | if got, want := w.Body.String(), "Hello from Origin"; got != want { 81 | t.Errorf("Body = %q, want %q", got, want) 82 | } 83 | } 84 | 85 | const bodySize = 64 * 1024 86 | 87 | func TestRequestUpstreamBody(t *testing.T) { 88 | body := make([]byte, bodySize) 89 | for i := range body { 90 | body[i] = 'A' 91 | } 92 | 93 | b, err := fastly.NewHTTPBody() 94 | if err != nil { 95 | t.Fatalf("NewHTTPBody: %v", err) 96 | } 97 | _, err = b.Write(body) 98 | if err != nil { 99 | t.Fatalf("Write: %v", err) 100 | } 101 | if err := b.Close(); err != nil { 102 | t.Fatalf("Close: %v", err) 103 | } 104 | 105 | testcases := []struct { 106 | name string 107 | body io.Reader 108 | size int 109 | chunked bool 110 | }{ 111 | {name: "nil", body: nil}, 112 | {name: "bytes.Reader", body: bytes.NewReader(body), size: bodySize}, 113 | {name: "bytes.Buffer", body: bytes.NewBuffer(body), size: bodySize}, 114 | {name: "strings.Reader", body: strings.NewReader(string(body)), size: bodySize}, 115 | {name: "io.NopCloser", body: io.NopCloser(bytes.NewReader(body)), chunked: true}, 116 | {name: "fastly.HTTPBody", body: b, chunked: true}, 117 | } 118 | 119 | for _, tc := range testcases { 120 | t.Run(tc.name, func(t *testing.T) { 121 | requestUpstreamBody(t, tc.body, tc.size, tc.chunked) 122 | }) 123 | } 124 | } 125 | 126 | func requestUpstreamBody(t *testing.T, body io.Reader, size int, chunked bool) { 127 | req, err := fsthttp.NewRequest("POST", "https://http.edgecompute.app/anything/", body) 128 | if err != nil { 129 | t.Fatalf("NewRequest: %v", err) 130 | } 131 | 132 | req.Header.Set("Content-Type", "application/octet-stream") 133 | req.CacheOptions.Pass = true 134 | 135 | resp, err := req.Send(context.Background(), "httpedge") 136 | if err != nil { 137 | t.Fatalf("Send: %v", err) 138 | } 139 | defer resp.Body.Close() 140 | 141 | var respData struct { 142 | Headers map[string]string `json:"headers"` 143 | } 144 | 145 | gotBody := new(bytes.Buffer) 146 | if err := json.NewDecoder(io.TeeReader(resp.Body, gotBody)).Decode(&respData); err != nil { 147 | t.Fatalf("Decode: %v\nBody:\n%s", err, gotBody.String()) 148 | } 149 | 150 | var teWant, clWant string 151 | if chunked { 152 | teWant = "chunked" 153 | } else { 154 | clWant = strconv.Itoa(size) 155 | } 156 | 157 | if got, want := respData.Headers["transfer-encoding"], teWant; got != want { 158 | t.Errorf("Header[transfer-encoding] = %q, want %q", got, want) 159 | } 160 | if got, want := respData.Headers["content-length"], clWant; got != want { 161 | t.Errorf("Header[content-length] = %q, want %q", got, want) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /integration_tests/secret_store/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "go" 7 | manifest_version = 2 8 | name = "secret_store" 9 | service_id = "" 10 | 11 | [local_server] 12 | secret_stores.phrases = [{key = "my_phrase", data = "sssh! don't tell anyone!"}] 13 | -------------------------------------------------------------------------------- /integration_tests/secret_store/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | package main 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/fastly/compute-sdk-go/secretstore" 9 | ) 10 | 11 | func TestSecretStore(t *testing.T) { 12 | v, err := secretstore.Plaintext("phrases", "my_phrase") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | if got, want := string(v), "sssh! don't tell anyone!"; got != want { 18 | t.Errorf("got %q, want %q", got, want) 19 | } 20 | } 21 | 22 | func TestSecretFromBytes(t *testing.T) { 23 | const plaintext = "not a real secret" 24 | 25 | s, err := secretstore.SecretFromBytes([]byte(plaintext)) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | v, err := s.Plaintext() 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | if got, want := string(v), plaintext; got != want { 36 | t.Errorf("got %q, want %q", got, want) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /integration_tests/status/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "status" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | 18 | [local_server.backends.TheOrigin2] 19 | url = "https://compute-sdk-test-backend.edgecompute.app/" 20 | 21 | [local_server.backends.example_backend] 22 | url = "https://example.org/" 23 | -------------------------------------------------------------------------------- /integration_tests/status/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | 11 | "github.com/fastly/compute-sdk-go/fsthttp" 12 | "github.com/fastly/compute-sdk-go/fsttest" 13 | ) 14 | 15 | func TestStatus(t *testing.T) { 16 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 17 | fsthttp.Error(w, "Unauthorized", fsthttp.StatusUnauthorized) 18 | } 19 | 20 | r, err := fsthttp.NewRequest("GET", "/", nil) 21 | if err != nil { 22 | t.Fatalf("NewRequest: %v", err) 23 | } 24 | 25 | w := fsttest.NewRecorder() 26 | 27 | handler(context.Background(), w, r) 28 | 29 | if got, want := w.Code, fsthttp.StatusUnauthorized; got != want { 30 | t.Errorf("Code = %d, want %d", got, want) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /integration_tests/streaming_close/fastly.toml: -------------------------------------------------------------------------------- 1 | # This file describes a Fastly Compute package. To learn more visit: 2 | # https://developer.fastly.com/reference/fastly-toml/ 3 | 4 | authors = ["oss@fastly.com"] 5 | description = "" 6 | language = "other" 7 | manifest_version = 2 8 | name = "streaming_close" 9 | service_id = "" 10 | 11 | [local_server] 12 | 13 | [local_server.backends] 14 | 15 | [local_server.backends.TheOrigin] 16 | url = "https://compute-sdk-test-backend.edgecompute.app/" 17 | 18 | [local_server.backends.TheOrigin2] 19 | url = "https://compute-sdk-test-backend.edgecompute.app/" 20 | 21 | [local_server.backends.example_backend] 22 | url = "https://example.org/" 23 | -------------------------------------------------------------------------------- /integration_tests/streaming_close/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "context" 10 | "io" 11 | "strconv" 12 | "testing" 13 | 14 | "github.com/fastly/compute-sdk-go/fsthttp" 15 | "github.com/fastly/compute-sdk-go/fsttest" 16 | ) 17 | 18 | func isVowel(b byte) bool { 19 | switch b { 20 | case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U': 21 | return true 22 | } 23 | return false 24 | } 25 | 26 | func TestStreamingClose(t *testing.T) { 27 | handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { 28 | req, err := fsthttp.NewRequest("GET", "https://compute-sdk-test-backend.edgecompute.app/streaming_close", nil) 29 | if err != nil { 30 | fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) 31 | return 32 | } 33 | 34 | req.CacheOptions.Pass = true 35 | resp, err := req.Send(ctx, "TheOrigin") 36 | if err != nil { 37 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 38 | return 39 | } 40 | 41 | var removed int 42 | 43 | br := bufio.NewReader(resp.Body) 44 | for { 45 | b, err := br.ReadByte() 46 | if err == io.EOF { 47 | break 48 | } 49 | 50 | if err != nil { 51 | fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) 52 | t.Errorf("ReadByte: %v", err) 53 | return 54 | } 55 | 56 | if isVowel(b) { 57 | removed++ 58 | } else { 59 | w.Write([]byte{b}) 60 | } 61 | } 62 | 63 | w.Close() 64 | 65 | req2, err := fsthttp.NewRequest("GET", "https://compute-sdk-test-backend.edgecompute.app", nil) 66 | if err != nil { 67 | t.Errorf("NewRequest: %v", err) 68 | return 69 | } 70 | 71 | req2.Header.Set("Vowels-Removed", strconv.Itoa(removed)) 72 | if _, err = req2.Send(ctx, "TheOrigin2"); err != nil { 73 | t.Errorf("Send: %v", err) 74 | return 75 | } 76 | } 77 | 78 | r, err := fsthttp.NewRequest("GET", "/", nil) 79 | if err != nil { 80 | t.Fatalf("NewRequest: %v", err) 81 | } 82 | 83 | w := fsttest.NewRecorder() 84 | 85 | handler(context.Background(), w, r) 86 | 87 | if got, want := w.Code, fsthttp.StatusOK; got != want { 88 | t.Errorf("Code = %d, want %d", got, want) 89 | } 90 | 91 | if got, want := w.Body.String(), "wll smth\n"; got != want { 92 | t.Errorf("Body = %q, want %q", got, want) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/abi/fastly/acl_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2024 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import ( 8 | "io" 9 | "net" 10 | 11 | "github.com/fastly/compute-sdk-go/internal/abi/prim" 12 | ) 13 | 14 | // witx: 15 | // 16 | // (@interface func (export "open") 17 | // (param $name string) 18 | // (result $err (expected $acl_handle (error $fastly_status))) 19 | // ) 20 | 21 | //go:wasmimport fastly_acl open 22 | //go:noescape 23 | func fastlyACLOpen( 24 | nameData prim.Pointer[prim.U8], nameLen prim.Usize, 25 | h prim.Pointer[aclHandle], 26 | ) FastlyStatus 27 | 28 | // ACL is a handle to the ACL subsystem. 29 | type ACLHandle struct { 30 | h aclHandle 31 | } 32 | 33 | // OpenACL returns a handle to the named ACL set. 34 | func OpenACL(name string) (*ACLHandle, error) { 35 | var acl ACLHandle 36 | 37 | nameBuffer := prim.NewReadBufferFromString(name).Wstring() 38 | 39 | if err := fastlyACLOpen( 40 | nameBuffer.Data, nameBuffer.Len, 41 | prim.ToPointer(&acl.h), 42 | ).toError(); err != nil { 43 | return nil, err 44 | } 45 | 46 | return &acl, nil 47 | } 48 | 49 | // witx: 50 | // 51 | // (@interface func (export "lookup") 52 | // (param $acl $acl_handle) 53 | // (param $ip_octets (@witx const_pointer (@witx char8))) 54 | // (param $ip_len (@witx usize)) 55 | // (param $body_handle_out (@witx pointer $body_handle)) 56 | // (param $acl_error_out (@witx pointer $acl_error)) 57 | // (result $err (expected (error $fastly_status))) 58 | // ) 59 | 60 | //go:wasmimport fastly_acl lookup 61 | //go:noescape 62 | func fastlyACLLookup( 63 | h aclHandle, 64 | ipData prim.Pointer[prim.U8], ipLen prim.Usize, 65 | b prim.Pointer[bodyHandle], 66 | aclErr prim.Pointer[ACLError], 67 | ) FastlyStatus 68 | 69 | // Lookup returns the entry for the IP, if it exists. 70 | func (a *ACLHandle) Lookup(ip net.IP) (io.Reader, error) { 71 | body := HTTPBody{h: invalidBodyHandle} 72 | 73 | var ipBytes []byte 74 | if ipBytes = ip.To4(); ipBytes == nil { 75 | ipBytes = ip.To16() 76 | } 77 | ipBuffer := prim.NewReadBufferFromBytes(ipBytes).ArrayChar8() 78 | 79 | var aclErr ACLError = ACLErrorUninitialized 80 | 81 | if err := fastlyACLLookup( 82 | a.h, 83 | ipBuffer.Data, ipBuffer.Len, 84 | prim.ToPointer(&body.h), 85 | prim.ToPointer(&aclErr), 86 | ).toError(); err != nil { 87 | return nil, err 88 | } 89 | 90 | if aclErr != ACLErrorOK { 91 | return nil, aclErr 92 | } 93 | 94 | // Didn't get a valid handle back. This means there was no matching 95 | // ACL prefix. Report back to caller we got no match. 96 | if body.h == invalidBodyHandle { 97 | return nil, ACLErrorNoContent 98 | } 99 | 100 | return &body, nil 101 | } 102 | -------------------------------------------------------------------------------- /internal/abi/fastly/compute_runtime_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2024 Fastly, Inc. 4 | // 5 | package fastly 6 | 7 | import ( 8 | "github.com/fastly/compute-sdk-go/internal/abi/prim" 9 | ) 10 | 11 | // witx: 12 | // (module $fastly_compute_runtime 13 | // 14 | // (@interface func (export "get_vcpu_ms") 15 | // (result $err (expected $vcpu_ms (error $fastly_status))) 16 | // ) 17 | // 18 | // ) 19 | // 20 | //go:wasmimport fastly_compute_runtime get_vcpu_ms 21 | //go:noescape 22 | func fastlyGetVCPUMs(prim.Pointer[prim.U64]) FastlyStatus 23 | 24 | // Return the number of milliseconds spent on the CPU for the current 25 | // session. 26 | // 27 | // Because compute guests can run on a variety of different platforms, 28 | // you should not necessarily expect these values to converge across 29 | // different sessions. Instead, we strongly recommend using this value 30 | // to look at the relative cost of various operations in your code base, 31 | // by taking the time before and after a particular operation and then 32 | // dividing this by the total amount of vCPU time your program takes. 33 | // The resulting percentage should be relatively stable across different 34 | // platforms, and useful in doing A/B testing. 35 | func GetVCPUMilliseconds() (uint64, error) { 36 | var milliseconds prim.U64 37 | 38 | err := fastlyGetVCPUMs(prim.ToPointer(&milliseconds)).toError() 39 | 40 | if err != nil { 41 | return 0, err 42 | } 43 | 44 | return uint64(milliseconds), nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/abi/fastly/configstore_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import ( 8 | "sync" 9 | 10 | "github.com/fastly/compute-sdk-go/internal/abi/prim" 11 | ) 12 | 13 | // witx: 14 | // 15 | // (module $fastly_config_store 16 | // (@interface func (export "open") 17 | // (param $name string) 18 | // (result $err (expected $config_store_handle (error $fastly_status))) 19 | // ) 20 | // 21 | //go:wasmimport fastly_config_store open 22 | //go:noescape 23 | func fastlyConfigStoreOpen( 24 | nameData prim.Pointer[prim.U8], nameLen prim.Usize, 25 | h prim.Pointer[configstoreHandle], 26 | ) FastlyStatus 27 | 28 | // ConfigStore represents a Fastly config store a collection of read-only 29 | // key/value pairs. For convenience, keys are modeled as Go strings, and values 30 | // as byte slices. 31 | // 32 | // NOTE: wasm, by definition, is a single-threaded execution environment. This 33 | // allows us to use valueBuf scratch space between the guest and host to avoid 34 | // allocations any larger than necessary, without locking. 35 | type ConfigStore struct { 36 | h configstoreHandle 37 | 38 | mu sync.Mutex // protects valueBuf 39 | valueBuf [configstoreMaxValueLen]byte 40 | } 41 | 42 | // Dictionaries are subject to very specific limitations: 255 character keys and 8000 character values, utf-8 encoded. 43 | // The current storage collation limits utf-8 representations to 3 bytes in length. 44 | // https://docs.fastly.com/en/guides/about-edge-dictionaries#limitations-and-considerations 45 | // https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8mb3.html 46 | // https://en.wikipedia.org/wiki/UTF-8#Encoding 47 | const ( 48 | configstoreMaxKeyLen = 255 * 3 // known maximum size for config store keys: 755 bytes, for 255 3-byte utf-8 encoded characters 49 | configstoreMaxValueLen = 8000 * 3 // known maximum size for config store values: 24,000 bytes, for 8000 3-byte utf-8 encoded characters 50 | ) 51 | 52 | // OpenConfigStore returns a reference to the named config store, if it exists. 53 | func OpenConfigStore(name string) (*ConfigStore, error) { 54 | var c ConfigStore 55 | 56 | nameBuffer := prim.NewReadBufferFromString(name).Wstring() 57 | 58 | if err := fastlyConfigStoreOpen( 59 | nameBuffer.Data, nameBuffer.Len, 60 | prim.ToPointer(&c.h), 61 | ).toError(); err != nil { 62 | return nil, err 63 | } 64 | return &c, nil 65 | } 66 | 67 | // witx: 68 | // 69 | // (@interface func (export "get") 70 | // (param $h $config_store_handle) 71 | // (param $key string) 72 | // (param $value (@witx pointer (@witx char8))) 73 | // (param $value_max_len (@witx usize)) 74 | // (param $nwritten_out (@witx pointer (@witx usize))) 75 | // (result $err (expected (error $fastly_status))) 76 | // ) 77 | // 78 | //go:wasmimport fastly_config_store get 79 | //go:noescape 80 | func fastlyConfigStoreGet( 81 | h configstoreHandle, 82 | keyData prim.Pointer[prim.U8], keyLen prim.Usize, 83 | value prim.Pointer[prim.Char8], 84 | valueMaxLen prim.Usize, 85 | nWritten prim.Pointer[prim.Usize], 86 | ) FastlyStatus 87 | 88 | // Get the value for key, if it exists. The returned slice's backing array is 89 | // shared between multiple calls to getBytesUnlocked. 90 | func (c *ConfigStore) getBytesUnlocked(key string) ([]byte, error) { 91 | keyBuffer := prim.NewReadBufferFromString(key) 92 | if keyBuffer.Len() > configstoreMaxKeyLen { 93 | return nil, FastlyStatusInval.toError() 94 | } 95 | buf := prim.NewWriteBufferFromBytes(c.valueBuf[:]) // fresh slice of backing array 96 | keyStr := keyBuffer.Wstring() 97 | status := fastlyConfigStoreGet( 98 | c.h, 99 | keyStr.Data, keyStr.Len, 100 | prim.ToPointer(buf.Char8Pointer()), buf.Cap(), 101 | prim.ToPointer(buf.NPointer()), 102 | ) 103 | if err := status.toError(); err != nil { 104 | return nil, err 105 | } 106 | return buf.AsBytes(), nil 107 | } 108 | 109 | // GetBytes returns a slice of newly-allocated memory for the value 110 | // corresponding to key. 111 | func (c *ConfigStore) GetBytes(key string) ([]byte, error) { 112 | c.mu.Lock() 113 | defer c.mu.Unlock() 114 | v, err := c.getBytesUnlocked(key) 115 | if err != nil { 116 | return nil, err 117 | } 118 | p := make([]byte, len(v)) 119 | copy(p, v) 120 | return p, nil 121 | } 122 | 123 | // Has returns true if key is found. 124 | func (c *ConfigStore) Has(key string) (bool, error) { 125 | keyBuffer := prim.NewReadBufferFromString(key).Wstring() 126 | var npointer prim.Usize = 0 127 | 128 | status := fastlyConfigStoreGet( 129 | c.h, 130 | keyBuffer.Data, keyBuffer.Len, 131 | prim.NullChar8Pointer(), 0, 132 | prim.ToPointer(&npointer), 133 | ) 134 | switch status { 135 | case FastlyStatusOK, FastlyStatusBufLen: 136 | return true, nil 137 | case FastlyStatusNone: 138 | return false, nil 139 | default: 140 | return false, status.toError() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /internal/abi/fastly/device_detection_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import "github.com/fastly/compute-sdk-go/internal/abi/prim" 8 | 9 | // witx: 10 | // 11 | // (module $fastly_device_detection 12 | // (@interface func (export "lookup") 13 | // (param $user_agent string) 14 | // 15 | // (param $buf (@witx pointer (@witx char8))) 16 | // (param $buf_len (@witx usize)) 17 | // (param $nwritten_out (@witx pointer (@witx usize))) 18 | // (result $err (expected (error $fastly_status))) 19 | // ) 20 | // ) 21 | // 22 | //go:wasmimport fastly_device_detection lookup 23 | //go:noescape 24 | func fastlyDeviceDetectionLookup( 25 | userAgentData prim.Pointer[prim.U8], userAgentLen prim.Usize, 26 | buf prim.Pointer[prim.Char8], bufLen prim.Usize, 27 | nWritten prim.Pointer[prim.Usize], 28 | ) FastlyStatus 29 | 30 | func DeviceLookup(userAgent string) ([]byte, error) { 31 | userAgentBuffer := prim.NewReadBufferFromString(userAgent).Wstring() 32 | n := DefaultMediumBufLen // Longest JSON of https://www.fastly.com/documentation/reference/vcl/variables/client-request/client-identified/ 33 | for { 34 | buf := prim.NewWriteBuffer(n) 35 | status := fastlyDeviceDetectionLookup( 36 | userAgentBuffer.Data, userAgentBuffer.Len, 37 | prim.ToPointer(buf.Char8Pointer()), 38 | buf.Cap(), 39 | prim.ToPointer(buf.NPointer()), 40 | ) 41 | if status == FastlyStatusBufLen && buf.NValue() > 0 { 42 | n = int(buf.NValue()) 43 | continue 44 | } 45 | if err := status.toError(); err != nil { 46 | return nil, err 47 | } 48 | return buf.AsBytes(), nil 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/abi/fastly/dictionary_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import ( 8 | "sync" 9 | 10 | "github.com/fastly/compute-sdk-go/internal/abi/prim" 11 | ) 12 | 13 | // witx: 14 | // 15 | // (module $fastly_dictionary 16 | // (@interface func (export "open") 17 | // (param $name string) 18 | // (result $err $fastly_status) 19 | // (result $h $dictionary_handle) 20 | // ) 21 | // 22 | //go:wasmimport fastly_dictionary open 23 | //go:noescape 24 | func fastlyDictionaryOpen( 25 | nameData prim.Pointer[prim.U8], nameLen prim.Usize, 26 | h prim.Pointer[dictionaryHandle], 27 | ) FastlyStatus 28 | 29 | // Dictionary represents a Fastly edge dictionary, a collection of read-only 30 | // key/value pairs. For convenience, keys are modeled as Go strings, and values 31 | // as byte slices. 32 | // 33 | // NOTE: wasm, by definition, is a single-threaded execution environment. This 34 | // allows us to use valueBuf scratch space between the guest and host to avoid 35 | // allocations any larger than necessary, without locking. 36 | type Dictionary struct { 37 | h dictionaryHandle 38 | 39 | mu sync.Mutex // protects valueBuf 40 | valueBuf [dictionaryMaxValueLen]byte 41 | } 42 | 43 | // Dictionaries are subject to very specific limitations: 255 character keys and 8000 character values, utf-8 encoded. 44 | // The current storage collation limits utf-8 representations to 3 bytes in length. 45 | // https://docs.fastly.com/en/guides/about-edge-dictionaries#limitations-and-considerations 46 | // https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8mb3.html 47 | // https://en.wikipedia.org/wiki/UTF-8#Encoding 48 | const ( 49 | dictionaryMaxKeyLen = 255 * 3 // known maximum size for config store keys: 755 bytes, for 255 3-byte utf-8 encoded characters 50 | dictionaryMaxValueLen = 8000 * 3 // known maximum size for config store values: 24,000 bytes, for 8000 3-byte utf-8 encoded characters 51 | ) 52 | 53 | // OpenDictionary returns a reference to the named dictionary, if it exists. 54 | func OpenDictionary(name string) (*Dictionary, error) { 55 | var d Dictionary 56 | 57 | nameBuffer := prim.NewReadBufferFromString(name).Wstring() 58 | 59 | if err := fastlyDictionaryOpen( 60 | nameBuffer.Data, nameBuffer.Len, 61 | prim.ToPointer(&d.h), 62 | ).toError(); err != nil { 63 | return nil, err 64 | } 65 | return &d, nil 66 | } 67 | 68 | // witx: 69 | // 70 | // (@interface func (export "get") 71 | // (param $h $dictionary_handle) 72 | // (param $key string) 73 | // (param $value (@witx pointer char8)) 74 | // (param $value_max_len (@witx usize)) 75 | // (result $err $fastly_status) 76 | // (result $nwritten (@witx usize)) 77 | // ) 78 | // 79 | //go:wasmimport fastly_dictionary get 80 | //go:noescape 81 | func fastlyDictionaryGet( 82 | h dictionaryHandle, 83 | keyData prim.Pointer[prim.U8], keyLen prim.Usize, 84 | value prim.Pointer[prim.Char8], 85 | valueMaxLen prim.Usize, 86 | nWritten prim.Pointer[prim.Usize], 87 | ) FastlyStatus 88 | 89 | // Get the value for key, if it exists. The returned slice's backing array is 90 | // shared between multiple calls to getBytesUnlocked. 91 | func (d *Dictionary) getBytesUnlocked(key string) ([]byte, error) { 92 | keyBuffer := prim.NewReadBufferFromString(key) 93 | if keyBuffer.Len() > dictionaryMaxKeyLen { 94 | return nil, FastlyStatusInval.toError() 95 | } 96 | buf := prim.NewWriteBufferFromBytes(d.valueBuf[:]) // fresh slice of backing array 97 | keyStr := keyBuffer.Wstring() 98 | status := fastlyDictionaryGet( 99 | d.h, 100 | keyStr.Data, keyStr.Len, 101 | prim.ToPointer(buf.Char8Pointer()), buf.Cap(), 102 | prim.ToPointer(buf.NPointer()), 103 | ) 104 | if err := status.toError(); err != nil { 105 | return nil, err 106 | } 107 | return buf.AsBytes(), nil 108 | } 109 | 110 | // GetBytes returns a slice of newly-allocated memory for the value 111 | // corresponding to key. 112 | func (d *Dictionary) GetBytes(key string) ([]byte, error) { 113 | d.mu.Lock() 114 | defer d.mu.Unlock() 115 | v, err := d.getBytesUnlocked(key) 116 | if err != nil { 117 | return nil, err 118 | } 119 | p := make([]byte, len(v)) 120 | copy(p, v) 121 | return p, nil 122 | } 123 | 124 | // Has returns true if key is found. 125 | func (d *Dictionary) Has(key string) (bool, error) { 126 | keyBuffer := prim.NewReadBufferFromString(key).Wstring() 127 | var npointer prim.Usize = 0 128 | 129 | status := fastlyDictionaryGet( 130 | d.h, 131 | keyBuffer.Data, keyBuffer.Len, 132 | prim.NullChar8Pointer(), 0, 133 | prim.ToPointer(&npointer), 134 | ) 135 | switch status { 136 | case FastlyStatusOK, FastlyStatusBufLen: 137 | return true, nil 138 | case FastlyStatusNone: 139 | return false, nil 140 | default: 141 | return false, status.toError() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /internal/abi/fastly/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package fastly provides access to the Fastly Compute hostcall ABI. 4 | // 5 | // The TinyGo SDK is modeled in layers. Each layer has a single purpose. This 6 | // package is the lowest layer, and it's singular purpose is to adapt each 7 | // Compute hostcall to a function which is basically idiomatic Go. 8 | // 9 | // In support of that purpose, the package defines a few types, e.g. HTTPBody, 10 | // which model the modules of the hostcalls, and implement corresponding 11 | // functions as methods on those types. Each hostcall should have a single 12 | // corresponding Go method or function. 13 | // 14 | // There are also helper types, like Values, which make it easier to interact 15 | // with the hostcall ABI. But, in general, this package should be kept as small 16 | // as possible, and all nontrivial work performed at the next layer up, e.g. 17 | // package fsthttp. 18 | // 19 | // This package is not and should not be user-accessible. All features, 20 | // capabilities, etc. that should be accessible by users should be made 21 | // available via separate packages that treat this package as a dependency. 22 | package fastly 23 | -------------------------------------------------------------------------------- /internal/abi/fastly/geo_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import ( 8 | "net" 9 | 10 | "github.com/fastly/compute-sdk-go/internal/abi/prim" 11 | ) 12 | 13 | // witx: 14 | // 15 | // (module $fastly_geo 16 | // (@interface func (export "lookup") 17 | // (param $addr_octets (@witx pointer (@witx char8))) 18 | // (param $addr_len (@witx usize)) 19 | // (param $buf (@witx pointer (@witx char8))) 20 | // (param $buf_len (@witx usize)) 21 | // (param $nwritten_out (@witx pointer (@witx usize))) 22 | // (result $err (expected (error $fastly_status))) 23 | // ) 24 | // 25 | // ) 26 | // 27 | //go:wasmimport fastly_geo lookup 28 | //go:noescape 29 | func fastlyGeoLookup( 30 | addrOctets prim.Pointer[prim.Char8], addrLen prim.Usize, 31 | buf prim.Pointer[prim.Char8], bufLen prim.Usize, 32 | nWrittenOut prim.Pointer[prim.Usize], 33 | ) FastlyStatus 34 | 35 | // GeoLookup returns the geographic data associated with the IP address. 36 | func GeoLookup(ip net.IP) ([]byte, error) { 37 | if x := ip.To4(); x != nil { 38 | ip = x 39 | } 40 | addrOctets := prim.NewReadBufferFromBytes(ip) 41 | 42 | n := DefaultMediumBufLen 43 | for { 44 | buf := prim.NewWriteBuffer(n) // initial geo buf size 45 | status := fastlyGeoLookup( 46 | prim.ToPointer(addrOctets.Char8Pointer()), addrOctets.Len(), 47 | prim.ToPointer(buf.Char8Pointer()), buf.Cap(), 48 | prim.ToPointer(buf.NPointer()), 49 | ) 50 | if status == FastlyStatusBufLen && buf.NValue() > 0 { 51 | n = int(buf.NValue()) 52 | continue 53 | } 54 | if err := status.toError(); err != nil { 55 | return nil, err 56 | } 57 | return buf.AsBytes(), nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/abi/fastly/init_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import "github.com/fastly/compute-sdk-go/internal/abi/prim" 8 | 9 | func init() { 10 | fastlyABIInit(1) 11 | } 12 | 13 | // witx: 14 | // 15 | // (module $fastly_abi 16 | // (@interface func (export "init") 17 | // (param $abi_version u64) 18 | // (result $err $fastly_status)) 19 | // ) 20 | // 21 | //go:wasmimport fastly_abi init 22 | //go:noescape 23 | func fastlyABIInit(abiVersion prim.U64) FastlyStatus 24 | 25 | // TODO(pb): this doesn't need to be exported, I don't think? 26 | // Initialize the Fastly ABI at the given version. 27 | //func Initialize(version uint64) error { 28 | -------------------------------------------------------------------------------- /internal/abi/fastly/log_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import ( 8 | "github.com/fastly/compute-sdk-go/internal/abi/prim" 9 | ) 10 | 11 | // (module $fastly_log 12 | 13 | // LogEndpoint represents a specific Fastly log endpoint. 14 | type LogEndpoint struct { 15 | h endpointHandle 16 | } 17 | 18 | // witx: 19 | // 20 | // (@interface func (export "endpoint_get") 21 | // (param $name (array u8)) 22 | // (result $err $fastly_status) 23 | // (result $endpoint_handle_out $endpoint_handle)) 24 | // 25 | //go:wasmimport fastly_log endpoint_get 26 | //go:noescape 27 | func fastlyLogEndpointGet( 28 | nameData prim.Pointer[prim.U8], nameLen prim.Usize, 29 | endpointHandleOut prim.Pointer[endpointHandle], 30 | ) FastlyStatus 31 | 32 | // GetLogEndpoint opens the log endpoint identified by name. 33 | func GetLogEndpoint(name string) (*LogEndpoint, error) { 34 | var e LogEndpoint 35 | 36 | nameBuffer := prim.NewReadBufferFromString(name).ArrayU8() 37 | 38 | if err := fastlyLogEndpointGet( 39 | nameBuffer.Data, nameBuffer.Len, 40 | prim.ToPointer(&e.h), 41 | ).toError(); err != nil { 42 | return nil, err 43 | } 44 | 45 | return &e, nil 46 | } 47 | 48 | // witx: 49 | // 50 | // (@interface func (export "write") 51 | // (param $h $endpoint_handle) 52 | // (param $msg (array u8)) 53 | // (result $err $fastly_status) 54 | // (result $nwritten_out (@witx usize))) 55 | // 56 | // ) 57 | // 58 | //go:wasmimport fastly_log write 59 | //go:noescape 60 | func fastlyLogWrite( 61 | h endpointHandle, 62 | msgData prim.Pointer[prim.U8], msgLen prim.Usize, 63 | nWrittenOut prim.Pointer[prim.Usize], 64 | ) FastlyStatus 65 | 66 | // Write implements io.Writer, writing len(p) bytes from p into the endpoint. 67 | // Returns the number of bytes written, and any error encountered. 68 | // By contract, if n < len(p), the returned error will be non-nil. 69 | func (e *LogEndpoint) Write(p []byte) (n int, err error) { 70 | for n < len(p) && err == nil { 71 | var nWritten prim.Usize 72 | p_n_Buffer := prim.NewReadBufferFromBytes(p[n:]).ArrayU8() 73 | 74 | if err = fastlyLogWrite( 75 | e.h, 76 | p_n_Buffer.Data, p_n_Buffer.Len, 77 | prim.ToPointer(&nWritten), 78 | ).toError(); err == nil { 79 | n += int(nWritten) 80 | } 81 | } 82 | return n, err 83 | } 84 | -------------------------------------------------------------------------------- /internal/abi/fastly/secret_store_guest.go: -------------------------------------------------------------------------------- 1 | //go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls 2 | 3 | // Copyright 2022 Fastly, Inc. 4 | 5 | package fastly 6 | 7 | import "github.com/fastly/compute-sdk-go/internal/abi/prim" 8 | 9 | // SecretStore represents a Fastly secret store, a collection of 10 | // key/value pairs for storing sensitive data. 11 | type SecretStore struct { 12 | h secretStoreHandle 13 | } 14 | 15 | // Secret represents a secret value. Data is encrypted at rest, and is 16 | // only decrypted upon the first call to the secret's Plaintext method. 17 | type Secret struct { 18 | h secretHandle 19 | } 20 | 21 | // witx: 22 | // 23 | // (module $fastly_secret_store 24 | // (@interface func (export "open") 25 | // (param $name string) 26 | // (result $err (expected $secret_store_handle (error $fastly_status))) 27 | // ) 28 | 29 | //go:wasmimport fastly_secret_store open 30 | //go:noescape 31 | func fastlySecretStoreOpen( 32 | nameData prim.Pointer[prim.U8], nameLen prim.Usize, 33 | h prim.Pointer[secretStoreHandle], 34 | ) FastlyStatus 35 | 36 | // OpenSecretStore returns a reference to the named secret store, if it exists. 37 | func OpenSecretStore(name string) (*SecretStore, error) { 38 | var st SecretStore 39 | 40 | nameBuffer := prim.NewReadBufferFromString(name).Wstring() 41 | 42 | if err := fastlySecretStoreOpen( 43 | nameBuffer.Data, nameBuffer.Len, 44 | prim.ToPointer(&st.h), 45 | ).toError(); err != nil { 46 | return nil, err 47 | } 48 | 49 | return &st, nil 50 | } 51 | 52 | // witx: 53 | // 54 | // (module $fastly_secret_store 55 | // (@interface func (export "get") 56 | // (param $store $secret_store_handle) 57 | // (param $key string) 58 | // (result $err (expected $secret_handle (error $fastly_status))) 59 | // ) 60 | // ) 61 | 62 | //go:wasmimport fastly_secret_store get 63 | //go:noescape 64 | func fastlySecretStoreGet( 65 | h secretStoreHandle, 66 | keyData prim.Pointer[prim.U8], keyLen prim.Usize, 67 | s prim.Pointer[secretHandle], 68 | ) FastlyStatus 69 | 70 | // Get returns a handle to the secret value for the given name, if it 71 | // exists. 72 | func (st *SecretStore) Get(name string) (*Secret, error) { 73 | var s Secret 74 | 75 | nameBuffer := prim.NewReadBufferFromString(name).Wstring() 76 | 77 | if err := fastlySecretStoreGet( 78 | st.h, 79 | nameBuffer.Data, nameBuffer.Len, 80 | prim.ToPointer(&s.h), 81 | ).toError(); err != nil { 82 | return nil, err 83 | } 84 | 85 | return &s, nil 86 | } 87 | 88 | // witx: 89 | // 90 | // (module $fastly_secret_store 91 | // (@interface func (export "plaintext") 92 | // (param $secret $secret_handle) 93 | // (param $buf (@witx pointer (@witx char8))) 94 | // (param $buf_len (@witx usize)) 95 | // (param $nwritten_out (@witx pointer (@witx usize))) 96 | // (result $err (expected (error $fastly_status))) 97 | // ) 98 | // ) 99 | 100 | //go:wasmimport fastly_secret_store plaintext 101 | //go:noescape 102 | func fastlySecretPlaintext( 103 | h secretHandle, 104 | buf prim.Pointer[prim.Char8], bufLen prim.Usize, 105 | nwritten prim.Pointer[prim.Usize], 106 | ) FastlyStatus 107 | 108 | // Plaintext decrypts and returns the secret value as a byte slice. 109 | func (s *Secret) Plaintext() ([]byte, error) { 110 | n := DefaultMediumBufLen 111 | for { 112 | buf := prim.NewWriteBuffer(n) 113 | status := fastlySecretPlaintext( 114 | s.h, 115 | prim.ToPointer(buf.Char8Pointer()), 116 | buf.Cap(), 117 | prim.ToPointer(buf.NPointer()), 118 | ) 119 | if status == FastlyStatusBufLen && buf.NValue() > 0 { 120 | n = int(buf.NValue()) 121 | continue 122 | } 123 | if err := status.toError(); err != nil { 124 | return nil, err 125 | } 126 | return buf.AsBytes(), nil 127 | } 128 | } 129 | 130 | func (s *Secret) Handle() secretHandle { 131 | return s.h 132 | } 133 | 134 | // witx: 135 | // 136 | // (@interface func (export "from_bytes") 137 | // (param $buf (@witx pointer (@witx char8))) 138 | // (param $buf_len (@witx usize)) 139 | // (result $err (expected $secret_handle (error $fastly_status))) 140 | // ) 141 | 142 | //go:wasmimport fastly_secret_store from_bytes 143 | //go:noescape 144 | func fastlySecretFromBytes( 145 | buf prim.Pointer[prim.Char8], 146 | bufLen prim.Usize, 147 | h prim.Pointer[secretHandle], 148 | ) FastlyStatus 149 | 150 | // FromBytes creates a secret handle for the given byte slice. This is 151 | // for use with APIs that require a secret handle but cannot (for 152 | // whatever reason) use a secret store. 153 | func SecretFromBytes(b []byte) (*Secret, error) { 154 | var s Secret 155 | 156 | buf := prim.NewReadBufferFromBytes(b) 157 | 158 | if err := fastlySecretFromBytes( 159 | prim.ToPointer(buf.Char8Pointer()), 160 | buf.Len(), 161 | prim.ToPointer(&s.h), 162 | ).toError(); err != nil { 163 | return nil, err 164 | } 165 | 166 | return &s, nil 167 | } 168 | -------------------------------------------------------------------------------- /internal/abi/prim/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package prim contains primitive types used in Wasm ABI functions. It acts as 4 | // a common dependency for package wasi, which provides access to general Wasi 5 | // capabilities, and package fastly, which provides access to hostcalls specific 6 | // to the Fastly Compute environment. 7 | package prim 8 | -------------------------------------------------------------------------------- /internal/abi/prim/prim.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package prim 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | // Usize is an unsigned integer who's size is based on the system architecture. 10 | type Usize uint32 11 | 12 | // Char8 is an unsigned 8 bit integer. 13 | type Char8 uint8 14 | 15 | // U8 is an unsigned 8 bit integer. 16 | type U8 uint8 17 | 18 | // U16 is an unsigned 16 bit integer. 19 | type U16 uint16 20 | 21 | // U32 is an unsigned 32 bit integer. 22 | type U32 uint32 23 | 24 | // U64 is an unsigned 64 bit integer. 25 | type U64 uint64 26 | 27 | // Pointer is the type of a pointer 28 | type Pointer[_ any] uint32 29 | 30 | // ToPointer turns an arbitrary pointer into a Pointer 31 | func ToPointer[T any](ptr *T) Pointer[T] { 32 | return Pointer[T](uintptr(unsafe.Pointer(ptr))) 33 | } 34 | 35 | // NullPointer makes a null pointer to a byte buffer. 36 | func NullChar8Pointer() Pointer[Char8] { 37 | return Pointer[Char8](uintptr(unsafe.Pointer(nil))) 38 | } 39 | 40 | // Wstring is a header for a string. 41 | type Wstring struct { 42 | Data Pointer[U8] 43 | Len Usize 44 | } 45 | 46 | // ArrayU8 is a header for an array of U8. 47 | type ArrayU8 Wstring 48 | 49 | // ArrayChar8 is a header for an array of Char8. 50 | type ArrayChar8 Wstring 51 | 52 | // WriteBuffer provides some memory that hostcalls can write into. 53 | // 54 | // Technically, Go's GC is permitted to move memory around whenever it wants 55 | // (with a few exceptions). This is normally safe, because it updates references 56 | // to that memory at the same time. But unsafe.Pointer isn't understood by the 57 | // GC as a reference, which means that our usage here is technically unsafe: if 58 | // the GC moved the buffer around during a hostcall, the hostcall would end up 59 | // writing to an invalid location. 60 | // 61 | // This works fine, though, because hostcalls only happen under +build tinygo, 62 | // and all of the GC implementations provided by TinyGo don't do any of that 63 | // fancy stuff. But it's definitely a risk we need to be aware of when upgrading 64 | // TinyGo in the future. 65 | type WriteBuffer struct { 66 | buf []byte 67 | n Usize 68 | } 69 | 70 | // NewWriteBuffer creates a new WriteBuffer with the provided capacity. 71 | func NewWriteBuffer(cap int) *WriteBuffer { 72 | return NewWriteBufferFromBytes(make([]byte, 0, cap)) 73 | } 74 | 75 | // NewWriteBufferFromBytes creates a new WriteBuffer with the provided byte 76 | // slice used as its buffer. 77 | func NewWriteBufferFromBytes(buf []byte) *WriteBuffer { 78 | return &WriteBuffer{buf: buf} 79 | } 80 | 81 | // Char8Pointer returns a pointer to the buffer's data as a Char8. 82 | func (b *WriteBuffer) Char8Pointer() *Char8 { 83 | return (*Char8)(unsafe.SliceData(b.buf)) 84 | } 85 | 86 | // U8Pointer returns a pointer to the buffer's data as a U8. 87 | func (b *WriteBuffer) U8Pointer() *U8 { 88 | return (*U8)(unsafe.SliceData(b.buf)) 89 | } 90 | 91 | // Cap returns the capacity of the buffer as a Usize. 92 | func (b *WriteBuffer) Cap() Usize { 93 | return Usize(cap(b.buf)) 94 | } 95 | 96 | // Len returns the length of data in the buffer as a Usize. 97 | func (b *WriteBuffer) Len() Usize { 98 | return Usize(len(b.buf)) 99 | } 100 | 101 | // NPointer returns a pointer to the number of bytes written to the buffer as a 102 | // Usize. 103 | func (b *WriteBuffer) NPointer() *Usize { 104 | return &b.n 105 | } 106 | 107 | // NValue returns the number of bytes written to the buffer as a Usize. 108 | func (b *WriteBuffer) NValue() Usize { 109 | return b.n 110 | } 111 | 112 | // AsBytes returns a slice of the buffer's data as a byte slice. 113 | func (b *WriteBuffer) AsBytes() []byte { 114 | return b.buf[:b.n:b.n] 115 | } 116 | 117 | // ToString returns a copy of the buffer's data as a string. 118 | func (b *WriteBuffer) ToString() string { 119 | return string(b.AsBytes()) 120 | } 121 | 122 | // ReadBuffer is like WriteBuffer, but only allows hostcalls to read the 123 | // underlying memory via a smaller, more restricted API. 124 | type ReadBuffer struct { 125 | buf []byte 126 | } 127 | 128 | // NewReadBufferFromString creates a ReadBuffer with its buffer based on the 129 | // provided string. 130 | func NewReadBufferFromString(s string) *ReadBuffer { 131 | return NewReadBufferFromBytes([]byte(s)) 132 | } 133 | 134 | // NewReadBufferFromBytes creates a new ReadBuffer with the provided byte slice 135 | // used as its buffer. 136 | func NewReadBufferFromBytes(buf []byte) *ReadBuffer { 137 | return &ReadBuffer{buf: buf} 138 | } 139 | 140 | // Wstring returns the buffers data as a Wstring. 141 | func (b *ReadBuffer) Wstring() Wstring { 142 | return Wstring{ 143 | Data: Pointer[U8](uintptr(unsafe.Pointer(unsafe.SliceData(b.buf)))), 144 | Len: Usize(len(b.buf)), 145 | } 146 | } 147 | 148 | // ArrayU8 returns the buffers data as a ArrayU8. 149 | func (b *ReadBuffer) ArrayU8() ArrayU8 { 150 | return ArrayU8{ 151 | Data: Pointer[U8](uintptr(unsafe.Pointer(unsafe.SliceData(b.buf)))), 152 | Len: Usize(len(b.buf)), 153 | } 154 | } 155 | 156 | // ArrayChar8 returns the buffers data as a ArrayChar8. 157 | func (b *ReadBuffer) ArrayChar8() ArrayChar8 { 158 | return ArrayChar8{ 159 | Data: Pointer[U8](uintptr(unsafe.Pointer(unsafe.SliceData(b.buf)))), 160 | Len: Usize(len(b.buf)), 161 | } 162 | } 163 | 164 | // Char8Pointer returns a pointer to the buffer's data as a Char8. 165 | func (b *ReadBuffer) Char8Pointer() *Char8 { 166 | return (*Char8)(unsafe.Pointer(unsafe.SliceData(b.buf))) 167 | } 168 | 169 | // U8Pointer returns a pointer to the buffer's data as a U8. 170 | func (b *ReadBuffer) U8Pointer() *U8 { 171 | return (*U8)(unsafe.Pointer(unsafe.SliceData(b.buf))) 172 | } 173 | 174 | // Len returns the length of data in the buffer as a Usize. 175 | func (b *ReadBuffer) Len() Usize { 176 | return Usize(len(b.buf)) 177 | } 178 | -------------------------------------------------------------------------------- /internal/abi/wasi/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package wasi provides access to Wasi features independent of Fastly Compute. 4 | package wasi 5 | -------------------------------------------------------------------------------- /objectstore/doc.go: -------------------------------------------------------------------------------- 1 | // Deprecated: Use the kvstore package instead. 2 | package objectstore 3 | -------------------------------------------------------------------------------- /objectstore/objectstore.go: -------------------------------------------------------------------------------- 1 | package objectstore 2 | 3 | import ( 4 | "github.com/fastly/compute-sdk-go/kvstore" 5 | ) 6 | 7 | var ErrKeyNotFound = kvstore.ErrKeyNotFound 8 | 9 | // Deprecated: Use the kvstore package instead. 10 | type Entry = kvstore.Entry 11 | 12 | // Store represents a Fastly object store 13 | // 14 | // Deprecated: Use the kvstore package instead. 15 | type Store = kvstore.Store 16 | 17 | // Open returns a handle to the named object store 18 | // 19 | // Deprecated: Use kvstore.Open() instead. 20 | func Open(name string) (*Store, error) { 21 | return kvstore.Open(name) 22 | } 23 | -------------------------------------------------------------------------------- /purge/purge.go: -------------------------------------------------------------------------------- 1 | // Package purge provides cache purging operations for Fastly 2 | // Compute. 3 | // 4 | // See the [Fastly purge documentation] for details. 5 | // 6 | // [Fastly purge documentation]: https://developer.fastly.com/learning/concepts/purging/ 7 | package purge 8 | 9 | import "github.com/fastly/compute-sdk-go/internal/abi/fastly" 10 | 11 | // PurgeOptions control the behavior of purge operations. 12 | type PurgeOptions struct { 13 | // Whether to soft purge the item. A soft purge marks a cached 14 | // object as stale, rather than invalidating it. 15 | Soft bool 16 | } 17 | 18 | // PurgeSurrogateKey purges all cached objects with the provided 19 | // surrogate key. 20 | func PurgeSurrogateKey(surrogateKey string, opts PurgeOptions) error { 21 | var abiOpts fastly.PurgeOptions 22 | abiOpts.SoftPurge(opts.Soft) 23 | 24 | return fastly.PurgeSurrogateKey(surrogateKey, abiOpts) 25 | } 26 | -------------------------------------------------------------------------------- /rtlog/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package rtlog provides an interface to real-time logging endpoints. Refer to 4 | // https://developer.fastly.com/reference/api/logging/ for more information. 5 | package rtlog 6 | -------------------------------------------------------------------------------- /rtlog/endpoint.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package rtlog 4 | 5 | import "github.com/fastly/compute-sdk-go/internal/abi/fastly" 6 | 7 | // Endpoint represents a real-time logging endpoint. 8 | type Endpoint struct { 9 | abiEndpoint *fastly.LogEndpoint 10 | err error 11 | } 12 | 13 | // Open returns an endpoint corresponding to the given name. Names are case 14 | // sensitive. Calling Open with a name that doesn't correspond to any logging 15 | // endpoint available in your service will still return a usable endpoint, and 16 | // writes to that endpoint will succeed. Refer to your service dashboard to 17 | // diagnose missing log events. 18 | func Open(name string) *Endpoint { 19 | e, err := fastly.GetLogEndpoint(name) 20 | return &Endpoint{e, err} 21 | } 22 | 23 | // Write implements io.Writer, writing len(p) bytes from p to the endpoint. 24 | // Returns the number of bytes written, and any error encountered. 25 | // By contract, if n < len(p), the returned error will be non-nil. 26 | // 27 | // Each call to Write produces a single log event. 28 | func (e *Endpoint) Write(p []byte) (int, error) { 29 | if e.err != nil { 30 | return 0, e.err 31 | } 32 | 33 | return e.abiEndpoint.Write(p) 34 | } 35 | -------------------------------------------------------------------------------- /secretstore/secretstore.go: -------------------------------------------------------------------------------- 1 | // Package secretstore provides a read-only interface to Fastly 2 | // Compute Secret Stores. 3 | // 4 | // Secret stores are persistent, globally distributed stores for 5 | // secrets. Secrets are decrypted as-needed at the edge. 6 | // 7 | // See the [Fastly Secret Store documentation] for details. 8 | // 9 | // [Fastly Secret Store documentation]: https://developer.fastly.com/learning/concepts/dynamic-config/#secret-stores 10 | package secretstore 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | 16 | "github.com/fastly/compute-sdk-go/internal/abi/fastly" 17 | ) 18 | 19 | var ( 20 | // ErrSecretStoreNotFound indicates that the named secret store 21 | // doesn't exist. 22 | ErrSecretStoreNotFound = errors.New("secret store not found") 23 | 24 | // ErrInvalidSecretStoreName indicates that the given secret store 25 | // name is invalid. 26 | ErrInvalidSecretStoreName = errors.New("invalid secret store name") 27 | 28 | // ErrSecretNotFound indicates that the named secret doesn't exist 29 | // within this store. 30 | ErrSecretNotFound = errors.New("secret not found") 31 | 32 | // ErrInvalidSecretName indicates that the given secret name is 33 | // invalid. 34 | ErrInvalidSecretName = errors.New("invalid secret name") 35 | 36 | // ErrUnexpected indicates than an unexpected error occurred. 37 | ErrUnexpected = errors.New("unexpected error") 38 | ) 39 | 40 | // Store represents a Fastly Secret Store 41 | type Store struct { 42 | st *fastly.SecretStore 43 | } 44 | 45 | // Secret represents a secret in a store 46 | type Secret struct { 47 | s *fastly.Secret 48 | } 49 | 50 | // Open returns a handle to the named secret store, if it exists. It 51 | // will return [ErrSecretStoreNotFound] if it doesn't exist. 52 | func Open(name string) (*Store, error) { 53 | st, err := fastly.OpenSecretStore(name) 54 | if err != nil { 55 | status, ok := fastly.IsFastlyError(err) 56 | switch { 57 | case ok && status == fastly.FastlyStatusNone: 58 | return nil, ErrSecretStoreNotFound 59 | case ok && status == fastly.FastlyStatusInval: 60 | return nil, ErrInvalidSecretStoreName 61 | case ok: 62 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 63 | default: 64 | return nil, err 65 | } 66 | } 67 | 68 | return &Store{st: st}, nil 69 | } 70 | 71 | // Get returns a handle to the named secret within the store, if it 72 | // exists. It will return [ErrSecretNotFound] if it doesn't exist. 73 | func (st *Store) Get(name string) (*Secret, error) { 74 | s, err := st.st.Get(name) 75 | if err != nil { 76 | status, ok := fastly.IsFastlyError(err) 77 | switch { 78 | case ok && status == fastly.FastlyStatusNone: 79 | return nil, ErrSecretNotFound 80 | case ok && status == fastly.FastlyStatusInval: 81 | return nil, ErrInvalidSecretName 82 | case ok: 83 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 84 | default: 85 | return nil, err 86 | } 87 | } 88 | 89 | return &Secret{s: s}, nil 90 | } 91 | 92 | // Plaintext decrypts and returns the secret value as a byte slice. 93 | func (s *Secret) Plaintext() ([]byte, error) { 94 | plaintext, err := s.s.Plaintext() 95 | if err != nil { 96 | status, ok := fastly.IsFastlyError(err) 97 | if ok { 98 | return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) 99 | } 100 | return nil, err 101 | } 102 | return plaintext, nil 103 | } 104 | 105 | // Handle returns an opaque pointer for other packages which need a Secret. 106 | // 107 | // This should not be needed by user code. 108 | func (s *Secret) Handle() *fastly.Secret { 109 | return s.s 110 | } 111 | 112 | // FromBytes creates an instance of the [Secret] type for use with APIs 113 | // that require it from the provided byte slice. 114 | // 115 | // This function is provided as a way to use data that should be secret, 116 | // but is being obtained by some other means than a Fastly Secret Store. 117 | // Secret values created this way are plaintext only, and are not shared 118 | // with other sessions. This should only be used in situations in which 119 | // an API requires a [Secret], but you cannot (for whatever reason) use 120 | // a [Store] to store them. 121 | // 122 | // It is strongly suggested to store secrets in a [Store] and create 123 | // Secret values by calling [Store.Get]. 124 | func SecretFromBytes(b []byte) (*Secret, error) { 125 | s, err := fastly.SecretFromBytes(b) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | return &Secret{s: s}, nil 131 | } 132 | 133 | // Plaintext decrypts and returns the secret value as a byte slice for 134 | // the given secret store and secret name. 135 | // 136 | // This is a convenience function that combines the functionality of 137 | // [Open], [Store.Get], and [Secret.Plaintext]. 138 | func Plaintext(storeName, secretName string) ([]byte, error) { 139 | st, err := Open(storeName) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | s, err := st.Get(secretName) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | return s.Plaintext() 150 | } 151 | -------------------------------------------------------------------------------- /semgrep/README.txt: -------------------------------------------------------------------------------- 1 | This is a set of static analysis patterns for [semgrep](https://semgrep.dev). 2 | 3 | # List of rules 4 | 5 | - edgedict: warn and migrate from the edgedict API to the configstore API 6 | - objectstore: warn and migrate from objectstore API to the kvstore API 7 | -------------------------------------------------------------------------------- /semgrep/edgedict.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: deprecated-edgedict-dictionary 3 | pattern: edgedict.Dictionary 4 | fix: configstore.Store 5 | message: edgedict package is deprecated 6 | languages: [go] 7 | severity: ERROR 8 | 9 | - id: deprecated-edgedict-open 10 | pattern: edgedict.Open($N) 11 | fix: configstore.Open($N) 12 | message: edgedict package is deprecated 13 | languages: [go] 14 | severity: ERROR 15 | 16 | - id: deprecated-edgedict-errdictionarynotfound 17 | pattern: edgedict.ErrDictionaryNotFound 18 | fix: configstore.ErrStoreNotFound 19 | message: edgedict package is deprecated 20 | languages: [go] 21 | severity: ERROR 22 | 23 | - id: deprecated-edgedict-errdictionarynameempty 24 | pattern: edgedict.ErrDictionaryNameEmpty 25 | fix: configstore.ErrStoreNameEmpty 26 | message: edgedict package is deprecated 27 | languages: [go] 28 | severity: ERROR 29 | 30 | - id: deprecated-edgedict-errdictionarynameinvalid 31 | pattern: edgedict.ErrDictionaryNameInvalid 32 | fix: configstore.ErrStoreNameInvalid 33 | message: edgedict package is deprecated 34 | languages: [go] 35 | severity: ERROR 36 | 37 | - id: deprecated-edgedict-errdictionarynametoolong 38 | pattern: edgedict.ErrDictionaryNameTooLong 39 | fix: configstore.ErrStoreNameTooLong 40 | message: edgedict package is deprecated 41 | languages: [go] 42 | severity: ERROR 43 | 44 | - id: deprecated-edgedict-errkeynotfound 45 | pattern: edgedict.ErrKeyNotFound 46 | fix: configstore.ErrKeyNotFound 47 | message: edgedict package is deprecated 48 | languages: [go] 49 | severity: ERROR 50 | -------------------------------------------------------------------------------- /semgrep/objectstore.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: deprecated-objectstore-store 3 | pattern: objectstore.Store 4 | fix: kvstore.Store 5 | message: objectstore package is deprecated 6 | languages: [go] 7 | severity: ERROR 8 | 9 | - id: deprecated-objectstore-open 10 | pattern: objectstore.Open($N) 11 | fix: kvstore.Open($N) 12 | message: objectstore package is deprecated 13 | languages: [go] 14 | severity: ERROR 15 | 16 | - id: deprecated-objectstore-errkeynotfound 17 | pattern: objectstore.ErrKeyNotFound 18 | fix: kvstore.ErrKeyNotFound 19 | message: objectstore package is deprecated 20 | languages: [go] 21 | severity: ERROR 22 | -------------------------------------------------------------------------------- /targets/fastly-compute-wasi.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherits": ["wasi"], 3 | "emulator": "viceroy run -C fastly.toml {}" 4 | } 5 | -------------------------------------------------------------------------------- /targets/fastly-compute-wasip1.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherits": ["wasip1"], 3 | "emulator": "viceroy run -C fastly.toml {}" 4 | } 5 | -------------------------------------------------------------------------------- /x/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package x contains temporary and/or experimental sub-packages. Functionality 4 | // under `x` should be considered unstable. Each package in `x` should replicate 5 | // this warning and explain any additional use considerations. There is 6 | // intentionally no source code in `x` itself. 7 | package x 8 | -------------------------------------------------------------------------------- /x/exp/handoff/handoff.go: -------------------------------------------------------------------------------- 1 | package handoff 2 | 3 | import "github.com/fastly/compute-sdk-go/internal/abi/fastly" 4 | 5 | // Websocket passes the WebSocket directly to a backend. 6 | // 7 | // This can only be used on services that have the WebSockets feature 8 | // enabled and on requests that are valid WebSocket requests. The sending 9 | // completes in the background. 10 | // 11 | // Once this method has been called, no other response can be sent to this 12 | // request, and the application can exit without affecting the send. 13 | func Websocket(backend string) error { 14 | return fastly.HandoffWebsocket(backend) 15 | 16 | } 17 | 18 | // Fanout passes the request through the Fanout GRIP proxy and on to 19 | // a backend. 20 | // 21 | // This can only be used on services that have the Fanout feature enabled. 22 | // 23 | // The sending completes in the background. Once this method has been 24 | // called, no other response can be sent to this request, and the 25 | // application can exit without affecting the send. 26 | func Fanout(backend string) error { 27 | return fastly.HandoffFanout(backend) 28 | } 29 | -------------------------------------------------------------------------------- /x/fstctx/ctxutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | package fstctx 4 | 5 | import ( 6 | "context" 7 | "time" 8 | ) 9 | 10 | // WithDeadline returns a copy of the parent context which will be cancelled 11 | // when the deadline arrives. Unlike context.WithDeadline the deadline will not 12 | // be adjusted, and it will not return the context.DeadlineExceeded error. 13 | // Canceling before the deadline will not clean up the spawned goroutine however 14 | // the goroutine will become a no-op. It is possible for pauses in Wasm 15 | // execution to cause the cancellation to happen after the deadline. This should 16 | // not be relied on for applications where a precise deadline is required. 17 | // 18 | // Canceling this context releases resources associated with it, so code should 19 | // call cancel as soon as the operations running in this Context complete. 20 | func WithDeadline(parent context.Context, d time.Time) (context.Context, context.CancelFunc) { 21 | return WithTimeout(parent, time.Until(d)) 22 | } 23 | 24 | // WithTimeout returns a copy of the parent context which will be cancelled 25 | // after the provided timeout. Unlike context.WithTimeout the deadline will not 26 | // be adjusted, and it will not return the context.DeadlineExceeded error. 27 | // Canceling before the timeout will not clean up the spawned goroutine 28 | // however the goroutine will become a no-op. It is possible for pauses in Wasm 29 | // execution to cause the timeout to be delayed. This should not be relied on 30 | // for applications where a precise timout is required. 31 | // 32 | // Canceling this context releases resources associated with it, so code should 33 | // call cancel as soon as the operations running in this Context complete: 34 | // 35 | // func slowOperationWithTimeout(ctx context.Context) (Result, error) { 36 | // ctx, cancel := fstctx.WithTimeout(ctx, 100*time.Millisecond) 37 | // defer cancel() // releases resources if slowOperation completes before timeout elapses 38 | // return slowOperation(ctx) 39 | // } 40 | func WithTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { 41 | ctx, cancel := context.WithCancel(parent) 42 | go func() { 43 | time.Sleep(timeout) 44 | if ctx.Err() == nil { 45 | cancel() 46 | } 47 | }() 48 | 49 | return ctx, cancel 50 | } 51 | -------------------------------------------------------------------------------- /x/fstctx/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Fastly, Inc. 2 | 3 | // Package fstctx provides alternatives to context.WithTimeout and 4 | // context.WithDeadline. At the time of writing, TinyGo does not support the 5 | // runtime methods needed to support those time based operations. 6 | // 7 | // All packages in `x`, including package fstctx, should be considered 8 | // temporary, experimental, and unstable. 9 | // 10 | // Deprecated: Use the standard library `context` package instead. 11 | package fstctx 12 | --------------------------------------------------------------------------------