Dito-能否网络之巅的高级反向代理黑马?部分源码浅析与分享

文摘   2024-10-11 00:24   江苏  

Dito:能否网络之巅的高级反向代理黑马?

各位朋友,大家好早晨好。 我是悟道,今分享一个高层的反向代理工具,主要也看了一下源码,正好简单地介绍给你们,下面是使用教程和源码浅析,如果自己有兴趣,链接在最后,自己也可以仔细看。

Dito 是一个用 Go 编写的高级第七层反向代理服务器。它提供了灵活的中间件支持、后端连接的自定义证书处理、动态配置重载、以及使用 Redis 的分布式缓存和速率限制。

特性

  • 第七层反向代理,用于处理 HTTP 请求
  • 动态配置重载(热重载)
  • 中间件支持(例如,认证、速率限制、缓存)
  • 使用 Redis 进行分布式速率限制
  • 使用 Redis 进行分布式缓存
  • 后端的自定义 TLS 证书管理(支持 mTLS)
  • 头部操作(添加头部、排除头部)
  • 支持详细请求和响应日志的日志记录

项目结构

  • cmd/:应用程序的入口点。
  • app/:核心应用程序逻辑。
  • client:用于缓存和速率限制的 Redis 客户端。
  • config/:与配置相关的实用程序(加载、热重载)。
  • handlers/:请求路由和反向代理逻辑的核心处理程序。
  • middlewares/:自定义中间件实现(例如,认证、缓存、速率限制)。
  • transport/:HTTP 传输自定义(包括 TLS 管理)。
  • writer/:自定义 HTTP 响应编写器,用于捕获状态代码。
  • logging/:记录请求和响应的实用程序。

安装

确保已安装 Go(版本 1.16 或更高)。

  1. 克隆仓库:

    git clone https://github.com/andrearaponi/dito.git
    cd dito
  2. 构建应用程序:

    go build -o dito ./cmd

使用方法

你可以通过简单地执行二进制文件来运行 Dito。默认情况下,它会在当前工作目录中查找 config.yaml

./dito

命令行选项

  • -f <path/to/config.yaml>:指定自定义配置文件。

示例:

./dito -f /path/to/custom-config.yaml

配置

配置在 yaml 文件中定义,如果启用了 hot_reload 选项,可以动态重载。这里是一个基本配置的示例:

port: '8081'  # 服务器监听的端口
hot_reload: true  # 启用配置的热重载

logging:
  enabled: true  # 启用或禁用日志记录
  verbose: false  # 启用详细日志记录

redis:
  enabled: true  # 启用 Redis 进行缓存和速率限制
  address: "localhost:6379"
  password: "yourpassword"
  db: 0

locations:
  - path: "^/api$"
    target_url: https://example.com
    replace_path: true
    additional_headers:
      X-Custom-Header: "my-value"
      il-molise: "non-esiste"
    excluded_headers:
      - Cookie
    middlewares:
      - auth
      - rate-limiter-redis
      - cache
    cache_config:
      enabled: true
      ttl: 60  # 缓存生存时间(秒)
    rate_limiting:
      enabled: true
      requests_per_second: 5
      burst: 10
    cert_file: "certs/client-cert.pem"
    key_file: "certs/client-key.pem"
    ca_file: "certs/ca-cert.pem"

中间件

Dito 支持自定义中间件,可以在配置中指定。目前可用的中间件包括:

  • auth:添加认证逻辑。
  • rate-limiter:使用内存方法限制每个 IP 的请求数量。
  • rate-limiter-redis:使用 Redis 进行分布式管理,限制每个 IP 的请求数量。
  • cache:使用 Redis 缓存响应,提高幂等响应(例如,GET)的性能。

Redis 集成

速率限制

Dito 支持使用 Redis 进行分布式速率限制。速率限制器可以针对每个位置进行配置,参数如 requests_per_second 和 burst 用于控制请求流。

缓存

