Merge pull request #88 from aki-0421/incoming-properties

Support Incoming Properties
This commit is contained in:
syumai 2024-01-24 21:08:56 +09:00 committed by GitHub
commit 3b8e2550b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 299 additions and 66 deletions

View File

@ -2,20 +2,19 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/syumai/workers/cloudflare"
"github.com/syumai/workers/cloudflare/cron" "github.com/syumai/workers/cloudflare/cron"
) )
func task(ctx context.Context, event *cron.Event) error { func task(ctx context.Context) error {
fmt.Println(cloudflare.Getenv(ctx, "HELLO")) e, err := cron.NewEvent(ctx)
if err != nil {
if event.ScheduledTime.Minute()%2 == 0 { return err
return errors.New("even numbers cause errors")
} }
fmt.Println(e.ScheduledTime.Unix())
return nil return nil
} }

View File

@ -3,9 +3,6 @@ main = "./build/worker.mjs"
compatibility_date = "2023-02-24" compatibility_date = "2023-02-24"
workers_dev = false workers_dev = false
[vars]
HELLO = "hello, world!"
[triggers] [triggers]
crons = ["* * * * *"] crons = ["* * * * *"]

3
_examples/incoming/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build
node_modules
.wrangler

View File

@ -0,0 +1,12 @@
.PHONY: dev
dev:
wrangler dev
.PHONY: build
build:
go run ../../cmd/workers-assets-gen
tinygo build -o ./build/app.wasm -target wasm -no-debug ./...
.PHONY: deploy
deploy:
wrangler deploy

View File

@ -0,0 +1,7 @@
module github.com/syumai/workers/_examples/incoming
go 1.21.3
require github.com/syumai/workers v0.0.0
replace github.com/syumai/workers => ../../

View File

@ -0,0 +1,2 @@
github.com/syumai/workers v0.1.0 h1:z5QfQR2X+PCKzom7RodpI5J4D5YF7NT7Qwzb9AM9dgY=
github.com/syumai/workers v0.1.0/go.mod h1:alXIDhTyeTwSzh0ZgQ3cb9HQPyyYfIejupE4Z3efr14=

View File

