From e6532b6d854a7a2a5d66a037d8c7827c661a5813 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 15 Apr 2015 23:24:39 -0600 Subject: [PATCH] Multiple addresses may be specified per server block --- config/parser.go | 21 ++++++++- config/parser_test.go | 101 ++++++++++++++++++++++++++++++++++++++---- config/parsing.go | 83 ++++++++++++++++++++++++++++------ 3 files changed, 181 insertions(+), 24 deletions(-) diff --git a/config/parser.go b/config/parser.go index 7379fd4b6..61859442f 100644 --- a/config/parser.go +++ b/config/parser.go @@ -13,7 +13,9 @@ type ( parser struct { filename string // the name of the file that we're parsing 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 scope *locationContext // the current location context (path scope) being populated unused *token // sometimes a token will be read but not immediately consumed @@ -27,6 +29,11 @@ type ( path string 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 @@ -53,7 +60,7 @@ func (p *parser) parse() ([]Config, error) { if err != nil { return nil, err } - configs = append(configs, p.cfg) + configs = append(configs, p.cfgs...) } return configs, nil @@ -95,6 +102,7 @@ func (p *parser) next() bool { // another token and you're not in another server // block already. func (p *parser) parseOne() error { + p.cfgs = []Config{} p.cfg = Config{ Middleware: make(map[string][]middleware.Middleware), } @@ -110,6 +118,15 @@ func (p *parser) parseOne() error { 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 } diff --git a/config/parser_test.go b/config/parser_test.go index 0765af618..43a482aa5 100644 --- a/config/parser_test.go +++ b/config/parser_test.go @@ -35,7 +35,7 @@ func TestParserBasic(t *testing.T) { input := `localhost:1234 root /test/www - tls cert.pem key.pem` + tls cert.pem key.pem` 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"} input := `host1.com:443 { @@ -84,7 +84,7 @@ func TestParserBasicWithMultipleHosts(t *testing.T) { 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) + t.Fatalf("Expected 2 configurations, but got %d: %#v", len(confs), confs) } // 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) { p := &parser{filename: "test"} @@ -178,21 +263,21 @@ func TestParserLocationContext(t *testing.T) { t.Fatalf("Expected no errors, but got '%s'", err) } 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 { - 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 != "/" { - 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" { - 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 { - 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) } } diff --git a/config/parsing.go b/config/parsing.go index 556fcc468..376ccc7d0 100644 --- a/config/parsing.go +++ b/config/parsing.go @@ -3,6 +3,7 @@ package config import ( "errors" "net" + "strings" ) // This file contains the recursive-descent parsing @@ -12,7 +13,7 @@ import ( // It parses at most one server configuration (an address // and its directives). func (p *parser) begin() error { - err := p.address() + err := p.addresses() if err != nil { return err } @@ -25,26 +26,80 @@ func (p *parser) begin() error { return nil } -// address expects that the current token is a host:port -// combination. -func (p *parser) address() (err error) { - if p.tkn() == "}" || p.tkn() == "{" { - return p.err("Syntax", "'"+p.tkn()+"' is not EOF or address") +// addresses expects that the current token is a +// "scheme://host:port" combination (the "scheme://" +// and/or ":port" portions may be omitted). If multiple +// addresses are specified, they must be space- +// 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 // possible opening/closing curly braces around the block. // 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 { - if !p.next() { - // file consisted only of an address - return nil - } - errOpenCurlyBrace := p.openCurlyBrace() if errOpenCurlyBrace != nil { // meh, single-server configs don't need curly braces