diff --git a/_examples/bot-detection/fastly.toml b/_examples/bot-detection/fastly.toml new file mode 100644 index 0000000..d5cce19 --- /dev/null +++ b/_examples/bot-detection/fastly.toml @@ -0,0 +1,8 @@ +# This file describes a Fastly Compute package. To learn more visit: +# https://developer.fastly.com/reference/fastly-toml/ + +authors = ["oss@fastly.com"] +description = "" +language = "go" +manifest_version = 2 +name = "bot-detection" diff --git a/_examples/bot-detection/main.go b/_examples/bot-detection/main.go new file mode 100644 index 0000000..0edc6ec --- /dev/null +++ b/_examples/bot-detection/main.go @@ -0,0 +1,25 @@ +// Copyright 2022 Fastly, Inc. + +package main + +import ( + "context" + "fmt" + "log" + + "github.com/fastly/compute-sdk-go/fsthttp" +) + +func main() { + fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { + bot, _ := r.BotDetection() + + if bot.Analyzed { + if bot.Detected { + log.Println(w, "request from bot:", bot.Category, bot.Name) + } + } + + fmt.Fprintf(w, "Hello, %s!\n", r.RemoteAddr) + }) +} diff --git a/fsthttp/bot.go b/fsthttp/bot.go new file mode 100644 index 0000000..fbf09db --- /dev/null +++ b/fsthttp/bot.go @@ -0,0 +1,117 @@ +package fsthttp + +import "github.com/fastly/compute-sdk-go/internal/abi/fastly" + +type BotCategory = fastly.BotCategory + +const ( + // BotCategoryNone indicates bot detection was not executed, or no bot was detected. + BotCategoryNone = fastly.BotCategoryNone + + // BotCategorySuspected is for a suspected bot. + BotCategorySuspected = fastly.BotCategorySuspected + + // BotCategoryAccessibility is for tools that make content accessible (e.g., screen readers). + BotCategoryAccessibility = fastly.BotCategoryAccessibility + + // BotCategoryAICrawler is for crawlers used for training AIs and LLMs, generally used for building AI models or indexes. + BotCategoryAICrawler = fastly.BotCategoryAICrawler + + // BotCategoryAIFetcher is for fetchers used by AIs and LLMs for enriching results in response to a user query. + BotCategoryAIFetcher = fastly.BotCategoryAIFetcher + + // BotCategoryContentFetcher is for tools that extract content from websites to be used elsewhere. + BotCategoryContentFetcher = fastly.BotCategoryContentFetcher + + // BotCategoryMonitoringSiteTools is for tools that access your website to monitor things like performance, uptime, and proving domain control. + BotCategoryMonitoringSiteTools = fastly.BotCategoryMonitoringSiteTools + + // BotCategoryOnlineMarketing is for crawlers from online marketing platforms (e.g., Facebook, Pinterest). + BotCategoryOnlineMarketing = fastly.BotCategoryOnlineMarketing + + // BotCategoryPagePreview is for tools that access your website to show a preview of the page in other online services and social media platforms. + BotCategoryPagePreview = fastly.BotCategoryPagePreview + + // BotCategoryPlatformIntegrations is for integration with other platforms by accessing the website's API, notably Webhooks. + BotCategoryPlatformIntegrations = fastly.BotCategoryPlatformIntegrations + + // BotCategoryResearch is for commercial and academic tools that collect and analyze data for research purposes. + BotCategoryResearch = fastly.BotCategoryResearch + + // BotCategorySearchEngineCrawler is for crawlers that index your website for search engines. + BotCategorySearchEngineCrawler = fastly.BotCategorySearchEngineCrawler + + // BotCategorySearchEngineSpecialization is for tools that support search engine optimization tasks (e.g., link analysis, ranking). + BotCategorySearchEngineSpecialization = fastly.BotCategorySearchEngineSpecialization + + // BotCategorySecurityTools is for security analysis tools that inspect your website for vulnerabilities, misconfigurations and other security features. + BotCategorySecurityTools = fastly.BotCategorySecurityTools + + // BotCategoryUnknown indicates the detected bot belongs to a category not recognized by this SDK version. + BotCategoryUnknown = fastly.BotCategoryUnknown +) + +type BotDetectionResult struct { + // Analyzed indicates if the request was analyzed by the bot detection framework. + Analyzed bool + + // Detected indicates if a bot was detected. + Detected bool + + // Name is string identifying the specific bot detected (e.g., `GoogleBot`, `GPTBot`, `Bingbot`). + // Returns the empty string if bot detection was not executed or no bot was detected. + // + // Note: String values may change over time. Use this for logging or informational purposes. + // For conditional logic, use CategoryKind. + Name string + + // Category is a string indicating the type of bot detected (e.g., `SEARCH-ENGINE-CRAWLER`, `AI-CRAWLER`, + // `SUSPECTED-BOT`). + // + // Note: String values may change over time. Use this for logging or informational purposes. + // For conditional logic, use [`get_bot_category_kind()`][Self::get_bot_category_kind]. + Category string + + // An enum uniquely identifying the type of bot detected. + CategoryKind BotCategory + + // Verified is whether the detected bot is a verified bot. + Verfied bool +} + +func (r *Request) BotDetection() (*BotDetectionResult, error) { + var result BotDetectionResult + + var err error + if result.Analyzed, err = r.downstream.req.DownstreamBotAnalyzed(); err != nil { + return nil, err + } + + // Didn't analyze the request? Nothing else to do. + if !result.Analyzed { + return &result, nil + } + + if result.Detected, err = r.downstream.req.DownstreamBotDetected(); err != nil { + return nil, err + } + + // Request wasn't detected as a bot? Nothing to fill in. + if !result.Detected { + return &result, nil + } + + if result.Name, err = r.downstream.req.DownstreamBotName(); err != nil { + return nil, err + } + + if result.Category, err = r.downstream.req.DownstreamBotCategory(); err != nil { + return nil, err + } + + if result.Verfied, err = r.downstream.req.DownstreamBotVerified(); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index 247daa0..73cc9b7 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -147,6 +147,30 @@ func (r *HTTPRequest) DownstreamFastlyKeyIsValid() (bool, error) { return false, fmt.Errorf("not implemented") } +func (r *HTTPRequest) DownstreamBotAnalyzed() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamBotDetected() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamBotName() (string, error) { + return "", fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamBotCategory() (string, error) { + return "", fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamBotCategoryKind() (uint32, error) { + return 0, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamBotVerified() (bool, error) { + return false, fmt.Errorf("not implemented") +} + func NewHTTPRequest() (*HTTPRequest, error) { return nil, fmt.Errorf("not implemented") } diff --git a/internal/abi/fastly/http_guest.go b/internal/abi/fastly/http_guest.go index 51a7987..59c1403 100644 --- a/internal/abi/fastly/http_guest.go +++ b/internal/abi/fastly/http_guest.go @@ -1832,6 +1832,190 @@ func (r *HTTPRequest) DownstreamFastlyKeyIsValid() (bool, error) { return valid.b, nil } +// witx: +// +// (@interface func (export "downstream_bot_analyzed") +// (param $req $request_handle) +// (result $err (expected $bot_analyzed (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_bot_analyzed +//go:noescape +func fastlyHTTPDownstreamBotAnalyzed( + req requestHandle, + analyzed prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamBotAnalyzed() (bool, error) { + var analyzed struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamBotAnalyzed( + r.h, + prim.ToPointer(&analyzed.b), + ).toError(); err != nil { + return false, err + } + + return analyzed.b, nil +} + +// witx: +// +// (@interface func (export "downstream_bot_detected") +// (param $req $request_handle) +// (result $err (expected $bot_detected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_bot_detected +//go:noescape +func fastlyHTTPDownstreamBotDetected( + req requestHandle, + analyzed prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamBotDetected() (bool, error) { + var detected struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamBotDetected( + r.h, + prim.ToPointer(&detected.b), + ).toError(); err != nil { + return false, err + } + + return detected.b, nil +} + +// witx: +// +// (@interface func (export "downstream_bot_name") +// (param $req $request_handle) +// (param $bot_name_out (@witx pointer (@witx char8))) +// (param $bot_name_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_bot_name +//go:noescape +func fastlyHTTPReqDownstreamBotName( + req requestHandle, + botNameOut prim.Pointer[prim.Char8], + botNameMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +// DownstreamBotName returns the bot name detected +func (r *HTTPRequest) DownstreamBotName() (string, error) { + value, err := withAdaptiveBuffer(DefaultSmallBufLen, func(buf *prim.WriteBuffer) FastlyStatus { + return fastlyHTTPReqDownstreamBotName( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + }) + if err != nil { + return "", err + } + return value.ToString(), nil +} + +// witx: +// +// (@interface func (export "downstream_bot_category") +// (param $req $request_handle) +// (param $bot_category_out (@witx pointer (@witx char8))) +// (param $bot_category_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_bot_category +//go:noescape +func fastlyHTTPReqDownstreamBotCategory( + req requestHandle, + botCategoryOut prim.Pointer[prim.Char8], + botCategoryMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +// DownstreamBotCategory returns the bot category +func (r *HTTPRequest) DownstreamBotCategory() (string, error) { + value, err := withAdaptiveBuffer(DefaultSmallBufLen, func(buf *prim.WriteBuffer) FastlyStatus { + return fastlyHTTPReqDownstreamBotCategory( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + }) + if err != nil { + return "", err + } + return value.ToString(), nil +} + +// witx: +// +// (@interface func (export "downstream_bot_category_kind") +// (param $req $request_handle) +// (result $err (expected $bot_category_kind (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_bot_category_kind +//go:noescape +func fastlyHTTPDownstreamBotCategoryKind( + req requestHandle, + kind prim.Pointer[prim.U32], +) FastlyStatus + +func (r *HTTPRequest) DownstreamBotCategoryKind() (uint32, error) { + var kind prim.U32 + if err := fastlyHTTPDownstreamBotCategoryKind( + r.h, + prim.ToPointer(&kind), + ).toError(); err != nil { + return 0, err + } + + return uint32(kind), nil +} + +// witx: +// +// (@interface func (export "downstream_bot_verified") +// +// (param $req $request_handle) +// (result $err (expected $bot_verified (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_bot_verified +//go:noescape +func fastlyHTTPDownstreamBotVerified( + req requestHandle, + analyzed prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamBotVerified() (bool, error) { + var verified struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamBotVerified( + r.h, + prim.ToPointer(&verified.b), + ).toError(); err != nil { + return false, err + } + + return verified.b, nil +} + // witx: // // ;;; Hostcall for Fastly Compute guests to inspect request HTTP traffic diff --git a/internal/abi/fastly/types.go b/internal/abi/fastly/types.go index b26adf3..a7f36e9 100644 --- a/internal/abi/fastly/types.go +++ b/internal/abi/fastly/types.go @@ -1629,6 +1629,61 @@ func tlsAlertString(id prim.U8) string { } } +type BotCategory int + +const ( + BotCategoryNone BotCategory = 0 + BotCategorySuspected BotCategory = 1 + BotCategoryAccessibility BotCategory = 2 + BotCategoryAICrawler BotCategory = 3 + BotCategoryAIFetcher BotCategory = 4 + BotCategoryContentFetcher BotCategory = 5 + BotCategoryMonitoringSiteTools BotCategory = 6 + BotCategoryOnlineMarketing BotCategory = 7 + BotCategoryPagePreview BotCategory = 8 + BotCategoryPlatformIntegrations BotCategory = 9 + BotCategoryResearch BotCategory = 10 + BotCategorySearchEngineCrawler BotCategory = 11 + BotCategorySearchEngineSpecialization BotCategory = 12 + BotCategorySecurityTools BotCategory = 13 + BotCategoryUnknown BotCategory = -1 +) + +func (c BotCategory) String() string { + + switch c { + case BotCategoryNone: + return "None" + case BotCategorySuspected: + return "Suspected" + case BotCategoryAccessibility: + return "Accessibility" + case BotCategoryAICrawler: + return "AICrawler" + case BotCategoryAIFetcher: + return "AIFetcher" + case BotCategoryContentFetcher: + return "ContentFetcher" + case BotCategoryMonitoringSiteTools: + return "MonitoringSiteTools" + case BotCategoryOnlineMarketing: + return "OnlineMarketing" + case BotCategoryPagePreview: + return "PagePreview" + case BotCategoryPlatformIntegrations: + return "PlatformIntegrations" + case BotCategoryResearch: + return "Research" + case BotCategorySearchEngineCrawler: + return "SearchEngineCrawler" + case BotCategorySearchEngineSpecialization: + return "SearchEngineSpecialization" + case BotCategorySecurityTools: + return "SecurityTools" + } + return "Unknown" +} + type RateWindow struct { value prim.U32 }