夏溪辰的博客

xiaxichen's blog

Caddy 源码分析 Caddyfile 解析 by Loader & Parser

2023-12-03

Caddy 源码分析 Caddyfile 解析 by Loader & Parser

Preview

前文提到了 Caddy 的启动流程 和 Run 函数,现在我们顺着启动流程的第一步,读取 Caddyfile 的 源码阅读开始。

80F5F012-F4D9-4B2E-B4B7-31C10A30E86A.png

Loader (读取器)

在 caddy 中由 loader 执行配置文件装载。

工作流程

111ABAF2-F45C-42D0-8399-C23F62738851.png

loader 解耦了 caddyfile 文件的读取,所以把它放在了 plugin.go 文件中,作为一个插件注册在 caddy app 中。

  • 可以多定义 caddyfileloader 在图中由三个长方形的表示为数组
  • 自定义的 caddyfileloader 会实现 右半部分的 Loader 接口
  • caddyfileloader 会有对应读取的 ServerType 的 name
  • 最后生产出的是 Input

实现

保存

在 plugin.go 中有一个全局变量保存

// caddyfileLoaders is the list of all Caddyfile loaders
// in registration order.
caddyfileLoaders []caddyfileLoader

设计

type Loader interface {
    Load(serverType string) (Input, error)
}

这是用来 load caddyfile 的 你可以为自己的所有独有的 server 自定义读取 caddyfile 的方式

Usage

我们自定义的时候,只需要实现一个 LoaderFunc 即可。因为它使用了 类似 http.HandlerFunc 的模式,帮你自动实现了上文所说的 Loader 接口

// LoaderFunc is a convenience type similar to http.HandlerFunc
// that allows you to use a plain function as a Load() method.
type LoaderFunc func(serverType string) (Input, error)

// Load loads a Caddyfile.
func (lf LoaderFunc) Load(serverType string) (Input, error) {
    return lf(serverType)
}

扩展

两个方法,

  1. 可以注册新的 Loader
  2. 或者设置为默认的 Loader
// RegisterCaddyfileLoader registers loader named name.
func RegisterCaddyfileLoader(name string, loader Loader) {
    caddyfileLoaders = append(caddyfileLoaders, caddyfileLoader{name: name, loader: loader})
}

// SetDefaultCaddyfileLoader registers loader by name
// as the default Caddyfile loader if no others produce
// a Caddyfile. If another Caddyfile loader has already
// been set as the default, this replaces it.
//
// Do not call RegisterCaddyfileLoader on the same
// loader; that would be redundant.
func SetDefaultCaddyfileLoader(name string, loader Loader) {
    defaultCaddyfileLoader = caddyfileLoader{name: name, loader: loader}
}

Logic

code

// loadCaddyfileInput iterates the registered Caddyfile loaders
// and, if needed, calls the default loader, to load a Caddyfile.
// It is an error if any of the loaders return an error or if
// more than one loader returns a Caddyfile.
func loadCaddyfileInput(serverType string) (Input, error) {
    var loadedBy string
    var caddyfileToUse Input
    for _, l := range caddyfileLoaders {
        cdyfile, err := l.loader.Load(serverType)
        if err != nil {
            return nil, fmt.Errorf("loading Caddyfile via %s: %v", l.name, err)
        }
        if cdyfile != nil {
            if caddyfileToUse != nil {
                return nil, fmt.Errorf("Caddyfile loaded multiple times; first by %s, then by %s", loadedBy, l.name)
            }
            loaderUsed = l
            caddyfileToUse = cdyfile
            loadedBy = l.name
        }
    }
    if caddyfileToUse == nil && defaultCaddyfileLoader.loader != nil {
        cdyfile, err := defaultCaddyfileLoader.loader.Load(serverType)
        if err != nil {
            return nil, err
        }
        if cdyfile != nil {
            loaderUsed = defaultCaddyfileLoader
            caddyfileToUse = cdyfile
        }
    }
    return caddyfileToUse, nil
}

很轻松的看到,装载 caddyfile 的逻辑 是尝试调用 所有的 Loader 并且重复装载会报错。 如果尝试过之后装载失败,则使用 默认的 defaultCaddyfileLoader 这里可以看到最终流程是 caddyfile -> caddy.Input 那么这个 Input 是什么呢?

实际上 Input 就是 caddyfile 在代码中的映射。可以理解为,caddyfile 转化为了 Input 给 caddy 读取。

Parser

文件结构

├── dispenser.go
├── dispenser_test.go
├── json.go
├── json_test.go
├── lexer.go
├── lexer_test.go
├── parse.go
├── parse_test.go
└── testdata
    ├── import_glob0.txt
    ├── import_glob1.txt
    ├── import_glob2.txt
    ├── import_test1.txt
    └── import_test2.txt