@ -0,0 +1,27 @@
package main
import (
"encoding/json"
"net/http"
"github.com/syumai/workers"
"github.com/syumai/workers/cloudflare/fetch"
)
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
p, err := fetch.NewIncomingProperties(req.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
encoder := json.NewEncoder(w)
w.Header().Set("Content-Type", "application/json")
if err := encoder.Encode(p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
workers.Serve(handler)
}

View File

@ -0,0 +1,9 @@
name = "incoming"
main = "./build/worker.mjs"
compatibility_date = "2022-05-13"
compatibility_flags = [
"streams_enable_constructors"
]
[build]
command = "make build"

28
cloudflare/cron/event.go Normal file
View File

@ -0,0 +1,28 @@
package cron
import (
"context"
"errors"
"time"
"github.com/syumai/workers/internal/runtimecontext"
)
// Event represents information about the Cron that invoked this worker.
type Event struct {
Cron string
ScheduledTime time.Time
}
func NewEvent(ctx context.Context) (*Event, error) {
obj := runtimecontext.MustExtractTriggerObj(ctx)
if obj.IsUndefined() {
return nil, errors.New("event is null")
}
scheduledTimeVal := obj.Get("scheduledTime").Float()
return &Event{
Cron: obj.Get("cron").String(),
ScheduledTime: time.Unix(int64(scheduledTimeVal)/1000, 0).UTC(),
}, nil
}

View File

@ -2,53 +2,20 @@ package cron
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"syscall/js" "syscall/js"
"time"
"github.com/syumai/workers/internal/jsutil" "github.com/syumai/workers/internal/jsutil"
"github.com/syumai/workers/internal/runtimecontext" "github.com/syumai/workers/internal/runtimecontext"
) )
// Event represents information about the Cron that invoked this worker. type Task func(ctx context.Context) error
type Event struct {
Cron string
ScheduledTime time.Time
}
// toEvent converts JS Object to Go Event struct
func toEvent(obj js.Value) (*Event, error) {
if obj.IsUndefined() {
return nil, errors.New("event is null")
}
cronVal := obj.Get("cron").String()
scheduledTimeVal := obj.Get("scheduledTime").Float()
return &Event{
Cron: cronVal,
ScheduledTime: time.Unix(int64(scheduledTimeVal)/1000, 0).UTC(),
}, nil
}
type Task func(ctx context.Context, event *Event) error
var scheduledTask Task var scheduledTask Task
// ScheduleTask sets the Task to be executed
func ScheduleTask(task Task) {
scheduledTask = task
js.Global().Call("ready")
select {}
}
func runScheduler(eventObj js.Value, runtimeCtxObj js.Value) error { func runScheduler(eventObj js.Value, runtimeCtxObj js.Value) error {
ctx := runtimecontext.New(context.Background(), runtimeCtxObj) ctx := runtimecontext.New(context.Background(), eventObj, runtimeCtxObj)
event, err := toEvent(eventObj) if err := scheduledTask(ctx); err != nil {
if err != nil {
return err
}
err = scheduledTask(ctx, event)
if err != nil {
return err return err
} }
return nil return nil
@ -56,18 +23,17 @@ func runScheduler(eventObj js.Value, runtimeCtxObj js.Value) error {
func init() { func init() {
runSchedulerCallback := js.FuncOf(func(_ js.Value, args []js.Value) any { runSchedulerCallback := js.FuncOf(func(_ js.Value, args []js.Value) any {
if len(args) != 2 { if len(args) != 1 {
panic(fmt.Errorf("invalid number of arguments given to runScheduler: %d", len(args))) panic(fmt.Errorf("invalid number of arguments given to runScheduler: %d", len(args)))
} }
event := args[0] eventObj := args[0]
runtimeCtx := args[1] runtimeCtxObj := jsutil.RuntimeContext
var cb js.Func var cb js.Func
cb = js.FuncOf(func(_ js.Value, pArgs []js.Value) any { cb = js.FuncOf(func(_ js.Value, pArgs []js.Value) any {
defer cb.Release() defer cb.Release()
resolve := pArgs[0] resolve := pArgs[0]
go func() { go func() {
err := runScheduler(event, runtimeCtx) err := runScheduler(eventObj, runtimeCtxObj)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -80,3 +46,13 @@ func init() {
}) })
jsutil.Binding.Set("runScheduler", runSchedulerCallback) jsutil.Binding.Set("runScheduler", runSchedulerCallback)
} }
//go:wasmimport workers ready
func ready()
// ScheduleTask sets the Task to be executed
func ScheduleTask(task Task) {
scheduledTask = task
ready()
select {}
}

View File

@ -15,7 +15,8 @@ func fetch(namespace js.Value, req *http.Request, init *RequestInit) (*http.Resp
if namespace.IsUndefined() { if namespace.IsUndefined() {
return nil, errors.New("fetch function not found") return nil, errors.New("fetch function not found")
} }
promise := namespace.Call("fetch", fetchObj := namespace.Get("fetch")
promise := fetchObj.Invoke(
// The Request object to fetch. // The Request object to fetch.
// Docs: https://developers.cloudflare.com/workers/runtime-apis/request // Docs: https://developers.cloudflare.com/workers/runtime-apis/request
jshttp.ToJSRequest(req), jshttp.ToJSRequest(req),
@ -23,9 +24,11 @@ func fetch(namespace js.Value, req *http.Request, init *RequestInit) (*http.Resp
// Docs: https://developers.cloudflare.com/workers/runtime-apis/request#requestinit // Docs: https://developers.cloudflare.com/workers/runtime-apis/request#requestinit
init.ToJS(), init.ToJS(),
) )
jsRes, err := jsutil.AwaitPromise(promise) jsRes, err := jsutil.AwaitPromise(promise)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return jshttp.ToResponse(jsRes) return jshttp.ToResponse(jsRes)
} }

View File

@ -1,9 +1,12 @@
package fetch package fetch
import ( import (
"context"
"errors"
"syscall/js" "syscall/js"
"github.com/syumai/workers/internal/jsutil" "github.com/syumai/workers/internal/jsutil"
"github.com/syumai/workers/internal/runtimecontext"
) )
// RedirectMode represents the redirect mode of a fetch() request. // RedirectMode represents the redirect mode of a fetch() request.
@ -45,3 +48,158 @@ func (init *RequestInit) ToJS() js.Value {
type RequestInitCF struct { type RequestInitCF struct {
/* TODO: implement */ /* TODO: implement */
} }
type IncomingBotManagementJsDetection struct {
Passed bool
}
func NewIncomingBotManagementJsDetection(cf js.Value) *IncomingBotManagementJsDetection {
if cf.IsUndefined() {
return nil
}
return &IncomingBotManagementJsDetection{
Passed: cf.Get("passed").Bool(),
}
}
type IncomingBotManagement struct {
CorporateProxy bool
VerifiedBot bool
JsDetection *IncomingBotManagementJsDetection
StaticResource bool
Score int
}
func NewIncomingBotManagement(cf js.Value) *IncomingBotManagement {
if cf.IsUndefined() {
return nil
}
return &IncomingBotManagement{
CorporateProxy: cf.Get("corporateProxy").Bool(),
VerifiedBot: cf.Get("verifiedBot").Bool(),
JsDetection: NewIncomingBotManagementJsDetection(cf.Get("jsDetection")),
StaticResource: cf.Get("staticResource").Bool(),
Score: cf.Get("score").Int(),
}
}
type IncomingTLSClientAuth struct {
CertIssuerDNLegacy string
CertIssuerSKI string
CertSubjectDNRFC2253 string
CertSubjectDNLegacy string
CertFingerprintSHA256 string
CertNotBefore string
CertSKI string
CertSerial string
CertIssuerDN string
CertVerified string
CertNotAfter string
CertSubjectDN string
CertPresented string
CertRevoked string
CertIssuerSerial string
CertIssuerDNRFC2253 string
CertFingerprintSHA1 string
}
func NewIncomingTLSClientAuth(cf js.Value) *IncomingTLSClientAuth {
if cf.IsUndefined() {
return nil
}
return &IncomingTLSClientAuth{
CertIssuerDNLegacy: jsutil.MaybeString(cf.Get("certIssuerDNLegacy")),
CertIssuerSKI: jsutil.MaybeString(cf.Get("certIssuerSKI")),
CertSubjectDNRFC2253: jsutil.MaybeString(cf.Get("certSubjectDNRFC2253")),
CertSubjectDNLegacy: jsutil.MaybeString(cf.Get("certSubjectDNLegacy")),
CertFingerprintSHA256: jsutil.MaybeString(cf.Get("certFingerprintSHA256")),
CertNotBefore: jsutil.MaybeString(cf.Get("certNotBefore")),
CertSKI: jsutil.MaybeString(cf.Get("certSKI")),
CertSerial: jsutil.MaybeString(cf.Get("certSerial")),
CertIssuerDN: jsutil.MaybeString(cf.Get("certIssuerDN")),
CertVerified: jsutil.MaybeString(cf.Get("certVerified")),
CertNotAfter: jsutil.MaybeString(cf.Get("certNotAfter")),
CertSubjectDN: jsutil.MaybeString(cf.Get("certSubjectDN")),
CertPresented: jsutil.MaybeString(cf.Get("certPresented")),
CertRevoked: jsutil.MaybeString(cf.Get("certRevoked")),
CertIssuerSerial: jsutil.MaybeString(cf.Get("certIssuerSerial")),
CertIssuerDNRFC2253: jsutil.MaybeString(cf.Get("certIssuerDNRFC2253")),
CertFingerprintSHA1: jsutil.MaybeString(cf.Get("certFingerprintSHA1")),
}
}
type IncomingTLSExportedAuthenticator struct {
ClientFinished string
ClientHandshake string
ServerHandshake string
ServerFinished string
}
func NewIncomingTLSExportedAuthenticator(cf js.Value) *IncomingTLSExportedAuthenticator {
if cf.IsUndefined() {
return nil
}
return &IncomingTLSExportedAuthenticator{
ClientFinished: jsutil.MaybeString(cf.Get("clientFinished")),
ClientHandshake: jsutil.MaybeString(cf.Get("clientHandshake")),
ServerHandshake: jsutil.MaybeString(cf.Get("serverHandshake")),
ServerFinished: jsutil.MaybeString(cf.Get("serverFinished")),
}
}
type IncomingProperties struct {
Longitude string
Latitude string
TLSCipher string
Continent string
Asn int
ClientAcceptEncoding string
Country string
TLSClientAuth *IncomingTLSClientAuth
TLSExportedAuthenticator *IncomingTLSExportedAuthenticator
TLSVersion string
Colo string
Timezone string
City string
VerifiedBotCategory string
// EdgeRequestKeepAliveStatus int
RequestPriority string
HttpProtocol string
Region string
RegionCode string
AsOrganization string
PostalCode string
BotManagement *IncomingBotManagement
}
func NewIncomingProperties(ctx context.Context) (*IncomingProperties, error) {
obj := runtimecontext.MustExtractTriggerObj(ctx)
cf := obj.Get("cf")
if cf.IsUndefined() {
return nil, errors.New("runtime is not cloudflare")
}
return &IncomingProperties{
Longitude: jsutil.MaybeString(cf.Get("longitude")),
Latitude: jsutil.MaybeString(cf.Get("latitude")),
TLSCipher: jsutil.MaybeString(cf.Get("tlsCipher")),
Continent: jsutil.MaybeString(cf.Get("continent")),
Asn: cf.Get("asn").Int(),
ClientAcceptEncoding: jsutil.MaybeString(cf.Get("clientAcceptEncoding")),
Country: jsutil.MaybeString(cf.Get("country")),
TLSClientAuth: NewIncomingTLSClientAuth(cf.Get("tlsClientAuth")),
TLSExportedAuthenticator: NewIncomingTLSExportedAuthenticator(cf.Get("tlsExportedAuthenticator")),
TLSVersion: cf.Get("tlsVersion").String(),
Colo: cf.Get("colo").String(),
Timezone: cf.Get("timezone").String(),
City: jsutil.MaybeString(cf.Get("city")),
VerifiedBotCategory: jsutil.MaybeString(cf.Get("verifiedBotCategory")),
RequestPriority: jsutil.MaybeString(cf.Get("requestPriority")),
HttpProtocol: cf.Get("httpProtocol").String(),
Region: jsutil.MaybeString(cf.Get("region")),
RegionCode: jsutil.MaybeString(cf.Get("regionCode")),
AsOrganization: cf.Get("asOrganization").String(),
PostalCode: jsutil.MaybeString(cf.Get("postalCode")),
BotManagement: NewIncomingBotManagement(cf.Get("botManagement")),
}, nil
}

View File

@ -49,8 +49,8 @@ var ErrValueNotFound = errors.New("execution context value for specified key not
// GetRuntimeContextValue gets value for specified key from RuntimeContext. // GetRuntimeContextValue gets value for specified key from RuntimeContext.
// - if the value is undefined, return error. // - if the value is undefined, return error.
func GetRuntimeContextValue(ctx context.Context, key string) (js.Value, error) { func GetRuntimeContextValue(ctx context.Context, key string) (js.Value, error) {
runtimeCtxValue := runtimecontext.MustExtract(ctx) runtimeObj := runtimecontext.MustExtractRuntimeObj(ctx)
v := runtimeCtxValue.Get(key) v := runtimeObj.Get(key)
if v.IsUndefined() { if v.IsUndefined() {
return js.Value{}, ErrValueNotFound return js.Value{}, ErrValueNotFound
} }

View File

@ -49,7 +49,7 @@ func handleRequest(reqObj js.Value, runtimeCtxObj js.Value) (js.Value, error) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
ctx := runtimecontext.New(context.Background(), runtimeCtxObj) ctx := runtimecontext.New(context.Background(), reqObj, runtimeCtxObj)
req = req.WithContext(ctx) req = req.WithContext(ctx)
reader, writer := io.Pipe() reader, writer := io.Pipe()
w := &jshttp.ResponseWriter{ w := &jshttp.ResponseWriter{

View File

@ -2,24 +2,36 @@ package runtimecontext
import ( import (
"context" "context"
"errors"
"syscall/js" "syscall/js"
) )
type runtimeCtxKey struct{} type (
contextKeyTriggerObj struct{}
contextKeyRuntimeObj struct{}
)
func New(ctx context.Context, runtimeCtxObj js.Value) context.Context { func New(ctx context.Context, triggerObj, runtimeObj js.Value) context.Context {
return context.WithValue(ctx, runtimeCtxKey{}, runtimeCtxObj) ctx = context.WithValue(ctx, contextKeyTriggerObj{}, triggerObj)
ctx = context.WithValue(ctx, contextKeyRuntimeObj{}, runtimeObj)
return ctx
} }
var ErrRuntimeContextNotFound = errors.New("runtime context was not found") // MustExtractTriggerObj extracts trigger object from context.
// This function panics when trigger object was not found.
// MustExtract extracts runtime context object from context. func MustExtractTriggerObj(ctx context.Context) js.Value {
// This function panics when runtime context object was not found. v, ok := ctx.Value(contextKeyTriggerObj{}).(js.Value)
func MustExtract(ctx context.Context) js.Value {
v, ok := ctx.Value(runtimeCtxKey{}).(js.Value)
if !ok { if !ok {
panic(ErrRuntimeContextNotFound) panic("trigger object was not found")
}
return v
}
// MustExtractRuntimeObj extracts runtime object from context.
// This function panics when runtime object was not found.
func MustExtractRuntimeObj(ctx context.Context) js.Value {
v, ok := ctx.Value(contextKeyRuntimeObj{}).(js.Value)
if !ok {
panic("runtime object was not found")
} }
return v return v
} }