cache 中间件使用 Redis 存储响应。它有助于通过为可配置的 ttl(生存时间)缓存响应来减轻后端的负载。可以根据请求头部或特定条件使缓存失效。

实现新的中间件

要实现新的中间件,将你的逻辑放在 middlewares/ 目录中,并在配置中引用它。

TLS/SSL

Dito 支持与后端的安全连接使用 mTLS。你可以指定:

  • cert_file:客户端证书。
  • key_file:客户端私钥。
  • ca_file:用于验证后端的证书颁发机构(CA)。

下面是项目源码分析

配置项

MakeFile:

# GO_CMD: The command to run Go.
# GO_BUILD: The command to build the Go project.
# GO_TEST: The command to run Go tests.
# GO_VET: The command to run Go vet.
# GO_FMT: The command to format Go code.
# BINARY_NAME: The name of the binary to be created.
# PKG: The package to be used for Go commands.
# API_DIR: The directory containing the API source code.
# CONFIG_FILE: The path to the configuration file.
GO_CMD=go
GO_BUILD=$(GO_CMD) build
GO_TEST=$(GO_CMD) test
GO_VET=$(GO_CMD) vet
GO_FMT=$(GO_CMD) fmt
BINARY_NAME=dito
PKG=./...
API_DIR=cmd
CONFIG_FILE=cmd/config.yaml

# SONAR_HOST_URL: The URL of the SonarQube server.
# SONAR_PROJECT_KEY: The unique key for the SonarQube project.
SONAR_HOST_URL=http://localhost:9000
SONAR_PROJECT_KEY=dito

# .PHONY: Declares phony targets that are not actual files.
.PHONY: build sonar test vet fmt clean run

