diff --git a/cmd/hive/cmd/service.go b/cmd/hive/cmd/service.go index 899aa8003..a064a0f45 100644 --- a/cmd/hive/cmd/service.go +++ b/cmd/hive/cmd/service.go @@ -55,6 +55,11 @@ var hiveCmd = &cobra.Command{ Short: "Manage the hive service", } +var awsgwCmd = &cobra.Command{ + Use: "awsgw", + Short: "Manage the awsgw (AWS gateway) service", +} + var predastoreStartCmd = &cobra.Command{ Use: "start", Short: "Start the predastore service", @@ -410,6 +415,88 @@ var hiveStatusCmd = &cobra.Command{ }, } +// AWS GW + +var awsgwStartCmd = &cobra.Command{ + Use: "start", + Short: "Start the awsgw service", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Starting awsgw service...") + + cfgFile := viper.GetString("config") + + if cfgFile == "" { + fmt.Println("Config file is not set") + return + } + + // TODO: Support ENV vars, CLI, otherwise revert to config.LoadConfig() + appConfig, err := config.LoadConfig(cfgFile) + + if err != nil { + fmt.Println("Error loading config file:", err) + return + } + + // Overwrite defaults (CLI first, config second, env third) + awsgwHost := viper.GetString("host") + if awsgwHost != "" { + fmt.Println("Overwriting awsgw host to:", awsgwHost) + appConfig.AWSGW.Host = awsgwHost + } + + awsgwTlsCert := viper.GetString("tls-cert") + if awsgwTlsCert != "" { + fmt.Println("Overwriting awsgw tls-cert to:", awsgwTlsCert) + appConfig.AWSGW.TLSCert = awsgwTlsCert + } + + awsgwTlsKey := viper.GetString("tls-key") + + if awsgwTlsKey != "" { + fmt.Println("Overwriting awsgw tls-key to:", awsgwTlsKey) + appConfig.AWSGW.TLSKey = awsgwTlsKey + } + + service, err := service.New("awsgw", appConfig) + + if err != nil { + fmt.Println("Error starting awsgw service:", err) + return + } + + service.Start() + fmt.Println("AWSGW service started") + }, +} + +var awsgwStopCmd = &cobra.Command{ + Use: "stop", + Short: "Stop the awsgw service", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Stopping awsgw service...") + + service, err := service.New("awsgw", &config.Config{}) + + if err != nil { + fmt.Println("Error stopping awsgw service:", err) + return + } + + service.Stop() + + fmt.Println("AWSGW service stopped") + }, +} + +var awsgwStatusCmd = &cobra.Command{ + Use: "status", + Short: "Get status of the awsgw service", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("AWSGW service status: ...") + }, +} + func init() { viper.SetEnvPrefix("HIVE") // Prefix for environment variables @@ -521,4 +608,29 @@ func init() { viper.BindEnv("wal-dir", "HIVE_WAL_DIR") viper.BindPFlag("wal-dir", hiveCmd.PersistentFlags().Lookup("wal-dir")) + // AWS GW + serviceCmd.AddCommand(awsgwCmd) + + awsgwCmd.PersistentFlags().String("host", "0.0.0.0:9999", "AWS Gateway server host") + viper.BindEnv("host", "HIVE_AWSGW_HOST") + viper.BindPFlag("host", awsgwCmd.PersistentFlags().Lookup("host")) + + // AWS GW TLS Cert + awsgwCmd.PersistentFlags().String("tls-cert", "", "AWS Gateway TLS certificate") + viper.BindEnv("tls-cert", "HIVE_AWSGW_TLS_CERT") + viper.BindPFlag("tls-cert", awsgwCmd.PersistentFlags().Lookup("tls-cert")) + + // AWS GW TLS Key + awsgwCmd.PersistentFlags().String("tls-key", "", "AWS Gateway TLS key") + viper.BindEnv("tls-key", "HIVE_AWSGW_TLS_KEY") + viper.BindPFlag("tls-key", awsgwCmd.PersistentFlags().Lookup("tls-key")) + + awsgwCmd.PersistentFlags().Bool("debug", false, "AWS Gateway Debug") + viper.BindEnv("debug", "HIVE_AWSGW_DEBUG") + viper.BindPFlag("debug", awsgwCmd.PersistentFlags().Lookup("debug")) + + awsgwCmd.AddCommand(awsgwStartCmd) + awsgwCmd.AddCommand(awsgwStopCmd) + awsgwCmd.AddCommand(awsgwStatusCmd) + } diff --git a/config/awsgw/awsgw.toml b/config/awsgw/awsgw.toml new file mode 100644 index 000000000..d454024fd --- /dev/null +++ b/config/awsgw/awsgw.toml @@ -0,0 +1,13 @@ +# Test configuration file in TOML format. +version = "1.0" +region = "ap-southeast-2" + +# Enable verbose debugging +debug = false + +[[auth]] +access_key_id = "AKIAIOSFODNN7EXAMPLE" +secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +policy = [ + { bucket = "predastore", actions = ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListAllMyBuckets"] }, +] diff --git a/config/hive.toml b/config/hive.toml index 2ecabbb31..d1f065249 100644 --- a/config/hive.toml +++ b/config/hive.toml @@ -7,8 +7,14 @@ secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" [daemon] host = "0.0.0.0:4432" -sslkey = "config/server.key" -sslcert = "config/server.pem" +tlskey = "config/server.key" +tlscert = "config/server.pem" + +[awsgw] +host = "0.0.0.0:9999" +tlskey = "config/server.key" +tlscert = "config/server.pem" +debug = true [nats] host = "0.0.0.0:4222" diff --git a/hive/config/config.go b/hive/config/config.go index 4bcd0b308..e4a1ffa52 100644 --- a/hive/config/config.go +++ b/hive/config/config.go @@ -16,6 +16,7 @@ type Config struct { Daemon DaemonConfig `mapstructure:"daemon"` NATS NATSConfig `mapstructure:"nats"` Predastore PredastoreConfig `mapstructure:"predastore"` + AWSGW AWSGWConfig `mapstructure:"awsgw"` // Authentication AccessKey string `mapstructure:"access_key"` @@ -25,6 +26,14 @@ type Config struct { WalDir string `mapstructure:"wal_dir"` } +type AWSGWConfig struct { + Host string `mapstructure:"host"` + TLSKey string `mapstructure:"tlskey"` + TLSCert string `mapstructure:"tlscert"` + + Debug bool `mapstructure:"debug"` +} + type PredastoreConfig struct { Host string `mapstructure:"host"` Bucket string `mapstructure:"bucket"` @@ -37,8 +46,8 @@ type PredastoreConfig struct { // DaemonConfig holds the daemon configuration type DaemonConfig struct { Host string `mapstructure:"host"` - SSLKey string `mapstructure:"sslkey"` - SSLCert string `mapstructure:"sslcert"` + TLSKey string `mapstructure:"tlskey"` + TLSCert string `mapstructure:"tlscert"` } // NATSConfig holds the NATS configuration diff --git a/hive/gateway/account.go b/hive/gateway/account.go new file mode 100644 index 000000000..5e73abb01 --- /dev/null +++ b/hive/gateway/account.go @@ -0,0 +1,8 @@ +package gateway + +import "github.com/gofiber/fiber/v2" + +func (gw *GatewayConfig) Account_Request(ctx *fiber.Ctx) error { + + return nil +} diff --git a/hive/gateway/ec2.go b/hive/gateway/ec2.go new file mode 100644 index 000000000..da735b1b0 --- /dev/null +++ b/hive/gateway/ec2.go @@ -0,0 +1,167 @@ +package gateway + +import ( + "fmt" + "log/slog" + + "github.com/davecgh/go-spew/spew" + "github.com/gofiber/fiber/v2" +) + +func (gw *GatewayConfig) EC2_Request(ctx *fiber.Ctx) error { + + fmt.Println("HERE!") + + fmt.Println("Body") + spew.Dump(ctx.Body()) + + fmt.Println("Request POST") + spew.Dump(ctx.Request()) + + // Headers + fmt.Println("Headers") + spew.Dump(ctx.GetReqHeaders()) + + queryArgs := parseAWSQueryArgs(string(ctx.Body())) + + // Run the action + switch queryArgs["Action"] { + case "DescribeInstances": + return gw.EC2_DescribeInstances(ctx, queryArgs) + case "RunInstances": + return gw.EC2_RunInstances(ctx, queryArgs) + case "CreateKeyPair": + return gw.EC2_CreateKeyPair(ctx, queryArgs) + default: + slog.Warn("EC2 Unsupported Action", "action", queryArgs["Action"]) + } + + return fiber.NewError(fiber.StatusNotImplemented, "Action not implemented") +} + +func (gw *GatewayConfig) EC2_DescribeInstances(ctx *fiber.Ctx, args map[string]string) error { + + slog.Info("EC2 DescribeInstances called") + + // Return a dummy response + response := ` + + 2b7ac2f1-9acd-4d73-b0a1-1b2e5607f9ab + + + r-0f6b3c4d5e6789abc + 123456789012 + + + + i-0123456789abcdef0 + ami-08d4ac5b634553e16 + + 16 + running + + ip-10-0-1-25.ap-southeast-2.compute.internal + ec2-3-26-14-112.ap-southeast-2.compute.amazonaws.com + + my-keypair + 0 + + t3.medium + 2025-02-18T04:12:07.000Z + + ap-southeast-2a + + default + + + disabled + + 10.0.1.25 + 3.26.14.112 + subnet-0ab1c2d3e4f567890 + vpc-0123abcd4567efgh8 + true + + + sg-0abc1234def567890 + default + + + + x86_64 + ebs + /dev/xvda + + + /dev/xvda + + vol-06f5e4d3c2b1a0f98 + attached + 2025-02-18T04:12:09.000Z + true + + + + hvm + + + Name + web-01 + + + xen + + + eni-0a12b34c56d78ef90 + subnet-0ab1c2d3e4f567890 + vpc-0123abcd4567efgh8 + + 123456789012 + in-use + 0a:1b:2c:3d:4e:5f + 10.0.1.25 + ip-10-0-1-25.ap-southeast-2.compute.internal + true + + + sg-0abc1234def567890 + default + + + + eni-attach-0abc123def4567890 + 0 + attached + 2025-02-18T04:12:07.000Z + true + + + + 10.0.1.25 + ip-10-0-1-25.ap-southeast-2.compute.internal + true + + + + + + + + + +` + + return ctx.SendString(response) +} + +func (gw *GatewayConfig) EC2_RunInstances(ctx *fiber.Ctx, args map[string]string) error { + + return nil + +} + +func (gw *GatewayConfig) EC2_CreateKeyPair(ctx *fiber.Ctx, args map[string]string) error { + slog.Info("EC2 CreateKeyPair called") + + return nil +} diff --git a/hive/gateway/gateway.go b/hive/gateway/gateway.go new file mode 100644 index 000000000..9503a7a34 --- /dev/null +++ b/hive/gateway/gateway.go @@ -0,0 +1,228 @@ +package gateway + +import ( + "errors" + "log/slog" + "os" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/mulgadc/predastore/s3" + + "github.com/google/uuid" +) + +type GatewayConfig struct { + Debug bool `json:"debug"` + DisableLogging bool `json:"disable_logging"` +} + +var supportedServices = map[string]bool{ + "ec2": true, + "iam": true, + "account": true, +} + +func (gw *GatewayConfig) SetupRoutes() *fiber.App { + + var logLevel slog.Level + + if gw.Debug { + logLevel = slog.LevelDebug + } else if gw.DisableLogging { + logLevel = slog.LevelError + } else { + logLevel = slog.LevelInfo + } + + handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: logLevel, + }) + + // Create a new logger with the custom handler + slogger := slog.New(handler) + + // Set it as the default logger + slog.SetDefault(slogger) + + // Configure slog for logging + slog.New(slog.NewTextHandler(os.Stdout, nil)) + + app := fiber.New(fiber.Config{ + + // Disable the startup banner + DisableStartupMessage: gw.DisableLogging, + + // Set the body limit for S3 specs to 5GiB + //BodyLimit: 5 * 1024 * 1024 * 1024, + + // Override default error handler + ErrorHandler: func(ctx *fiber.Ctx, err error) error { + return gw.ErrorHandler(ctx, err) + }}, + ) + + if !gw.DisableLogging { + app.Use(logger.New()) + } + + s3 := s3.New(&s3.Config{}) + + // TODO: Support env var for config path, and external IAM + s3.ConfigPath = "config/awsgw/awsgw.toml" + + err := s3.ReadConfig() + + if err != nil { + slog.Error("Error reading config", "error", err) + return nil + } + + // Add authentication middleware for all requests + app.Use(s3.SigV4AuthMiddleware) + + // Define routes + app.Get("/*", func(c *fiber.Ctx) error { + + return gw.Request(c) + }) + + app.Post("/*", func(c *fiber.Ctx) error { + + return gw.Request(c) + }) + + return app +} + +// Note, custom endpoints can be configured via ENV vars to the AWS SDK/CLI tool, with individual endpoints depending the service +// AWS_ENDPOINT_URL_EC2=https://localhost:9999/ aws --no-verify-ssl ec2 describe-instances +// aws --endpoint-url https://localhost:9999/ --no-verify-ssl eks list-clusters +// AWS_ENDPOINT_URL=https://localhost:9999/ aws --no-verify-ssl ec2 describe-instances + +func (gw *GatewayConfig) Request(ctx *fiber.Ctx) error { + + // Route the request to the appropriate endpoint (e.g EC2, IAM, etc) + + // Determine the service from the Authorization header + // "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20250930/ap-southeast-2/account/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=adb8183545a9d6f5b626908582b4ca5f9a309137b1d4bcc802f0e79ede7af7bc" + + authHeader := ctx.Get("Authorization") + + parts := strings.Split(authHeader, ", ") + if len(parts) != 3 { + slog.Debug("Invalid Authorization header format") + return errors.New("AccessDenied") + } + + // Parse credential + creds := strings.Split(strings.TrimPrefix(parts[0], "AWS4-HMAC-SHA256 Credential="), "/") + if len(creds) != 5 { + slog.Debug("Invalid credential scope") + return errors.New("AccessDenied") + } + + svc := creds[3] + + if !supportedServices[svc] { + slog.Debug("Unsupported service", "service", svc) + return errors.New("UnsupportedOperation") + } + + slog.Info("Request", "service", svc, "method", ctx.Method(), "path", ctx.Path()) + + switch svc { + case "ec2": + return gw.EC2_Request(ctx) + case "account": + return gw.Account_Request(ctx) + case "iam": + return gw.IAM_Request(ctx) + default: + return errors.New("UnsupportedOperation") + } + +} + +func (gw *GatewayConfig) ErrorHandler(ctx *fiber.Ctx, err error) error { + // TODO: Support service type specific errors (e.g EC2, S3, etc) + // Status code defaults to 500 + httpCode := fiber.StatusInternalServerError + var s3error s3.S3Error + var e *fiber.Error + + // Check for specific error types + switch { + case strings.Contains(err.Error(), "NoSuchBucket") || strings.Contains(err.Error(), "Bucket not found"): + // File or bucket not found + httpCode = fiber.StatusNotFound + s3error.Code = "NoSuchBucket" + s3error.Message = "The specified bucket does not exist" + + case strings.Contains(err.Error(), "AccessDenied") || strings.Contains(err.Error(), "Not enough permissions"): + // Permission error + httpCode = fiber.StatusForbidden + s3error.Code = "AccessDenied" + s3error.Message = "Access Denied" + + case strings.Contains(err.Error(), "NoSuchObject") || strings.Contains(err.Error(), "not found") || + errors.Is(err, os.ErrNotExist): + // File not found + httpCode = fiber.StatusNotFound + s3error.Code = "NoSuchKey" + s3error.Message = "The specified key does not exist" + + case strings.Contains(err.Error(), "Invalid signature") || strings.Contains(err.Error(), "Invalid access key"): + // Authentication error + httpCode = fiber.StatusForbidden + s3error.Code = "SignatureDoesNotMatch" + s3error.Message = "The request signature does not match" + + case strings.Contains(err.Error(), "Missing Authorization header"): + // Missing auth header + httpCode = fiber.StatusForbidden + s3error.Code = "AccessDenied" + s3error.Message = "Access Denied" + + case strings.Contains(err.Error(), "UnsupportedOperation"): + // Unsupported operation + httpCode = fiber.StatusBadRequest + s3error.Code = "UnsupportedOperation" + s3error.Message = "The operation is not supported" + ctx.Set("X-Amzn-Errortype", "UnsupportedOperationException") + + case errors.As(err, &e): + httpCode = e.Code + s3error.Message = e.Message + s3error.Code = "InternalError" + default: + s3error.Code = "InternalError" + s3error.Message = err.Error() + } + + // Add request ID and host ID + + s3error.RequestId = ctx.GetRespHeader("x-amz-request-id", uuid.NewString()) + s3error.HostId = ctx.Hostname() + + // Set standard S3 error response headers + ctx.Set("Content-Type", "application/xml") + + return ctx.Status(httpCode).XML(s3error) +} + +// Parse AWS query arguments (used by some services like EC2/S3) +func parseAWSQueryArgs(query string) map[string]string { + params := make(map[string]string) + pairs := strings.Split(query, "&") + for _, pair := range pairs { + kv := strings.SplitN(pair, "=", 2) + if len(kv) == 2 { + params[kv[0]] = kv[1] + } else if len(kv) == 1 { + params[kv[0]] = "" + } + } + return params +} diff --git a/hive/gateway/iam.go b/hive/gateway/iam.go new file mode 100644 index 000000000..d6d98c6fd --- /dev/null +++ b/hive/gateway/iam.go @@ -0,0 +1,8 @@ +package gateway + +import "github.com/gofiber/fiber/v2" + +func (gw *GatewayConfig) IAM_Request(ctx *fiber.Ctx) error { + + return nil +} diff --git a/hive/service/service.go b/hive/service/service.go index adf7030ad..298234bd2 100644 --- a/hive/service/service.go +++ b/hive/service/service.go @@ -3,6 +3,7 @@ package service import ( "fmt" + "github.com/mulgadc/hive/hive/services/awsgw" "github.com/mulgadc/hive/hive/services/hive" "github.com/mulgadc/hive/hive/services/nats" "github.com/mulgadc/hive/hive/services/predastore" @@ -20,17 +21,20 @@ type Service interface { func New(btype string, config interface{}) (Service, error) { switch btype { + case "nats": + return nats.New(config) + case "predastore": return predastore.New(config) - case "nats": - return nats.New(config) + case "viperblock": + return viperblockd.New(config) case "hive": return hive.New(config) - case "viperblock": - return viperblockd.New(config) + case "awsgw": + return awsgw.New(config) } diff --git a/hive/services/awsgw/awsgw.go b/hive/services/awsgw/awsgw.go new file mode 100644 index 000000000..f1756dffb --- /dev/null +++ b/hive/services/awsgw/awsgw.go @@ -0,0 +1,77 @@ +package awsgw + +import ( + "log" + "log/slog" + "os" + + "github.com/mulgadc/hive/hive/config" + "github.com/mulgadc/hive/hive/gateway" + "github.com/mulgadc/hive/hive/utils" +) + +var serviceName = "awsgw" + +type Service struct { + Config *config.Config +} + +func New(cfg interface{}) (svc *Service, err error) { + svc = &Service{ + Config: cfg.(*config.Config), + } + return svc, nil +} + +func (svc *Service) Start() (int, error) { + utils.WritePidFile(serviceName, os.Getpid()) + err := launchService(svc.Config) + if err != nil { + return 0, err + } + + return os.Getpid(), nil +} + +func (svc *Service) Stop() (err error) { + err = utils.StopProcess(serviceName) + return err +} + +func (svc *Service) Status() (string, error) { + return "", nil +} + +func (svc *Service) Shutdown() (err error) { + return svc.Stop() +} + +func (svc *Service) Reload() (err error) { + return nil +} + +func launchService(config *config.Config) (err error) { + + /* + d := daemon.NewDaemon(config) + slog.Info("Starting AWS-GW daemon ...") + err = d.Start() + */ + + gw := gateway.GatewayConfig{ + Debug: config.AWSGW.Debug, + DisableLogging: false, + } + + app := gw.SetupRoutes() + + if err != nil { + slog.Warn("Failed to start AWS-GW daemon: %v", err) + return err + } + + log.Fatal(app.ListenTLS(config.AWSGW.Host, config.AWSGW.TLSCert, config.AWSGW.TLSKey)) + + return nil + +} diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 755b23a91..ece92ab63 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -196,13 +196,28 @@ export HIVE_WAL_DIR=$WAL_DIR #HIVE_CMD="./bin/hive service hive start --config config/hive.toml --base-dir $HIVE_BASE_DIR --wal-dir $WAL_DIR" HIVE_CMD="./bin/hive service hive start" -start_service "hive" "$HIVE_CMD" + +# 5️⃣ Start AWS Gateway +echo "" +echo "4️5️⃣ Starting AWS Gateway..." + +# Use the same base directory as Viperblock for consistency +export HIVE_CONFIG_PATH=$CONFIG_DIR/hive.toml +export HIVE_AWSGW_HOST="0.0.0.0:9999" +export HIVE_AWSGW_TLS_CERT=$CONFIG_DIR/server.pem +export HIVE_AWSGW_TLS_KEY=$CONFIG_DIR/server.key + +#HIVE_CMD="air -c .air-hive.toml" +#HIVE_CMD="./bin/hive service hive start --config config/hive.toml --base-dir $HIVE_BASE_DIR --wal-dir $WAL_DIR" +HIVE_CMD="./bin/hive service awsgw start" + +start_service "awsgw" "$HIVE_CMD" echo "" echo "🔗 Service endpoints will be:" echo " - NATS: nats://localhost:4222" echo " - Predastore: https://localhost:8443" -echo " - Hive Gateway: https://localhost:9999" +echo " - AWS Gateway: https://localhost:9999" echo "" echo "📊 Monitor background service logs:" echo " tail -f $LOGS_DIR/*.log"