From cf1a1a80f7e5cfe15d3dd848e19ecba9444cdf17 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 21 Apr 2023 13:11:12 +0300 Subject: [PATCH] [#XX] Support virtual-hosted-style access to container Signed-off-by: Denis Kirillov --- app.go | 45 +++++++++++++++++++++++-- config/config.env | 3 ++ config/config.yaml | 4 +++ docs/gate-configuration.md | 23 ++++++++----- router/router.go | 69 ++++++++++++++++++++++++++++++++++++++ settings.go | 3 ++ 6 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 router/router.go diff --git a/app.go b/app.go index 0f49477..c422758 100644 --- a/app.go +++ b/app.go @@ -15,6 +15,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-http-gw/metrics" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/resolver" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/response" + hostRouter "git.frostfs.info/TrueCloudLab/frostfs-http-gw/router" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/uploader" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" @@ -338,8 +339,11 @@ func (a *app) Serve(ctx context.Context) { uploadRoutes := uploader.New(ctx, a.AppParams(), a.settings.Uploader) downloadRoutes := downloader.New(ctx, a.AppParams(), a.settings.Downloader) + domains := a.cfg.GetStringSlice(cfgListenDomains) + a.log.Info("fetch domains, prepare to use API", zap.Strings("domains", domains)) + // Configure router. - a.configureRouter(uploadRoutes, downloadRoutes) + a.configureRouter(domains, uploadRoutes, downloadRoutes) a.startServices() a.initServers(ctx) @@ -436,7 +440,42 @@ func (a *app) stopServices() { } } -func (a *app) configureRouter(uploadRoutes *uploader.Uploader, downloadRoutes *downloader.Downloader) { +func (a *app) configureRouter(domains []string, uploadRoutes *uploader.Uploader, downloadRoutes *downloader.Downloader) { + hr := hostRouter.NewHostBucketRouter("cid") + hr.Default(a.defaultRouter(uploadRoutes, downloadRoutes)) + + bucketRouter := a.bucketRouter(uploadRoutes, downloadRoutes) + for _, domain := range domains { + hr.Map(domain, bucketRouter) + } + + a.webServer.Handler = hr.Handler +} + +func (a *app) bucketRouter(uploadRoutes *uploader.Uploader, downloadRoutes *downloader.Downloader) *router.Router { + r := router.New() + r.RedirectTrailingSlash = true + r.NotFound = func(r *fasthttp.RequestCtx) { + response.Error(r, "Not found", fasthttp.StatusNotFound) + } + r.MethodNotAllowed = func(r *fasthttp.RequestCtx) { + response.Error(r, "Method Not Allowed", fasthttp.StatusMethodNotAllowed) + } + r.POST("/upload", a.logger(uploadRoutes.Upload)) + a.log.Info("added path /upload") + r.GET("/get/{oid}", a.logger(downloadRoutes.DownloadByAddress)) + r.HEAD("/get/{oid}", a.logger(downloadRoutes.HeadByAddress)) + a.log.Info("added path /get/{oid}") + r.GET("/get_by_attribute/{attr_key}/{attr_val:*}", a.logger(downloadRoutes.DownloadByAttribute)) + r.HEAD("/get_by_attribute/{attr_key}/{attr_val:*}", a.logger(downloadRoutes.HeadByAttribute)) + a.log.Info("added path /get_by_attribute/{attr_key}/{attr_val:*}") + r.GET("/zip/{prefix:*}", a.logger(downloadRoutes.DownloadZipped)) + a.log.Info("added path /zip/{prefix}") + + return r +} + +func (a *app) defaultRouter(uploadRoutes *uploader.Uploader, downloadRoutes *downloader.Downloader) *router.Router { r := router.New() r.RedirectTrailingSlash = true r.NotFound = func(r *fasthttp.RequestCtx) { @@ -456,7 +495,7 @@ func (a *app) configureRouter(uploadRoutes *uploader.Uploader, downloadRoutes *d r.GET("/zip/{cid}/{prefix:*}", a.logger(downloadRoutes.DownloadZipped)) a.log.Info("added path /zip/{cid}/{prefix}") - a.webServer.Handler = r.Handler + return r } func (a *app) logger(h fasthttp.RequestHandler) fasthttp.RequestHandler { diff --git a/config/config.env b/config/config.env index 4f13bb0..18c6ad8 100644 --- a/config/config.env +++ b/config/config.env @@ -17,6 +17,9 @@ HTTP_GW_PROMETHEUS_ADDRESS=localhost:8084 # Log level. HTTP_GW_LOGGER_LEVEL=debug +# Domains to be able to use virtual-hosted-style access to bucket/container. +HTTP_GW_LISTEN_DOMAINS=httpdev.frostfs.devenv + HTTP_GW_SERVER_0_ADDRESS=0.0.0.0:443 HTTP_GW_SERVER_0_TLS_ENABLED=false HTTP_GW_SERVER_0_TLS_CERT_FILE=/path/to/tls/cert diff --git a/config/config.yaml b/config/config.yaml index 0a117df..3b4d4a6 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -13,6 +13,10 @@ prometheus: logger: level: debug # Log level. +# Domains to be able to use virtual-hosted-style access to bucket/container. +listen_domains: + - httpdev.frostfs.devenv + server: - address: 0.0.0.0:8080 tls: diff --git a/docs/gate-configuration.md b/docs/gate-configuration.md index 8323bdc..adc19e5 100644 --- a/docs/gate-configuration.md +++ b/docs/gate-configuration.md @@ -57,6 +57,10 @@ $ cat http.log # General section ```yaml +listen_domains: + - httpdev.frostfs.devenv + - httpdev2.frostfs.devenv + rpc_endpoint: http://morph-chain.frostfs.devenv:30333 resolve_order: - nns @@ -69,15 +73,16 @@ rebalance_timer: 30s pool_error_threshold: 100 ``` -| Parameter | Type | SIGHUP reload | Default value | Description | -|------------------------|------------|---------------|----------------|------------------------------------------------------------------------------------| -| `rpc_endpoint` | `string` | yes | | The address of the RPC host to which the gateway connects to resolve bucket names. | -| `resolve_order` | `[]string` | yes | `[nns, dns]` | Order of bucket name resolvers to use. | -| `connect_timeout` | `duration` | | `10s` | Timeout to connect to a node. | -| `stream_timeout` | `duration` | | `10s` | Timeout for individual operations in streaming RPC. | -| `request_timeout` | `duration` | | `15s` | Timeout to check node health during rebalance. | -| `rebalance_timer` | `duration` | | `60s` | Interval to check node health. | -| `pool_error_threshold` | `uint32` | | `100` | The number of errors on connection after which node is considered as unhealthy. | +| Parameter | Type | SIGHUP reload | Default value | Description | +|------------------------|------------|---------------|---------------|------------------------------------------------------------------------------------| +| `listen_domains` | `[]string` | | | Domains to be able to use virtual-hosted-style access to bucket/container. | +| `rpc_endpoint` | `string` | yes | | The address of the RPC host to which the gateway connects to resolve bucket names. | +| `resolve_order` | `[]string` | yes | `[nns, dns]` | Order of bucket name resolvers to use. | +| `connect_timeout` | `duration` | | `10s` | Timeout to connect to a node. | +| `stream_timeout` | `duration` | | `10s` | Timeout for individual operations in streaming RPC. | +| `request_timeout` | `duration` | | `15s` | Timeout to check node health during rebalance. | +| `rebalance_timer` | `duration` | | `60s` | Interval to check node health. | +| `pool_error_threshold` | `uint32` | | `100` | The number of errors on connection after which node is considered as unhealthy. | # `wallet` section diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..c40bb1f --- /dev/null +++ b/router/router.go @@ -0,0 +1,69 @@ +package router + +import ( + "strings" + + "github.com/fasthttp/router" + "github.com/valyala/fasthttp" +) + +type HostBucketRouter struct { + routes map[string]*router.Router + bktParam string + defaultRouter *router.Router +} + +func NewHostBucketRouter(bktParam string) HostBucketRouter { + return HostBucketRouter{ + routes: make(map[string]*router.Router), + bktParam: bktParam, + } +} + +func (hr *HostBucketRouter) Default(router *router.Router) { + hr.defaultRouter = router +} + +func (hr HostBucketRouter) Map(host string, h *router.Router) { + hr.routes[strings.ToLower(host)] = h +} + +func (hr HostBucketRouter) Handler(ctx *fasthttp.RequestCtx) { + bucket, domain := getBucketDomain(getHost(ctx)) + _ = bucket + domainRouter, ok := hr.routes[strings.ToLower(domain)] + if !ok { + domainRouter = hr.defaultRouter + if domainRouter == nil { + ctx.Error(fasthttp.StatusMessage(fasthttp.StatusNotFound), fasthttp.StatusNotFound) + return + } + } + + if bucket != "" { + ctx.SetUserValue(hr.bktParam, bucket) + } + + domainRouter.Handler(ctx) +} + +func getBucketDomain(host string) (bucket string, domain string) { + parts := strings.Split(host, ".") + if len(parts) > 1 { + return parts[0], strings.Join(parts[1:], ".") + } + return "", host +} + +// getHost tries its best to return the request host. +// According to section 14.23 of RFC 2616 the Host header +// can include the port number if the default value of 80 is not used. +func getHost(r *fasthttp.RequestCtx) string { + host := string(r.Host()) + + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + + return host +} diff --git a/settings.go b/settings.go index 8d569c0..c7a71ca 100644 --- a/settings.go +++ b/settings.go @@ -57,6 +57,9 @@ const ( // Logger. cfgLoggerLevel = "logger.level" + // Domains for host based bucket/container access + cfgListenDomains = "listen_domains" + // Wallet. cfgWalletPassphrase = "wallet.passphrase" cfgWalletPath = "wallet.path"