mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-25 09:18:56 +01:00
Multiple addresses may be specified per server block
This commit is contained in:
parent
7d96cfa424
commit
e6532b6d85
3 changed files with 181 additions and 24 deletions
|
@ -13,7 +13,9 @@ type (
|
||||||
parser struct {
|
parser struct {
|
||||||
filename string // the name of the file that we're parsing
|
filename string // the name of the file that we're parsing
|
||||||
lexer lexer // the lexer that is giving us tokens from the raw input
|
lexer lexer // the lexer that is giving us tokens from the raw input
|
||||||
cfg Config // each server gets one Config; this is the one we're currently building
|
hosts []hostPort // the list of host:port combinations current tokens apply to
|
||||||
|
cfg Config // each virtual host gets one Config; this is the one we're currently building
|
||||||
|
cfgs []Config // after a Config is created, it may need to be copied for multiple hosts
|
||||||
other []locationContext // tokens to be 'parsed' later by middleware generators
|
other []locationContext // tokens to be 'parsed' later by middleware generators
|
||||||
scope *locationContext // the current location context (path scope) being populated
|
scope *locationContext // the current location context (path scope) being populated
|
||||||
unused *token // sometimes a token will be read but not immediately consumed
|
unused *token // sometimes a token will be read but not immediately consumed
|
||||||
|
@ -27,6 +29,11 @@ type (
|
||||||
path string
|
path string
|
||||||
directives map[string]*controller
|
directives map[string]*controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hostPort just keeps a hostname and port together
|
||||||
|
hostPort struct {
|
||||||
|
host, port string
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// newParser makes a new parser and prepares it for parsing, given
|
// newParser makes a new parser and prepares it for parsing, given
|
||||||
|
@ -53,7 +60,7 @@ func (p *parser) parse() ([]Config, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
configs = append(configs, p.cfg)
|
configs = append(configs, p.cfgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs, nil
|
return configs, nil
|
||||||
|
@ -95,6 +102,7 @@ func (p *parser) next() bool {
|
||||||
// another token and you're not in another server
|
// another token and you're not in another server
|
||||||
// block already.
|
// block already.
|
||||||
func (p *parser) parseOne() error {
|
func (p *parser) parseOne() error {
|
||||||
|
p.cfgs = []Config{}
|
||||||
p.cfg = Config{
|
p.cfg = Config{
|
||||||
Middleware: make(map[string][]middleware.Middleware),
|
Middleware: make(map[string][]middleware.Middleware),
|
||||||
}
|
}
|
||||||
|
@ -110,6 +118,15 @@ func (p *parser) parseOne() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make a copy of the config for each
|
||||||
|
// address that will be using it
|
||||||
|
for _, hostport := range p.hosts {
|
||||||
|
cfgCopy := p.cfg
|
||||||
|
cfgCopy.Host = hostport.host
|
||||||
|
cfgCopy.Port = hostport.port
|
||||||
|
p.cfgs = append(p.cfgs, cfgCopy)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ func TestParserBasic(t *testing.T) {
|
||||||
|
|
||||||
input := `localhost:1234
|
input := `localhost:1234
|
||||||
root /test/www
|
root /test/www
|
||||||
tls cert.pem key.pem`
|
tls cert.pem key.pem`
|
||||||
|
|
||||||
p.lexer.load(strings.NewReader(input))
|
p.lexer.load(strings.NewReader(input))
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ func TestParserBasic(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParserBasicWithMultipleHosts(t *testing.T) {
|
func TestParserBasicWithMultipleServerBlocks(t *testing.T) {
|
||||||
p := &parser{filename: "test"}
|
p := &parser{filename: "test"}
|
||||||
|
|
||||||
input := `host1.com:443 {
|
input := `host1.com:443 {
|
||||||
|
@ -84,7 +84,7 @@ func TestParserBasicWithMultipleHosts(t *testing.T) {
|
||||||
t.Fatalf("Expected no errors, but got '%s'", err)
|
t.Fatalf("Expected no errors, but got '%s'", err)
|
||||||
}
|
}
|
||||||
if len(confs) != 2 {
|
if len(confs) != 2 {
|
||||||
t.Fatalf("Expected 2 configurations, but got '%d': %#v", len(confs), confs)
|
t.Fatalf("Expected 2 configurations, but got %d: %#v", len(confs), confs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// First server
|
// First server
|
||||||
|
@ -128,6 +128,91 @@ func TestParserBasicWithMultipleHosts(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParserBasicWithMultipleHostsPerBlock(t *testing.T) {
|
||||||
|
// This test is table-driven; it is expected that each
|
||||||
|
// input string produce the same set of configs.
|
||||||
|
for _, input := range []string{
|
||||||
|
`host1.com host2.com:1234
|
||||||
|
root /public_html`, // space-separated, no block
|
||||||
|
|
||||||
|
`host1.com, host2.com:1234
|
||||||
|
root /public_html`, // comma-separated, no block
|
||||||
|
|
||||||
|
`host1.com,
|
||||||
|
host2.com:1234
|
||||||
|
root /public_html`, // comma-separated, newlines, no block
|
||||||
|
|
||||||
|
`host1.com host2.com:1234 {
|
||||||
|
root /public_html
|
||||||
|
}`, // space-separated, block
|
||||||
|
|
||||||
|
`host1.com, host2.com:1234 {
|
||||||
|
root /public_html
|
||||||
|
}`, // comma-separated, block
|
||||||
|
|
||||||
|
`host1.com,
|
||||||
|
host2.com:1234 {
|
||||||
|
root /public_html
|
||||||
|
}`, // comma-separated, newlines, block
|
||||||
|
} {
|
||||||
|
|
||||||
|
p := &parser{filename: "test"}
|
||||||
|
p.lexer.load(strings.NewReader(input))
|
||||||
|
|
||||||
|
confs, err := p.parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no errors, but got '%s'", err)
|
||||||
|
}
|
||||||
|
if len(confs) != 2 {
|
||||||
|
t.Fatalf("Expected 2 configurations, but got %d: %#v", len(confs), confs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if confs[0].Host != "host1.com" {
|
||||||
|
t.Errorf("Expected host of first conf to be 'host1.com', got '%s'", confs[0].Host)
|
||||||
|
}
|
||||||
|
if confs[0].Port != defaultPort {
|
||||||
|
t.Errorf("Expected port of first conf to be '%s', got '%s'", defaultPort, confs[0].Port)
|
||||||
|
}
|
||||||
|
if confs[0].Root != "/public_html" {
|
||||||
|
t.Errorf("Expected root of first conf to be '/public_html', got '%s'", confs[0].Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
if confs[1].Host != "host2.com" {
|
||||||
|
t.Errorf("Expected host of second conf to be 'host2.com', got '%s'", confs[1].Host)
|
||||||
|
}
|
||||||
|
if confs[1].Port != "1234" {
|
||||||
|
t.Errorf("Expected port of second conf to be '1234', got '%s'", confs[1].Port)
|
||||||
|
}
|
||||||
|
if confs[1].Root != "/public_html" {
|
||||||
|
t.Errorf("Expected root of second conf to be '/public_html', got '%s'", confs[1].Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParserBasicWithAlternateAddressStyles(t *testing.T) {
|
||||||
|
p := &parser{filename: "test"}
|
||||||
|
input := `http://host1.com, https://host2.com,
|
||||||
|
host3.com:http, host4.com:1234 {
|
||||||
|
root /test/www
|
||||||
|
}`
|
||||||
|
p.lexer.load(strings.NewReader(input))
|
||||||
|
|
||||||
|
confs, err := p.parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no errors, but got '%s'", err)
|
||||||
|
}
|
||||||
|
if len(confs) != 4 {
|
||||||
|
t.Fatalf("Expected 4 configurations, but got %d: %#v", len(confs), confs)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conf := range confs {
|
||||||
|
if conf.Root != "/test/www" {
|
||||||
|
t.Fatalf("Expected root for conf of %s to be '/test/www', but got: %s", conf.Address(), conf.Root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParserImport(t *testing.T) {
|
func TestParserImport(t *testing.T) {
|
||||||
p := &parser{filename: "test"}
|
p := &parser{filename: "test"}
|
||||||
|
|
||||||
|
@ -178,21 +263,21 @@ func TestParserLocationContext(t *testing.T) {
|
||||||
t.Fatalf("Expected no errors, but got '%s'", err)
|
t.Fatalf("Expected no errors, but got '%s'", err)
|
||||||
}
|
}
|
||||||
if len(confs) != 1 {
|
if len(confs) != 1 {
|
||||||
t.Fatalf("Expected 1 configuration, but got '%d': %#v", len(confs), confs)
|
t.Fatalf("Expected 1 configuration, but got %d: %#v", len(confs), confs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.other) != 2 {
|
if len(p.other) != 2 {
|
||||||
t.Fatalf("Expected 2 path scopes, but got '%d': %#v", len(p.other), p.other)
|
t.Fatalf("Expected 2 path scopes, but got %d: %#v", len(p.other), p.other)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.other[0].path != "/" {
|
if p.other[0].path != "/" {
|
||||||
t.Fatalf("Expected first path scope to be default '/', but got '%d': %#v", p.other[0].path, p.other)
|
t.Fatalf("Expected first path scope to be default '/', but got %d: %#v", p.other[0].path, p.other)
|
||||||
}
|
}
|
||||||
if p.other[1].path != "/scope" {
|
if p.other[1].path != "/scope" {
|
||||||
t.Fatalf("Expected first path scope to be '/scope', but got '%d': %#v", p.other[0].path, p.other)
|
t.Fatalf("Expected first path scope to be '/scope', but got %d: %#v", p.other[0].path, p.other)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir, ok := p.other[1].directives["gzip"]; !ok {
|
if dir, ok := p.other[1].directives["gzip"]; !ok {
|
||||||
t.Fatalf("Expected scoped directive to be gzip, but got '%d': %#v", dir, p.other[1].directives)
|
t.Fatalf("Expected scoped directive to be gzip, but got %d: %#v", dir, p.other[1].directives)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file contains the recursive-descent parsing
|
// This file contains the recursive-descent parsing
|
||||||
|
@ -12,7 +13,7 @@ import (
|
||||||
// It parses at most one server configuration (an address
|
// It parses at most one server configuration (an address
|
||||||
// and its directives).
|
// and its directives).
|
||||||
func (p *parser) begin() error {
|
func (p *parser) begin() error {
|
||||||
err := p.address()
|
err := p.addresses()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -25,26 +26,80 @@ func (p *parser) begin() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// address expects that the current token is a host:port
|
// addresses expects that the current token is a
|
||||||
// combination.
|
// "scheme://host:port" combination (the "scheme://"
|
||||||
func (p *parser) address() (err error) {
|
// and/or ":port" portions may be omitted). If multiple
|
||||||
if p.tkn() == "}" || p.tkn() == "{" {
|
// addresses are specified, they must be space-
|
||||||
return p.err("Syntax", "'"+p.tkn()+"' is not EOF or address")
|
// separated on the same line, or each token must end
|
||||||
|
// with a comma.
|
||||||
|
func (p *parser) addresses() error {
|
||||||
|
var expectingAnother bool
|
||||||
|
p.hosts = []hostPort{}
|
||||||
|
|
||||||
|
// address gets host and port in a format accepted by net.Dial
|
||||||
|
address := func(str string) (host, port string, err error) {
|
||||||
|
if strings.HasPrefix(str, "https://") {
|
||||||
|
port = "https"
|
||||||
|
host = str[8:]
|
||||||
|
return
|
||||||
|
} else if strings.HasPrefix(str, "http://") {
|
||||||
|
port = "http"
|
||||||
|
host = str[7:]
|
||||||
|
return
|
||||||
|
} else if !strings.Contains(str, ":") {
|
||||||
|
str += ":" + defaultPort
|
||||||
|
}
|
||||||
|
host, port, err = net.SplitHostPort(str)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
p.cfg.Host, p.cfg.Port, err = net.SplitHostPort(p.tkn())
|
|
||||||
return
|
for {
|
||||||
|
tkn, startLine := p.tkn(), p.line()
|
||||||
|
|
||||||
|
// Open brace definitely indicates end of addresses
|
||||||
|
if tkn == "{" {
|
||||||
|
if expectingAnother {
|
||||||
|
return p.err("Syntax", "Expected another address but had '"+tkn+"' - check for extra comma")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing comma indicates another address will follow, which
|
||||||
|
// may possibly be on the next line
|
||||||
|
if tkn[len(tkn)-1] == ',' {
|
||||||
|
tkn = tkn[:len(tkn)-1]
|
||||||
|
expectingAnother = true
|
||||||
|
} else {
|
||||||
|
expectingAnother = false // but we may still see another one on this line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and save this address
|
||||||
|
host, port, err := address(tkn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.hosts = append(p.hosts, hostPort{host, port})
|
||||||
|
|
||||||
|
// Advance token and possibly break out of loop or return error
|
||||||
|
hasNext := p.next()
|
||||||
|
if expectingAnother && !hasNext {
|
||||||
|
return p.eofErr()
|
||||||
|
}
|
||||||
|
if !expectingAnother && p.line() > startLine {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addressBlock leads into parsing directives, including
|
// addressBlock leads into parsing directives, including
|
||||||
// possible opening/closing curly braces around the block.
|
// possible opening/closing curly braces around the block.
|
||||||
// It handles directives enclosed by curly braces and
|
// It handles directives enclosed by curly braces and
|
||||||
// directives not enclosed by curly braces.
|
// directives not enclosed by curly braces. It is expected
|
||||||
|
// that the current token is already the beginning of
|
||||||
|
// the address block.
|
||||||
func (p *parser) addressBlock() error {
|
func (p *parser) addressBlock() error {
|
||||||
if !p.next() {
|
|
||||||
// file consisted only of an address
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
errOpenCurlyBrace := p.openCurlyBrace()
|
errOpenCurlyBrace := p.openCurlyBrace()
|
||||||
if errOpenCurlyBrace != nil {
|
if errOpenCurlyBrace != nil {
|
||||||
// meh, single-server configs don't need curly braces
|
// meh, single-server configs don't need curly braces
|
||||||
|
|
Loading…
Reference in a new issue