diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..019f8e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Compiled binaries and build artifacts +*.exe +*.bin +/bin/ +/pkg/ + +# Module cache and dependencies (excluding go.mod and go.sum) +# go.mod and go.sum are typically tracked +# /vendor/ # Uncomment if you are not vendoring dependencies + +# Editor/IDE specific files +.vscode/ +.idea/ +*.swp + +# Log files and temporary data +*.log +*.tmp \ No newline at end of file diff --git a/api/auth/auth.go b/api/auth/auth.go new file mode 100644 index 0000000..5a2a9e9 --- /dev/null +++ b/api/auth/auth.go @@ -0,0 +1,180 @@ +package auth + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "log" + "net/http" + "net/url" + "os" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +// Authenticator is used to authenticate our users. +type Authenticator struct { + *oidc.Provider + oauth2.Config +} + +// New instantiates the *Authenticator. +func New() (*Authenticator, error) { + provider, err := oidc.NewProvider( + context.Background(), + "https://"+os.Getenv("AUTH0_DOMAIN")+"/", + ) + if err != nil { + return nil, err + } + + conf := oauth2.Config{ + ClientID: os.Getenv("AUTH0_CLIENT_ID"), + ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"), + RedirectURL: os.Getenv("AUTH0_CALLBACK_URL"), + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile", "email"}, + } + + return &Authenticator{ + Provider: provider, + Config: conf, + }, nil +} + +// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken. +func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) { + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + return nil, errors.New("no id_token field in oauth2 token") + } + + oidcConfig := &oidc.Config{ + ClientID: a.ClientID, + } + + return a.Verifier(oidcConfig).Verify(ctx, rawIDToken) +} + +// Handler for our login. +func LoginHandler(auth *Authenticator) gin.HandlerFunc { + return func(ctx *gin.Context) { + state, err := generateRandomState() + if err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + return + } + + // Save the state inside the session. + session := sessions.Default(ctx) + session.Set("state", state) + session.Options(sessions.Options{Path: "/"}) + if err := session.Save(); err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + return + } + audience_url := "https://" + os.Getenv("AUTH0_DOMAIN") + "/api/v2/" + auth_url := auth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("audience", audience_url)) + ctx.Redirect(http.StatusTemporaryRedirect, auth_url) + } +} + +// Handler for our logout. +func LogoutHandler(ctx *gin.Context) { + logoutUrl, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/v2/logout") + if err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + return + } + + scheme := "http" + if ctx.Request.TLS != nil { + scheme = "https" + } + + returnTo, err := url.Parse(scheme + "://" + ctx.Request.Host + os.Getenv("LOGOUT_CALLBACK_ENDPOINT")) + if err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + return + } + + parameters := url.Values{} + parameters.Add("returnTo", returnTo.String()) + parameters.Add("client_id", os.Getenv("AUTH0_CLIENT_ID")) + logoutUrl.RawQuery = parameters.Encode() + + ctx.Redirect(http.StatusTemporaryRedirect, logoutUrl.String()) +} + +func LogoutCallbackHandler(store cookie.Store) gin.HandlerFunc { + return func(ctx *gin.Context) { + session := sessions.Default(ctx) + session.Clear() + session.Options(sessions.Options{MaxAge: -1, Path: "/"}) + err := session.Save() + if err != nil { + ctx.String(http.StatusInternalServerError, "Failed to clear session") + } + + ctx.Redirect(http.StatusSeeOther, os.Getenv("PUBLIC_LOCATION")) + } +} + +func AuthenticationCallbackHandler(auth *Authenticator) gin.HandlerFunc { + return func(ctx *gin.Context) { + session := sessions.Default(ctx) + if ctx.Query("state") != session.Get("state") { + ctx.String(http.StatusBadRequest, "Invalid state parameter.") + return + } + + // Exchange an authorization code for a token. + token, err := auth.Exchange(ctx.Request.Context(), ctx.Query("code")) + if err != nil { + ctx.String(http.StatusUnauthorized, "Failed to convert an authorization code into a token.") + return + } + + idToken, err := auth.VerifyIDToken(ctx.Request.Context(), token) + if err != nil { + ctx.String(http.StatusInternalServerError, "Failed to verify ID Token.") + return + } + + var profile map[string]interface{} + if err := idToken.Claims(&profile); err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + return + } + + log.Println(token.ExpiresIn) + session.Set("access_token", token.AccessToken) + session.Set("refresh_token", token.RefreshToken) + session.Set("profile", profile) + if err := session.Save(); err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + return + } + + // Redirect to logged in page. + ctx.Redirect(http.StatusTemporaryRedirect, "/user") + } +} + +func generateRandomState() (string, error) { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + state := base64.StdEncoding.EncodeToString(b) + + return state, nil +} diff --git a/api/auth/middleware/authentication_middleware.go b/api/auth/middleware/authentication_middleware.go new file mode 100644 index 0000000..31bc729 --- /dev/null +++ b/api/auth/middleware/authentication_middleware.go @@ -0,0 +1,165 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "net/url" + "os" + "time" + + "git.conway.engineer/ada/ordr.git/auth" + "git.conway.engineer/ada/ordr.git/dto" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + + "github.com/google/go-querystring/query" +) + +func TokenIsNotExpired(access_token string) bool { + parser := new(jwt.Parser) + + token, err := parser.Parse(access_token, nil) + if err != nil && err.Error() != "no Keyfunc was provided." { + log.Fatal(err.Error()) + } + + claims, claims_ok := token.Claims.(jwt.MapClaims) + if !claims_ok { + log.Fatal("failed to read claims") + } + + exp, exp_ok := claims["exp"].(float64) + if !exp_ok { + log.Fatal("exp claim not present") + } + + return time.Now().Before(time.Unix(int64(exp), 0)) +} + +func GetUserProfile(context *gin.Context) { + + session := sessions.Default(context) + access_token := session.Get("access_token") + user_profile_client := http.Client{} + + user_profile_url, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + os.Getenv("AUTH0_USER_INFO_ENDPOINT")) + + if err != nil { + log.Println("Failed to build user profile url " + err.Error()) + context.Abort() + return + } + + user_profile_request, user_profile_request_error := http.NewRequest("GET", user_profile_url.String(), nil) + if user_profile_request_error != nil { + log.Println("Failed to initialize validation request", user_profile_request_error.Error()) + context.AbortWithStatus(http.StatusInternalServerError) + } + + user_profile_request.Header.Add("Accept", "application/json") + user_profile_request.Header.Add("Authorization", "Bearer "+access_token.(string)) + + user_profile_response, user_profile_response_err := user_profile_client.Do(user_profile_request) + if user_profile_response_err != nil { + log.Println("Failed to validate user") + context.AbortWithStatus(http.StatusInternalServerError) + } + + defer user_profile_response.Body.Close() + user_profile_bytes, _ := io.ReadAll(user_profile_response.Body) + + var user_profile dto.UserProfileResponse + json.Unmarshal(user_profile_bytes, &user_profile) + context.Set("user_profile", user_profile) + context.Next() +} + +func HandleRefreshToken(session sessions.Session) bool { + token_url, token_url_err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + os.Getenv("AUTH0_TOKEN_ENDPOINT")) + if token_url_err != nil { + log.Fatal("Failed to parse token url", token_url_err.Error()) + } + + refresh_token := session.Get("refresh_token") + refresh_request_dto := dto.RefreshTokenRequest{ + GrantType: "refresh_token", + ClientId: os.Getenv("AUTH0_CLIENT_ID"), + ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"), + RefreshToken: refresh_token.(string), + } + + refresh_body, marshal_err := query.Values(refresh_request_dto) + if marshal_err != nil { + log.Fatal("failed to marshal object: ", marshal_err) + } + + refresh_client := http.Client{} + refresh_request, refresh_request_err := http.NewRequest("POST", token_url.String(), bytes.NewReader([]byte(refresh_body.Encode()))) + if refresh_request_err != nil { + log.Fatal("Failed to create a request: ", refresh_request_err.Error()) + } + refresh_request.Header.Add("content-type", "application/x-www-form-urlencoded") + refresh_response, refresh_response_err := refresh_client.Do(refresh_request) + if refresh_response_err != nil { + log.Fatal("Failed to fetch refresh token and response: ", refresh_response_err.Error()) + } + defer refresh_response.Body.Close() + + if refresh_response.StatusCode == http.StatusOK { + var response_body dto.RefreshTokenResponse + json_decoder := json.NewDecoder(refresh_response.Body) + content_unmarshal_err := json_decoder.Decode(&response_body) + + if content_unmarshal_err != nil { + log.Println("Faileed to unmarshal data") + return false + } + + session.Set("access_token", response_body.AccessToken) + session.Set("refresh_token", response_body.RefreshToken) + session.Options(sessions.Options{Path: "/"}) + session_save_error := session.Save() + + if session_save_error != nil { + log.Println("Failed to set session: " + session_save_error.Error()) + return false + } + return true + } else { + return false + } +} + +func IsAuthenticated(auth *auth.Authenticator) gin.HandlerFunc { + return func(context *gin.Context) { + session := sessions.Default(context) + + if session.Get("profile") == nil { + context.Redirect(http.StatusSeeOther, "/auth/login") + context.Abort() + return + } + + access_token := session.Get("access_token") + + if access_token == nil { + context.Redirect(http.StatusSeeOther, "/auth/login") + return + } + + if TokenIsNotExpired(access_token.(string)) { + context.Next() + } else { + if !HandleRefreshToken(session) { + context.String(http.StatusUnauthorized, "Failed to refresh access token") + } else { + context.Next() + } + } + } +} diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..15c4988 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,48 @@ +module git.conway.engineer/ada/ordr.git/api + +go 1.25.3 + +require ( + github.com/coreos/go-oidc/v3 v3.16.0 + github.com/gin-contrib/sessions v1.0.4 + github.com/gin-gonic/gin v1.11.0 + golang.org/x/oauth2 v0.33.0 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..b96f182 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,94 @@ +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow= +github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= +github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=