diff --git a/_examples/cron/main.go b/_examples/cron/main.go index 0b01bf5..73c3ec9 100644 --- a/_examples/cron/main.go +++ b/_examples/cron/main.go @@ -2,20 +2,19 @@ package main import ( "context" - "errors" "fmt" - "github.com/syumai/workers/cloudflare" "github.com/syumai/workers/cloudflare/cron" ) -func task(ctx context.Context, event *cron.Event) error { - fmt.Println(cloudflare.Getenv(ctx, "HELLO")) - - if event.ScheduledTime.Minute()%2 == 0 { - return errors.New("even numbers cause errors") +func task(ctx context.Context) error { + e, err := cron.NewEvent(ctx) + if err != nil { + return err } + fmt.Println(e.ScheduledTime.Unix()) + return nil } diff --git a/_examples/cron/wrangler.toml b/_examples/cron/wrangler.toml index c6d714e..e09dddd 100644 --- a/_examples/cron/wrangler.toml +++ b/_examples/cron/wrangler.toml @@ -3,9 +3,6 @@ main = "./build/worker.mjs" compatibility_date = "2023-02-24" workers_dev = false -[vars] -HELLO = "hello, world!" - [triggers] crons = ["* * * * *"] diff --git a/_examples/incoming/.gitignore b/_examples/incoming/.gitignore new file mode 100644 index 0000000..aee7b7e --- /dev/null +++ b/_examples/incoming/.gitignore @@ -0,0 +1,3 @@ +build +node_modules +.wrangler diff --git a/_examples/incoming/Makefile b/_examples/incoming/Makefile new file mode 100644 index 0000000..019492c --- /dev/null +++ b/_examples/incoming/Makefile @@ -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 diff --git a/_examples/incoming/go.mod b/_examples/incoming/go.mod new file mode 100644 index 0000000..d54651d --- /dev/null +++ b/_examples/incoming/go.mod @@ -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 => ../../ diff --git a/_examples/incoming/go.sum b/_examples/incoming/go.sum new file mode 100644 index 0000000..8c27871 --- /dev/null +++ b/_examples/incoming/go.sum @@ -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= diff --git a/_examples/incoming/main.go b/_examples/incoming/main.go new file mode 100644 index 0000000..21ac872 --- /dev/null +++ b/_examples/incoming/main.go @@ -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) +} diff --git a/_examples/incoming/wrangler.toml b/_examples/incoming/wrangler.toml new file mode 100644 index 0000000..5c85632 --- /dev/null +++ b/_examples/incoming/wrangler.toml @@ -0,0 +1,9 @@ +name = "incoming" +main = "./build/worker.mjs" +compatibility_date = "2022-05-13" +compatibility_flags = [ + "streams_enable_constructors" +] + +[build] +command = "make build" diff --git a/cloudflare/cron/event.go b/cloudflare/cron/event.go new file mode 100644 index 0000000..96d104f --- /dev/null +++ b/cloudflare/cron/event.go @@ -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 +} diff --git a/cloudflare/cron/cron.go b/cloudflare/cron/scheduler.go similarity index 50% rename from cloudflare/cron/cron.go rename to cloudflare/cron/scheduler.go index 9f6d6e1..d559568 100644 --- a/cloudflare/cron/cron.go +++ b/cloudflare/cron/scheduler.go @@ -2,53 +2,20 @@ package cron import ( "context" - "errors" "fmt" "syscall/js" - "time" "github.com/syumai/workers/internal/jsutil" "github.com/syumai/workers/internal/runtimecontext" ) -// Event represents information about the Cron that invoked this worker. -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 +type Task func(ctx context.Context) error 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 { - ctx := runtimecontext.New(context.Background(), runtimeCtxObj) - event, err := toEvent(eventObj) - if err != nil { - return err - } - err = scheduledTask(ctx, event) - if err != nil { + ctx := runtimecontext.New(context.Background(), eventObj, runtimeCtxObj) + if err := scheduledTask(ctx); err != nil { return err } return nil @@ -56,18 +23,17 @@ func runScheduler(eventObj js.Value, runtimeCtxObj js.Value) error { func init() { 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))) } - event := args[0] - runtimeCtx := args[1] - + eventObj := args[0] + runtimeCtxObj := jsutil.RuntimeContext var cb js.Func cb = js.FuncOf(func(_ js.Value, pArgs []js.Value) any { defer cb.Release() resolve := pArgs[0] go func() { - err := runScheduler(event, runtimeCtx) + err := runScheduler(eventObj, runtimeCtxObj) if err != nil { panic(err) } @@ -80,3 +46,13 @@ func init() { }) 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 {} +} diff --git a/cloudflare/fetch/bind.go b/cloudflare/fetch/bind.go index 9207cdb..afc6ea1 100644 --- a/cloudflare/fetch/bind.go +++ b/cloudflare/fetch/bind.go @@ -15,7 +15,8 @@ func fetch(namespace js.Value, req *http.Request, init *RequestInit) (*http.Resp if namespace.IsUndefined() { return nil, errors.New("fetch function not found") } - promise := namespace.Call("fetch", + fetchObj := namespace.Get("fetch") + promise := fetchObj.Invoke( // The Request object to fetch. // Docs: https://developers.cloudflare.com/workers/runtime-apis/request 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 init.ToJS(), ) + jsRes, err := jsutil.AwaitPromise(promise) if err != nil { return nil, err } + return jshttp.ToResponse(jsRes) } diff --git a/cloudflare/fetch/property.go b/cloudflare/fetch/property.go index 73fab4b..9343785 100644 --- a/cloudflare/fetch/property.go +++ b/cloudflare/fetch/property.go @@ -1,9 +1,12 @@ package fetch import ( + "context" + "errors" "syscall/js" "github.com/syumai/workers/internal/jsutil" + "github.com/syumai/workers/internal/runtimecontext" ) // RedirectMode represents the redirect mode of a fetch() request. @@ -45,3 +48,158 @@ func (init *RequestInit) ToJS() js.Value { type RequestInitCF struct { /* 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 +} diff --git a/cloudflare/internal/cfruntimecontext/cfruntimecontext.go b/cloudflare/internal/cfruntimecontext/cfruntimecontext.go index 731190d..b36d9cc 100644 --- a/cloudflare/internal/cfruntimecontext/cfruntimecontext.go +++ b/cloudflare/internal/cfruntimecontext/cfruntimecontext.go @@ -49,8 +49,8 @@ var ErrValueNotFound = errors.New("execution context value for specified key not // GetRuntimeContextValue gets value for specified key from RuntimeContext. // - if the value is undefined, return error. func GetRuntimeContextValue(ctx context.Context, key string) (js.Value, error) { - runtimeCtxValue := runtimecontext.MustExtract(ctx) - v := runtimeCtxValue.Get(key) + runtimeObj := runtimecontext.MustExtractRuntimeObj(ctx) + v := runtimeObj.Get(key) if v.IsUndefined() { return js.Value{}, ErrValueNotFound } diff --git a/handler.go b/handler.go index d1d732c..87398bb 100644 --- a/handler.go +++ b/handler.go @@ -49,7 +49,7 @@ func handleRequest(reqObj js.Value, runtimeCtxObj js.Value) (js.Value, error) { if err != nil { panic(err) } - ctx := runtimecontext.New(context.Background(), runtimeCtxObj) + ctx := runtimecontext.New(context.Background(), reqObj, runtimeCtxObj) req = req.WithContext(ctx) reader, writer := io.Pipe() w := &jshttp.ResponseWriter{ diff --git a/internal/runtimecontext/context.go b/internal/runtimecontext/context.go index aed2463..288e0cd 100644 --- a/internal/runtimecontext/context.go +++ b/internal/runtimecontext/context.go @@ -2,24 +2,36 @@ package runtimecontext import ( "context" - "errors" "syscall/js" ) -type runtimeCtxKey struct{} +type ( + contextKeyTriggerObj struct{} + contextKeyRuntimeObj struct{} +) -func New(ctx context.Context, runtimeCtxObj js.Value) context.Context { - return context.WithValue(ctx, runtimeCtxKey{}, runtimeCtxObj) +func New(ctx context.Context, triggerObj, runtimeObj js.Value) context.Context { + ctx = context.WithValue(ctx, contextKeyTriggerObj{}, triggerObj) + ctx = context.WithValue(ctx, contextKeyRuntimeObj{}, runtimeObj) + return ctx } -var ErrRuntimeContextNotFound = errors.New("runtime context was not found") - -// MustExtract extracts runtime context object from context. -// This function panics when runtime context object was not found. -func MustExtract(ctx context.Context) js.Value { - v, ok := ctx.Value(runtimeCtxKey{}).(js.Value) +// MustExtractTriggerObj extracts trigger object from context. +// This function panics when trigger object was not found. +func MustExtractTriggerObj(ctx context.Context) js.Value { + v, ok := ctx.Value(contextKeyTriggerObj{}).(js.Value) 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 }