# build: Compiles the Go project and copies the configuration file to the bin directory.
build:
 $(GO_BUILD) -o bin/$(BINARY_NAME) $(API_DIR)/*.go && cp $(CONFIG_FILE) bin/

# vet: Runs the Go vet tool.
vet:
 $(GO_VET) $(PKG)

# fmt: Formats the Go code.
fmt:
 $(GO_FMT) $(PKG)

# clean: Removes the binary and configuration file from the bin directory.
clean:
 rm -f bin/$(BINARY_NAME) && rm -f bin/config.yaml

# run: Runs the compiled binary.
run:
  cd bin && ./$(BINARY_NAME)

# test: Runs the Go tests.
test:
 $(GO_TEST) $(PKG)

# sonar: Analyzes the project with SonarQube.
sonar:
 sonar-scanner  \
    -Dsonar.projectKey=$(SONAR_PROJECT_KEY) \
    -Dsonar.sources=. \
    -Dsonar.host.url=$(SONAR_HOST_URL) \
    -Dsonar.token=$(SONAR_DITO_TOKEN)

首先,这个项目的命令和相关环境配置都在:

变量定义

我们看一下它定义了哪些变量:

  • GO_CMD: 定义了运行 Go 的命令,即go
  • GO_BUILD: 定义了构建 Go 项目的命令,即go build
  • GO_TEST: 定义了运行 Go 测试的命令,即go test
  • GO_VET: 定义了运行 Go vet 工具的命令,即go vet
  • GO_FMT: 定义了格式化 Go 代码的命令,即go fmt
  • BINARY_NAME: 定义了生成的二进制文件的名称,即dito
  • PKG: 定义了用于 Go 命令的包名,即当前目录下的所有文件(./...).
  • API_DIR: 定义了 API 源代码的目录,即cmd
  • CONFIG_FILE: 定义了配置文件的路径,即cmd/config.yaml

** SonarQube 配置**

再看 SonarQube 的定义

  • SONAR_HOST_URL: 定义了 SonarQube 服务器的 URL,即http://localhost:9000
  • SONAR_PROJECT_KEY: 定义了 SonarQube 项目的唯一键,即dito

知识扩展:Sonar 是一个用于代码质量管理的开源平台,用于管理源代码的质量,可以从七个维度检测代码质量

Makefile 目标

再看一下项目构成和打包构建命令

  • .PHONY: 声明了伪目标,即不是实际文件的目标。
  • build: 编译 Go 项目并将配置文件复制到bin目录。
  • vet: 运行 Go vet 工具。
  • fmt: 格式化 Go 代码。
  • clean: 删除bin目录下的二进制文件和配置文件。
  • run: 运行编译后的二进制文件。
  • test: 运行 Go 测试。
  • sonar: 使用 SonarQube 分析项目。

命令

实际上命令很好理解,主要是我们要学习作者对于项目的整个架构的理解以及我们日常写代码的时候,应该如何快速地构建标准化的流程。

  • $(GO_BUILD) -o bin/$(BINARY_NAME) $(API_DIR)/*.go && cp $(CONFIG_FILE) bin/: 编译 Go 项目并将配置文件复制到bin目录。
  • $(GO_VET) $(PKG): 运行 Go vet 工具。
  • $(GO_FMT) $(PKG): 格式化 Go 代码。
  • rm -f bin/$(BINARY_NAME) && rm -f bin/config.yaml: 删除bin目录下的二进制文件和配置文件。
  • cd bin && ./$(BINARY_NAME): 运行编译后的二进制文件。
  • $(GO_TEST) $(PKG): 运行 Go 测试。
  • sonar-scanner ...: 使用 SonarQube 分析项目。

我们再分析一下他的缓存 redis 的中间件

middlewares/cache_redis.go

package middlewares

import (
 "context"
 "dito/app"
 "dito/config"
 "dito/writer"
 "fmt"
 "net/http"
 "time"
)

// CacheMiddleware is an HTTP middleware that caches responses in Redis.
// It checks if caching is enabled and if the request allows caching.
// If a cached response is found, it serves the response from the cache.
// Otherwise, it processes the request and caches the response.
//
// Parameters:
// - next: The next http.Handler to be called if the request is not cached.
// - dito: The Dito application instance containing the Redis client and logger.
// - locationConfig: The configuration for caching.
//
// Returns:
// - http.Handler: A handler that applies caching based on the provided configuration.
func CacheMiddleware(next http.Handler, dito *app.Dito, locationConfig config.Cache) http.Handler {
 middlewareType := "CacheMiddlewareRedis"
 dito.Logger.Debug(fmt.Sprintf("[%s] Executing", middlewareType))

 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  if !locationConfig.Enabled || locationConfig.TTL <= 0 || r.Header.Get("Cache-Control") == "no-cache" {
   dito.Logger.Debug(fmt.Sprintf("[%s] Cache is not enabled or request has 'Cache-Control: no-cache'. Proceeding without cache.", middlewareType))
   next.ServeHTTP(w, r)
   return
  }

  cacheKey := generateCacheKey(r)

  cachedContentType, err1 := dito.RedisClient.Get(context.Background(), cacheKey+":content-type").Result()
  cachedResponse, err2 := dito.RedisClient.Get(context.Background(), cacheKey).Result()

  if err1 == nil && err2 == nil {
   dito.Logger.Debug(fmt.Sprintf("[%s] Cache hit for key: %s", middlewareType, cacheKey))

   w.Header().Set("Content-Type", cachedContentType)
   w.WriteHeader(http.StatusOK)
   _, writeErr := w.Write([]byte(cachedResponse))
   if writeErr != nil {
    dito.Logger.Error(fmt.Sprintf("[%s] Failed to write cached response: %v", middlewareType, writeErr))
   }
   return
  } else {
   dito.Logger.Debug(fmt.Sprintf("[%s] Cache miss for key: %s", middlewareType, cacheKey))
  }

  lrw := &writer.ResponseWriter{ResponseWriter: w}
  next.ServeHTTP(lrw, r)

  if lrw.StatusCode == http.StatusOK && lrw.Body.Len() > 0 {
   err := dito.RedisClient.Set(context.Background(), cacheKey, lrw.Body.String(), time.Duration(locationConfig.TTL)*time.Second).Err()
   if err != nil {
    dito.Logger.Error(fmt.Sprintf("[%s] Failed to cache response: %v", middlewareType, err))
   }

   contentType := lrw.Header().Get("Content-Type")
   err = dito.RedisClient.Set(context.Background(), cacheKey+":content-type", contentType, time.Duration(locationConfig.TTL)*time.Second).Err()
   if err != nil {
    dito.Logger.Error(fmt.Sprintf("[%s] Failed to cache content-type: %v", middlewareType, err))
   }
  }
 })
}

// generateCacheKey generates a cache key based on the request method and URI.
//
// Parameters:
// - r: The HTTP request.
//
// Returns:
// - string: The generated cache key.
func generateCacheKey(r *http.Request) string {
 return fmt.Sprintf("cache:%s:%s", r.Method, r.URL.RequestURI())
}

让我们逐行分析 CacheMiddleware 方法:

func CacheMiddleware(next http.Handler, dito *app.Dito, locationConfig config.Cache) http.Handler {

这是一个函数声明,定义了一个 CacheMiddleware 的函数。该函数接受三个参数:

  • next: 一个 http.Handler 类型的参数,表示下一个要调用的处理器。
  • dito: 一个 *app.Dito 类型的参数,表示 Dito 应用实例,包含 Redis 客户端和日志器。
  • locationConfig: 一个 config.Cache 类型的参数,表示缓存配置。

函数返回一个 http.Handler 类型的值,表示一个处理器。

middlewareType := "CacheMiddlewareRedis"

这行代码定义了一个字符串常量 middlewareType,其值为 "CacheMiddlewareRedis"。这个常量用于日志输出。

dito.Logger.Debug(fmt.Sprintf("[%s] Executing", middlewareType))

这行代码使用 dito 实例的日志器输出一条调试日志,表明中间件正在执行。日志消息的格式为 "[CacheMiddlewareRedis] Executing"

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

这行代码返回一个匿名函数,类型为 http.HandlerFunc。这个匿名函数接受两个参数:

  • w: 一个 http.ResponseWriter 类型的参数,表示响应写入器。
  • r: 一个 http.Request 类型的参数,表示 HTTP 请求。

这个匿名函数将在稍后被调用,以处理 HTTP 请求。

if !locationConfig.Enabled || locationConfig.TTL <= 0 || r.Header.Get("Cache-Control") == "no-cache" {

这行代码检查缓存配置是否启用、缓存 TTL 是否大于 0 以及请求头是否包含 "Cache-Control: no-cache"。如果任意一个条件成立,则跳过缓存处理。

dito.Logger.Debug(fmt.Sprintf("[%s] Cache is not enabled or request has 'Cache-Control: no-cache'. Proceeding without cache.", middlewareType))

这行代码输出一条调试日志,表明缓存未启用或请求不允许缓存。

next.ServeHTTP(w, r)
return

这行代码调用下一个处理器 (next) 的 ServeHTTP 方法,以处理请求。然后返回,不再执行后续代码。

cacheKey := generateCacheKey(r)

这行代码调用 generateCacheKey 函数,以生成缓存键。缓存键基于请求方法和 URI 生成。

cachedContentType, err1 := dito.RedisClient.Get(context.Background(), cacheKey+":content-type").Result()
cachedResponse, err2 := dito.RedisClient.Get(context.Background(), cacheKey).Result()

这两行代码使用 Redis 客户端获取缓存的内容类型和响应。缓存键分别为 cacheKey+":content-type" 和 cacheKey

if err1 == nil && err2 == nil {

这行代码检查获取缓存的错误是否为 nil。如果两个错误均为 nil,则表示缓存命中。

dito.Logger.Debug(fmt.Sprintf("[%s] Cache hit for key: %s", middlewareType, cacheKey))

这行代码输出一条调试日志,表明缓存命中。

让我们逐行分析 CacheMiddleware 方法的后半部分:

  1. lrw := &writer.ResponseWriter{ResponseWriter: w}:创建一个新的 ResponseWriter 对象,包装原来的 http.ResponseWriter 对象 w。这个新对象允许我们捕获写入响应体的数据。
  1. next.ServeHTTP(lrw, r): 调用下一个中间件或处理器的 ServeHTTP 方法,将包装后的 ResponseWriter 对象 lrw 和请求对象 r 传递给它。这一步执行实际的请求处理。
  2. if lrw.StatusCode == http.StatusOK && lrw.Body.Len() > 0 { ... }: 检查响应状态码是否为 200(OK)且响应体长度大于 0。如果条件满足,则缓存响应体和内容类型。
  3. err := dito.RedisClient.Set(context.Background(), cacheKey, lrw.Body.String(), time.Duration(locationConfig.TTL)*time.Second).Err(): 使用 Redis 客户端缓存响应体。缓存键为 cacheKey,缓存值为响应体字符串,缓存过期时间为 locationConfig.TTL 秒。
  4. if err != nil { ... }: 检查缓存响应体是否出错。如果出错,则记录错误日志。
  5. contentType := lrw.Header().Get("Content-Type"): 获取响应头中的内容类型。
  6. err = dito.RedisClient.Set(context.Background(), cacheKey+":content-type", contentType, time.Duration(locationConfig.TTL)*time.Second).Err(): 缓存内容类型。缓存键为 cacheKey+":content-type",缓存值为内容类型字符串,缓存过期时间为 locationConfig.TTL 秒。
  7. if err != nil { ... }: 检查缓存内容类型是否出错。如果出错,则记录错误日志。

总的来说,这段代码缓存了成功响应的响应体和内容类型,以便下次请求时可以直接从缓存中读取。

分析 handlers/handlers.go

handlers/handlers.go

package handlers

import (
 "bytes"
 "dito/app"
 "dito/config"
 "dito/logging"
 cmid "dito/middlewares"
 ct "dito/transport"
 "dito/writer"
 "fmt"
 "io"
 "net/http"
 "net/http/httputil"
 "net/url"
 "strings"
 "time"
)

const (
 InternalServerErrorMessage = "Internal Server Error"
)

// DynamicProxyHandler handles dynamic proxying of requests based on the configuration.
// It reads the request body, matches the request path with configured locations, and applies middlewares.
//
// Parameters:
// - dito: The Dito application instance containing the configuration and logger.
// - w: The HTTP response writer.
// - r: The HTTP request.
func DynamicProxyHandler(dito *app.Dito, w http.ResponseWriter, r *http.Request) {
 var bodyBytes []byte
 var errBody error

 if r.Body != nil {
  bodyBytes, errBody = io.ReadAll(r.Body)
  if errBody != nil {
   dito.Logger.Error("Error reading request body: ", errBody)
   http.Error(w, InternalServerErrorMessage, http.StatusInternalServerError)
   return
  }
  r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
  dito.Logger.Debug(fmt.Sprintf("Request body: %s", string(bodyBytes)))
 }

 start := time.Now()

 for i, location := range dito.Config.Locations {
  if location.CompiledRegex.MatchString(r.URL.Path) {
   handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    ServeProxy(dito, i, w, r)
   })

   handlerWithMiddlewares := applyMiddlewares(dito, handler, location)
   handlerWithMiddlewares = cmid.LoggingMiddleware(handlerWithMiddlewares, dito)

   lrw := &writer.ResponseWriter{ResponseWriter: w}
   handlerWithMiddlewares.ServeHTTP(lrw, r)

   return
  }
 }

 duration := time.Since(start)

 http.NotFound(w, r)

 if dito.Config.Logging.Enabled {
  headers := r.Header
  logging.LogRequestCompact(r, &bodyBytes, (*map[string][]string)(&headers), 404, duration)
 }
}

// ServeProxy handles the proxying of requests to the target URL specified in the location configuration.
//
// Parameters:
// - dito: The Dito application instance containing the configuration and logger.
// - locationIndex: The index of the location configuration in the Dito configuration.
// - lrw: The HTTP response writer.
// - r: The HTTP request.
func ServeProxy(dito *app.Dito, locationIndex int, lrw http.ResponseWriter, r *http.Request) {
 location := dito.Config.Locations[locationIndex]

 customTransport := &ct.Caronte{
  RT:       http.DefaultTransport,
  Location: &location,
 }

 targetURL, err := url.Parse(location.TargetURL)
 if err != nil {
  dito.Logger.Error("Error parsing the target URL: ", err)
  http.Error(lrw, InternalServerErrorMessage, http.StatusInternalServerError)
  return
 }

 proxy := &httputil.ReverseProxy{
  Director: func(req *http.Request) {
   req.URL.Scheme = targetURL.Scheme
   req.URL.Host = targetURL.Host

   if location.ReplacePath {
    req.URL.Path = targetURL.Path
   } else {
    additionalPath := strings.TrimPrefix(r.URL.Path, location.Path)
    req.URL.Path = normalizePath(targetURL.Path, additionalPath)
   }

   req.URL.RawQuery = r.URL.RawQuery

   req.Host = targetURL.Host
  },
  Transport: customTransport,
 }
 proxy.ServeHTTP(lrw, r)
}

// applyMiddlewares applies the configured middlewares to the given handler.
//
// Parameters:
// - dito: The Dito application instance containing the configuration and logger.
// - handler: The HTTP handler to which the middlewares will be applied.
// - location: The location configuration containing the middlewares to be applied.
//
// Returns:
// - http.Handler: The handler with the applied middlewares.
func applyMiddlewares(dito *app.Dito, handler http.Handler, location config.LocationConfig) http.Handler {
 for i := len(location.Middlewares) - 1; i >= 0; i-- {
  middleware := location.Middlewares[i]
  switch middleware {
  case "auth":
   dito.Logger.Debug("Applying Auth Middleware")
   handler = cmid.AuthMiddleware(handler, dito.Logger)
  case "rate-limiter":
   if location.RateLimiting.Enabled {
    dito.Logger.Debug("Applying Rate Limiter Middleware")
    handler = cmid.RateLimiterMiddleware(handler, location.RateLimiting, dito.Logger)
   }
  case "rate-limiter-redis":
   if location.RateLimiting.Enabled && dito.RedisClient != nil && dito.Config.Redis.Enabled {
    dito.Logger.Debug("Applying Rate Limiter Middleware")
    handler = cmid.RateLimiterMiddlewareWithRedis(handler, location.RateLimiting, dito.RedisClient, dito.Logger)
   }
  case "cache":
   if location.Cache.Enabled {
    dito.Logger.Debug(fmt.Sprintf("Applying Cache Middleware with TTL: %d seconds", location.Cache.TTL))
    handler = cmid.CacheMiddleware(handler, dito, location.Cache)
   }
  }
 }
 return handler
}

// normalizePath normalizes the base path and additional path by ensuring there is exactly one slash between them.
//
// Parameters:
// - basePath: The base path.
// - additionalPath: The additional path to be appended to the base path.
//
// Returns:
// - string: The normalized path.
func normalizePath(basePath, additionalPath string) string {
 return strings.TrimSuffix(basePath, "/") + "/" + strings.TrimPrefix(additionalPath, "/")
}

下面继续分析:

让我们逐行分析 DynamicProxyHandler 函数:

func DynamicProxyHandler(dito *app.Dito, w http.ResponseWriter, r *http.Request) {

这是函数的定义,接受三个参数:dito 是一个 app.Dito 类型的指针,w 是一个 http.ResponseWriter 类型的对象,r 是一个 http.Request 类型的对象。

var bodyBytes []byte
var errBody error

这里定义了两个变量:bodyBytes 是一个字节数组,用于存储请求体的内容;errBody 是一个错误类型的变量,用于存储读取请求体时可能发生的错误。

if r.Body != nil {

这里检查请求体是否不为空。如果请求体不为空,则执行以下代码:

bodyBytes, errBody = io.ReadAll(r.Body)

这里使用 io.ReadAll 函数读取请求体的内容,并将其存储在 bodyBytes 变量中。如果读取过程中发生错误,则错误信息存储在 errBody 变量中。

if errBody != nil {

这里检查是否发生了错误。如果发生了错误,则执行以下代码:

dito.Logger.Error("Error reading request body: ", errBody)
http.Error(w, InternalServerErrorMessage, http.StatusInternalServerError)
return

这里使用 dito.Logger 记录错误信息,并使用 http.Error 函数返回一个内部服务器错误的响应。然后函数立即返回,不再执行后续代码。

r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
dito.Logger.Debug(fmt.Sprintf("Request body: %s"string(bodyBytes)))

这里将请求体的内容存储在一个新的缓冲区中,并将其设置为请求体的新内容。然后使用 dito.Logger 记录请求体的内容。

start := time.Now()

这里记录当前时间,用于后续计算请求处理时间。

for i, location := range dito.Config.Locations {

这里开始循环遍历 dito.Config.Locations 数组中的每个元素。每个元素代表一个位置配置。

if location.CompiledRegex.MatchString(r.URL.Path) {

这里检查当前请求的 URL 路径是否匹配当前位置配置的正则表达式。如果匹配,则执行以下代码:

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ServeProxy(dito, i, w, r)
})

这里定义了一个新的处理函数,用于处理当前请求。该函数调用 ServeProxy 函数,并传递必要的参数。

handlerWithMiddlewares := applyMiddlewares(dito, handler, location)
handlerWithMiddlewares = cmid.LoggingMiddleware(handlerWithMiddlewares, dito)

这里将中间件应用于处理函数。首先使用 applyMiddlewares 函数应用位置配置中的中间件,然后使用 cmid.LoggingMiddleware 函数应用日志中间件。

lrw := &writer.ResponseWriter{ResponseWriter: w}
handlerWithMiddlewares.ServeHTTP(lrw, r)

这里创建一个新的响应写入器,并使用处理函数处理当前请求。

这里结束循环遍历位置配置的循环。

duration := time.Since(start)
http.NotFound(w, r)

这里计算请求处理时间,并返回一个 404 未找到错误的响应。

if dito.Config.Logging.Enabled {

这里检查是否启用了日志记录。如果启用了,则执行以下代码:

headers := r.Header
logging.LogRequestCompact(r, &bodyBytes, (*map[string][]string)(&headers), 404, duration)

这里记录请求信息,包括请求头、请求体、响应状态码和请求处理时间。

精华部分:
for i, location := range dito.Config.Locations {
    if location.CompiledRegex.MatchString(r.URL.Path) {
        handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ServeProxy(dito, i, w, r)
        })

        handlerWithMiddlewares := applyMiddlewares(dito, handler, location)
        handlerWithMiddlewares = cmid.LoggingMiddleware(handlerWithMiddlewares, dito)

        lrw := &writer.ResponseWriter{ResponseWriter: w}
        handlerWithMiddlewares.ServeHTTP(lrw, r)

        return
    }
}

遍历 dito.Config.Locations 数组,检查每个 location 是否匹配当前请求的 URL 路径。

如果匹配,则创建一个新的 http.HandlerFunc,该函数调用 ServeProxy 函数来处理代理请求。 将该 http.HandlerFunc 传递给 [applyMiddlewares]

添加日志中间件到处理链中。

创建一个新的 ResponseWriter 对象,并调用处理链的 ServeHTTP 方法来处理请求。

剩余的代码,请自己分析:

https://github.com/andrearaponi/dito



编程悟道
自制软件研发、软件商店,全栈,ARTS 、架构,模型,原生系统,后端(Node、React)以及跨平台技术(Flutter、RN).vue.js react.js next.js express koa hapi uniapp Astro
 最新文章