Ragel based config file parser, ftw.

This commit is contained in:
Alex Bramley 2010-11-28 16:39:25 +00:00
parent 377fbcd3af
commit 22ccce6bb2
9 changed files with 250 additions and 366 deletions

View File

@ -7,11 +7,10 @@ include $(GOROOT)/src/Make.inc
TARG=irc/server/config
GOFILES=\
config.go\
port.go\
oper.go\
link.go\
ban.go\
info.go\
settings.go\
parser.go\
include $(GOROOT)/src/Make.pkg
parser.go: parser.rl
ragel -Z -G2 -o parser.go parser.rl

View File

@ -1,33 +0,0 @@
package config
type cBan interface {
Match(string) bool
Reason() string
}
// G-Line etc;
type cBanNick struct {
NickMask string // nick!ident@host
Reason string
}
// Z-Line
type cBanIP struct {
Address string // ip (or hostname), plus optional CIDR netmask
Reason string
ip string // parsed into these
cidr int
}
// CTCP version ban
type cBanVersion struct {
VersionRegex string // regex to match against version reply
Reason string
}
// Ban server from linking to network
type cBanServer struct {
ServerMask string // matched against name of linked server
Reason string
}

View File

@ -1,16 +1,14 @@
package config
import (
"io"
"os"
"fmt"
"strconv"
"scanner"
"net"
"strings"
)
type Config struct {
fn string
scan *scanner.Scanner
// Ports we listen on.
Ports map[int]*cPort
@ -31,17 +29,6 @@ type Config struct {
Errors []os.Error
}
type configMap map[string]func(*Config)
type keywordMap map[string]func(*Config, interface{})
var configKeywords = configMap{
"port": (*Config).parsePort,
"oper": (*Config).parseOper,
// "link": (*Config).parseLink,
// "ban": (*Config).parseBan,
// "info": (*Config).parseInfo,
// "set": (*Config).parseSettings,
}
func LoadConfig(filename string) *Config {
conf := &Config{fn: filename}
@ -74,103 +61,129 @@ func (conf *Config) Rehash() {
}
}
func (conf *Config) Parse(io io.Reader) {
s := &scanner.Scanner{}
s.Init(io)
s.Filename = conf.fn
conf.scan = s
tok, text := conf.next()
for tok != scanner.EOF {
// This external loop should only parse Config things
if f, ok := configKeywords[text]; ok {
f(conf)
} else {
conf.parseError("Invalid top-level keyword '%s'", text)
/* Port configuration */
type cPort struct {
Port int
BindIP net.IP // bind to a specific IP for listen port
Class string // "server" or "client"
// Is port a tls.Listener? Does it support compression (no)?
SSL, Zip bool
}
func defaultPort() *cPort {
return &cPort{
BindIP: nil, Class: "client",
SSL: false, Zip: false,
}
}
func (p *cPort) String() string {
str := []string{fmt.Sprintf("port %d {", p.Port)}
if p.BindIP != nil {
str = append(str,
fmt.Sprintf("\tbind_ip = %s", p.BindIP.String()))
}
str = append(str,
fmt.Sprintf("\tclass = %s", p.Class),
fmt.Sprintf("\tssl = %t", p.SSL),
fmt.Sprintf("\tzip = %t", p.Zip),
"}",
)
return strings.Join(str, "\n")
}
/* Oper configuration */
type cOper struct {
Username, Password string
HostMask []string
// Permissions for oper
CanKill, CanBan, CanRenick, CanLink bool
}
func defaultOper() *cOper {
return &cOper{
HostMask: []string{},
CanKill: true, CanBan: true,
CanRenick: false, CanLink: false,
}
}
func (o *cOper) String() string {
str := []string{fmt.Sprintf("oper %s {", o.Username)}
str = append(str, fmt.Sprintf("\tpassword = %s", o.Password))
if len(o.HostMask) == 0 {
str = append(str, fmt.Sprintf("\thostmask = *@*"))
} else {
for _, h := range o.HostMask {
str = append(str, fmt.Sprintf("\thostmask = %s", h))
}
fmt.Printf("Token: '%s', type %s\n", s.TokenText(), scanner.TokenString(tok))
tok, text = conf.next()
}
str = append(str,
fmt.Sprintf("\tkill = %t", o.CanKill),
fmt.Sprintf("\tban = %t", o.CanBan),
fmt.Sprintf("\trenick = %t", o.CanRenick),
fmt.Sprintf("\tlink = %t", o.CanLink),
"}",
)
return strings.Join(str, "\n")
}
func (conf *Config) parseKwBlock(dst interface{}, bt string, kw keywordMap) {
if ok := conf.expect("{"); !ok {
conf.parseError("Expected %s configuration block.", bt)
return
}
tok, text := conf.next()
for tok != scanner.EOF {
if f, ok := kw[text]; ok {
if ok = conf.expect("="); ok {
f(conf, dst)
}
} else if text == "}" {
break
} else {
conf.parseError("Invalid %s keyword '%s'", bt, text)
}
tok, text = conf.next()
}
/* Link configuration */
type cLink struct {
Server string // Server name for link
Address string // {ip,ip6,host}:port
ReceivePass string // Password when server connects to us
ConnectPass string // Password when we connect to server
// Do we use tls.Dial? or compression (no)? Do we auto-connect on start?
SSL, Zip, Auto bool
}
var booleans = map[string]bool {
"true": true,
"yes": true,
"on": true,
"1": true,
"false": false,
"no": false,
"off": false,
"0": false,
/* Static ban configuration */
type cBan interface {
Match(string) bool
Reason() string
}
func (conf *Config) expectBool() (bool, bool) {
tok, text := conf.next()
if val, ok := booleans[text]; tok == scanner.Ident && ok {
return val, ok
}
conf.parseError("Expected boolean, got '%s'", text)
return false, false
// G-Line etc;
type cBanNick struct {
NickMask string // nick!ident@host
Reason string
}
func (conf *Config) expectInt() (int, bool) {
tok, text := conf.next()
num, err := strconv.Atoi(text)
if tok != scanner.Int || err != nil {
conf.parseError("Expected integer, got '%s'", text)
return 0, false
}
return num, true
// Z-Line
type cBanIP struct {
Address string // ip (or hostname), plus optional CIDR netmask
Reason string
ip string // parsed into these
cidr int
}
func (conf *Config) expectString() (string, bool) {
tok, text := conf.next()
if tok != scanner.String && tok != scanner.Ident {
conf.parseError("Expected string, got '%s'", text)
return "", false
}
return text, true
// CTCP version ban
type cBanVersion struct {
VersionRegex string // regex to match against version reply
Reason string
}
func (conf *Config) expect(str string) bool {
_, text := conf.next()
if text != str {
conf.parseError("Expected '%s', got '%s'", str, text)
return false
}
return true
// Ban server from linking to network
type cBanServer struct {
ServerMask string // matched against name of linked server
Reason string
}
func (conf *Config) next() (int, string) {
tok := conf.scan.Scan()
text := conf.scan.TokenText()
if tok == scanner.String {
// drop "quotes" -> quotes
text = text[1:len(text)-1]
}
return tok, text
/* IRCd settings */
type cSettings struct {
SSLKey, SSLCert, SSLCACert string
MaxChans, MaxConnsPerIP int
LogFile string
}
func (conf *Config) parseError(err string, args ...interface{}) {
err = conf.scan.Pos().String() + ": " + err
conf.Errors = append(conf.Errors, os.NewError(fmt.Sprintf(err, args...)))
/* IRCd information */
type cInfo struct {
Name, Network, Info, MOTDFile string
Admins []string
Numeric int
}

View File

@ -1,8 +0,0 @@
package config
type cInfo struct {
Name, Network, Info, MOTDFile string
Admins []string
Numeric int
}

View File

@ -1,12 +0,0 @@
package config
type cLink struct {
Server string // Server name for link
Address string // {ip,ip6,host}:port
ReceivePass string // Password when server connects to us
ConnectPass string // Password when we connect to server
// Do we use tls.Dial? or compression (no)? Do we auto-connect on start?
SSL, Zip, Auto bool
}

View File

@ -1,106 +0,0 @@
package config
import (
"fmt"
"strings"
"scanner"
)
type cOper struct {
Username, Password string
HostMask []string
// Permissions for oper
CanKill, CanBan, CanRenick, CanLink bool
}
var operKeywords = keywordMap{
"password": (*Config).parseOperPassword,
"hostmask": (*Config).parseOperHostMask,
"kill": (*Config).parseOperKill,
"ban": (*Config).parseOperBan,
"renick": (*Config).parseOperRenick,
"link": (*Config).parseOperLink,
}
func defaultOper() *cOper {
return &cOper{
HostMask: []string{},
CanKill: true, CanBan: true,
CanRenick: false, CanLink: false,
}
}
func (o *cOper) String() string {
str := []string{fmt.Sprintf("oper \"%s\" {", o.Username)}
str = append(str, fmt.Sprintf("\tpassword = \"%s\"", o.Password))
if len(o.HostMask) == 0 {
str = append(str, fmt.Sprintf("\thostmask = \"*@*\""))
} else {
for _, h := range o.HostMask {
str = append(str, fmt.Sprintf("\thostmask = \"%s\"", h))
}
}
str = append(str,
fmt.Sprintf("\tkill = %t", o.CanKill),
fmt.Sprintf("\tban = %t", o.CanBan),
fmt.Sprintf("\trenick = %t", o.CanRenick),
fmt.Sprintf("\tlink = %t", o.CanLink),
"}",
)
return strings.Join(str, "\n")
}
func (conf *Config) parseOper() {
oper := defaultOper()
tok, text := conf.next()
if tok != scanner.String && tok != scanner.Ident {
conf.parseError("Invalid username '%s'", text)
return
}
oper.Username = text
conf.parseKwBlock(oper, "oper", operKeywords)
fmt.Println(oper.String())
}
func (conf *Config) parseOperPassword(oi interface{}) {
oper := oi.(*cOper)
if pass, ok := conf.expectString(); ok {
oper.Password = pass
}
}
func (conf *Config) parseOperHostMask(oi interface{}) {
oper := oi.(*cOper)
if mask, ok := conf.expectString(); ok {
oper.HostMask = append(oper.HostMask, mask)
}
}
func (conf *Config) parseOperKill(oi interface{}) {
oper := oi.(*cOper)
if kill, ok := conf.expectBool(); ok {
oper.CanKill = kill
}
}
func (conf *Config) parseOperBan(oi interface{}) {
oper := oi.(*cOper)
if ban, ok := conf.expectBool(); ok {
oper.CanBan = ban
}
}
func (conf *Config) parseOperRenick(oi interface{}) {
oper := oi.(*cOper)
if renick, ok := conf.expectBool(); ok {
oper.CanRenick = renick
}
}
func (conf *Config) parseOperLink(oi interface{}) {
oper := oi.(*cOper)
if link, ok := conf.expectBool(); ok {
oper.CanLink = link
}
}

136
server/config/parser.rl Normal file
View File

@ -0,0 +1,136 @@
package config
import (
"io"
"os"
"fmt"
"net"
"strconv"
)
%% machine config;
%% write data;
var booleans = map[string]bool {
"true": true,
"yes": true,
"on": true,
"1": true,
"false": false,
"no": false,
"off": false,
"0": false,
}
%%{
# mark the current position for acquiring data
action mark { mark = p }
# acceptable boolean values
true = "yes" | "on" | "1" | "true" ;
false = "no" | "off" | "0" | "false" ;
boolean = true | false ;
# IP addresses, v4 and v6
octet = digit # 0-9
| digit digit # 00-99
| [01] digit digit # 000-199
| "2" [0-4] digit # 200-249
| "2" "5" [0-5] ; # 250-255
ipv4addr = octet "." octet "." octet "." octet ;
ipv6addr = ( xdigit{1,4} ":" ){7} xdigit{1,4} # all 8 blocks
| xdigit{0,4} "::" # special case
| ( xdigit{1,4} ":" ){1,6} ":" # first 1-6, ::
| ":" ( ":" xdigit{1,4} ){1,6} # ::, final 1-6
| xdigit{1,4} ":" ( ":" xdigit{1,4} ){1,6} # 1::1-6
| ( xdigit{1,4} ":" ){2}( ":" xdigit{1,4} ){1,5} # 2::1-5
| ( xdigit{1,4} ":" ){3}( ":" xdigit{1,4} ){1,4} # 3::1-4
| ( xdigit{1,4} ":" ){4}( ":" xdigit{1,4} ){1,5} # 4::1-3
| ( xdigit{1,4} ":" ){5}( ":" xdigit{1,4} ){1,2} # 5::1-2
| ( xdigit{1,4} ":" ){6}( ":" xdigit{1,4} ) ; # 6::1
# Actions to create a cPort and save it into the Config struct
action new_port { cur = defaultPort() }
action save_port {
port := cur.(*cPort)
conf.Ports[port.Port] = port
cur = nil
}
# parse and save the port number
action set_portnum {
cur.(*cPort).Port, _ = strconv.Atoi(string(data[mark:p]))
}
portnum = digit+ >mark %set_portnum ;
# parse a bind_ip statement and save the IP
action set_bindip {
cur.(*cPort).BindIP = net.ParseIP(string(data[mark:p]))
}
bindip = (ipv4addr | ipv6addr) >mark %set_bindip ;
portbindip = "bind_ip" " "+ "=" " "+ bindip ;
# parse a class statement and save it
action set_class {
cur.(*cPort).Class = string(data[mark:p])
}
portclass = "class" " "+ "=" " "+ ("server" | "client" >mark %set_class) ;
# parse SSL and Zip booleans
action set_ssl {
cur.(*cPort).SSL = booleans[string(data[mark:p])]
}
portssl = "ssl" " "+ "=" " "+ (boolean >mark %set_ssl) ;
action set_zip {
cur.(*cPort).Zip = booleans[string(data[mark:p])]
}
portzip = "zip" " "+ "=" " "+ (boolean >mark %set_zip) ;
portstmt = ( portbindip | portclass | portssl | portzip ) ;
portblock = "{" space* ( portstmt | (portstmt " "* "\n" space* )+ ) space* "}" ;
# a port configuration can either be:
# port <portnum>\n
# port <portnum> { portblock }
basicport = "port" >new_port " "+ portnum " "* "\n" %save_port;
portdefn = "port" >new_port " "+ portnum " "+ portblock %save_port ;
portconfig = space* ( basicport | portdefn ) space*;
# config = portconfig+
# | operconfig+
# | linkconfig*
# | infoconfig
# | settings ;
main := portconfig+;
}%%
func (conf *Config) Parse(r io.Reader) {
cs, p, mark, pe, eof, buflen := 0, 0, 0, 0, 0, 16384
done := false
var cur interface{}
data := make([]byte, buflen)
%% write init;
for !done {
n, err := r.Read(data)
pe = p + n
if err == os.EOF {
fmt.Println("yeahhhhh.")
done = true
eof = pe
}
%% write exec;
}
if cs < config_first_final {
fmt.Printf("Parse error at %d near '%s'\n", p, data[p:p+10])
}
for _, port := range conf.Ports {
fmt.Println(port.String())
}
}

View File

@ -1,97 +0,0 @@
package config
import (
"net"
"fmt"
"strings"
"scanner"
)
type cPort struct {
Port int
BindIP net.IP // bind to a specific IP for listen port
Class string // "server" or "client"
// Is port a tls.Listener? Does it support compression (no)?
SSL, Zip bool
}
var portKeywords = keywordMap{
"bind_ip": (*Config).parsePortBindIP,
"class": (*Config).parsePortClass,
"ssl": (*Config).parsePortSSL,
"zip": (*Config).parsePortZip,
}
func defaultPort() *cPort {
return &cPort{
BindIP: nil, Class: "client",
SSL: false, Zip: false,
}
}
func (p *cPort) String() string {
str := []string{fmt.Sprintf("port %d {", p.Port)}
if p.BindIP != nil {
str = append(str,
fmt.Sprintf("\tbind_ip = \"%s\"", p.BindIP.String()))
}
str = append(str,
fmt.Sprintf("\tclass = \"%s\"", p.Class),
fmt.Sprintf("\tssl = %t", p.SSL),
fmt.Sprintf("\tzip = %t", p.Zip),
"}",
)
return strings.Join(str, "\n")
}
func (conf *Config) parsePort() {
port := defaultPort()
portnum, ok := conf.expectInt()
if !ok || portnum > 65535 || portnum < 1024 {
conf.parseError("Invalid port '%s'", portnum)
port = nil
} else {
port.Port = portnum
conf.Ports[portnum] = port
}
if conf.scan.Peek() != '\n' {
conf.parseKwBlock(port, "port", portKeywords)
}
fmt.Println(port.String())
}
func (conf *Config) parsePortBindIP(pi interface{}) {
port := pi.(*cPort)
_, text := conf.next()
if ip := net.ParseIP(text); ip != nil {
port.BindIP = ip
} else {
conf.parseError("'%s' is not a valid IP address", text)
}
}
func (conf *Config) parsePortClass(pi interface{}) {
port := pi.(*cPort)
tok, text := conf.next()
if tok == scanner.String && (text == "server" || text == "client") {
port.Class = text
} else {
conf.parseError(
"Port class must be \"server\" or \"client\", got '%s'", text)
}
}
func (conf *Config) parsePortSSL(pi interface{}) {
port := pi.(*cPort)
if ssl, ok := conf.expectBool(); ok {
port.SSL = ssl
}
}
func (conf *Config) parsePortZip(pi interface{}) {
port := pi.(*cPort)
if zip, ok := conf.expectBool(); ok {
port.Zip = zip
}
}

View File

@ -1,8 +0,0 @@
package config
type cSettings struct {
SSLKey, SSLCert, SSLCACert string
MaxChans, MaxConnsPerIP int
LogFile string
}