lexer.go (词法分析)

一个实用工具,它可以从 Reader获取一个token接一个token的值。

通过 next()函数 token 是一个单词,token由空格分隔。

如果一个单词包含空格,可以用引号括起来。

即 lexer 用来做 caddyfile 的语法分析 被其他程序调用

词法分析

next()函数就是 lexer 用来提供功能的函数

// next loads the next token into the lexer.
// A token is delimited by whitespace, unless
// the token starts with a quotes character (")
// in which case the token goes until the closing
// quotes (the enclosing quotes are not included).
// Inside quoted strings, quotes may be escaped
// with a preceding \ character. No other chars
// may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
    var val []rune
    var comment, quoted, escaped bool

    makeToken := func() bool {
        l.token.Text = string(val)
        return true
    }

    for {
        ch, _, err := l.reader.ReadRune()
        if err != nil {
            if len(val) > 0 {
                return makeToken()
            }
            if err == io.EOF {
                return false
            }
            panic(err)
        }

        if quoted {
            if !escaped {
                if ch == '\\' {
                    escaped = true
                    continue
                } else if ch == '"' {
                    quoted = false
                    return makeToken()
                }
            }
            if ch == '\n' {
                l.line++
            }
            if escaped {
                // only escape quotes
                if ch != '"' {
                    val = append(val, '\\')
                }
            }
            val = append(val, ch)
            escaped = false
            continue
        }

        if unicode.IsSpace(ch) {
            if ch == '\r' {
                continue
            }
            if ch == '\n' {
                l.line++
                comment = false
            }
            if len(val) > 0 {
                return makeToken()
            }
            continue
        }

        if ch == '#' {
            comment = true
        }

        if comment {
            continue
        }

        if len(val) == 0 {
            l.token = Token{Line: l.line}
            if ch == '"' {
                quoted = true
                continue
            }
        }

        val = append(val, ch)
    }
}

他会根据 caddyfile 定义的写法,进行多种判断来实现分词

理解了 next 函数,就很容易知道如何分析一块选项的 token 了,不过都是 next() 的包装函数罢了。

Parser.go

serverBlock

实际上, ServerBlock 存储的是 一组 token 信息,

//ServerBlock associates any number of keys 
//(usually addresses of some sort) with tokens 
//(grouped by directive name).
type ServerBlock struct {
    Keys []string
    Tokens map[string][]Token
}

它包含的 Token 正是 Parser 在 Caddyfile 中得来的。

context 还负责生成 caddy 管理的 Server,用来提供供 caddy Start 的信息。

注意:这里的 Server 是 TCPServer 和 UDPServer,用来 Listen 等操作的。

parse

在 parser.go 中,由 Parse 函数进行生成 ServerBlock 的操作。

// Parse parses the input just enough to group tokens, in
// order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear.
// Directives that do not appear in validDirectives will cause
// an error. If you do not want to check for valid directives,
// pass in nil instead.
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
    p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
    return p.parseAll()
}

这里使用的 Dispenser ,是令牌分发器。

allTokens

在 praser.go 中使用 allTokens 进行 lexer 分词生成 token。

// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) ([]Token, error) {
    l := new(lexer)
    err := l.load(input)
    if err != nil {
        return nil, err
    }
    var tokens []Token
    for l.next() {
        tokens = append(tokens, l.token)
    }
    return tokens, nil
}

Dispenser

allTokens 会在 新建 Dispenser 的时候调用

// NewDispenser returns a Dispenser, ready to use for parsing the given input.
func NewDispenser(filename string, input io.Reader) Dispenser {
    tokens, _ := allTokens(input) // ignoring error because nothing to do with it
    return Dispenser{
        filename: filename,
        tokens:   tokens,
        cursor:   -1,
    }
}

如此,整个解析流程就串起来了, lexer 负责词法分析,

Parse 用于将一组 tokens 分类(因为很多的插件的设置稍微复杂一些),

Dispenser 负责分析去的 tokens 令牌消费。

52519EEC-102A-41FB-96C7-86001DDF88F0.jpg

Dispenser 的关键是 把不同的 tokens 转换成 Directives 命令来执行

dispenser 中由 cursor 来进行 Token 数组中的迭代

关键在于移动 cursor 索引的函数

需要注意到的是 Dispenser 中的函数实际上都是 获取 tokens 的函数,

意思是,在 Dispenser 中不会做任何配置,而是在相应的 controller.go ,caddy.go 中使用。