Switch ircd.conf to block-based configuration format
authorRemco Rijnders <remmy@serenity-irc.net>
Sat, 7 Mar 2026 17:17:36 +0000 (12:17 -0500)
committerRemco Rijnders <remmy@serenity-irc.net>
Sat, 7 Mar 2026 17:17:36 +0000 (12:17 -0500)
Replace the legacy colon-delimited config format with a block-based
format using curly braces and key-value pairs. This eliminates the
colon separator conflict with IPv6 addresses and supports multi-value
fields (e.g. multiple hosts in a single kline block).

The connect{} block merges the old C/N/H line triplet into a single
block. The allow{} block replaces I-lines with separate ip and host
keys. All other block types map directly to their legacy equivalents.

Includes a one-time converter script (tools/convert-conf.py) to
migrate existing configs, and a rewritten chkconf validator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
doc/example.conf
include/h.h
src/chkconf.c
src/parse.c
src/s_conf.c
tools/convert-conf.py [new file with mode: 0755]

index 356e1331ad9cbe9be3dd0427df84c97366746ab8..358e0cccb2fed0ea1405ab32e6cecde2edee5fcb 100644 (file)
 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 #
 #
-# Example ircd.conf file for Serene5.28 or newer.
+# Example ircd.conf file for Serene IRCD.
 #
 #
-# In this file I will explain what the different lines in the ircd.conf file
-# do. You may want to test config changes with chkconf before you try them
-# on your server to prevent disruption to your users due to errors.
+# This file uses a block-based configuration format. Each configuration
+# directive is a named block with key-value pairs inside curly braces.
+# Lines are terminated with semicolons. Comments begin with #.
 #
-# In this file I use the term "ircd" to designate an irc server (daemon).
-#
-# The following terms can be used to determine if you need a line or not:
-# MANDATORY:   The ircd requires the line to operate.
-# NETWORKED:   The ircd needs the line when it is linked to one or more other
-#              servers
+# The following terms can be used to determine if you need a block or not:
+# MANDATORY:   The ircd requires the block to operate.
+# NETWORKED:   The ircd needs the block when it is linked to other servers.
 # SUGGESTED:   It would most likely be a good idea if you used this.
 # OPTIONAL:    It's up to the person doing the configuration.
 # DISCOURAGED: Probably not a good idea, except in limited circumstances.
-# OBSOLETE:    Old, out of date, don't use this
 #
-# NOTE: Many networks will require certain configuration lines to allow 
-#       their networks to run more smoothly. If you are given lines by
-#       the network you are linking to then they should be implemented 
+# NOTE: Many networks will require certain configuration to allow
+#       their networks to run more smoothly. If you are given config
+#       by the network you are linking to then they should be implemented
 #       regardless of this file's recommendation.
 #
-# This file goes through the lines in the order they should be placed in 
-# a functional ircd.conf file.
-#
-# NOTE: Keep in mind that this file is read in REVERSE, that means the
-#       bottom line is the first one read and the top line is the last. 
-#       Remember this when you are doing anything that needs to be in 
-#       a specific order such as I-lines.
-#
-#
-#
-# M: MANDATORY. This line specifies the basic information the server needs
-# about itself to operate.
-#
-# M:server name:IP address:Server description:port
-# Server name is the name the server shows to clients and other servers on
-# the network.
-# IP address tells the server what address on the machine to bind to, if 
-# you fill the field with a "*", or leave it empty, it will bind itself to 
-# all addresses it finds on the machine.
-# Server description is the description of the server shown to clients and
-# other servers on the network.
-# Port is the default port the server will attach itself to.
-# 
-# This line names the server Server1.Serenity-IRC.Net and attaches itself to
-# all addresses attached to the machine on port 7000:
-#
-M:Server1.Serenity-IRC.Net:*:Cool new server:7000
-#
-#
-#
-# A: MANDATORY. This is your administration info that is shown when someone
-# on your server types "/admin" This is a good place to keep the information
-# users need to contact someone in charge.
-#
-# ":" Designates a new line, other than that it's pretty much free form.
-#
-# Here is a format often used:
-# A:server's provider:admin: admin's email : coadmin: coadmin's email 
-#
-A:Lame1 colocations:admin John Random: email JRandom@lame1.com
-#
-#
-#
-# Y: SUGGESTED. Defines connection class types and behavior.
-# Class is the number representing the Y line, Ping frequency defines the
-# duration between pings. Auto-connect frequency defines how often the server
-# will try to connect to the server defined with the corresponding connection
-# class: max connection defines the maximum number of connections the server
-# will allow using this connection class.  
-#
-# Sendq is the maximum space the server will use to buffer outgoing
-# messages.  If the sendq is filled, the affected connection is dropped.
-# I reccomend setting this low for clients (prevents flooding) and high 
-# for servers. 
-#
-# NOTE: The max connections for all Y-lines should add up to the total 
-# number of connections the the server can handle MINUS the number of
-# file desrcriptors that are needed for things like wingate checking and one
-# for the config file. ( 5-10 should do)
-#
-# For example: A server compiled for 256 max filedescriptors should have all
-# Y-lines add up to 250 (remove 6 for server internal use)
-#
-#
-# Y:class:ping frequency:auto-connect frequency:max connections:max sendq
-#
-# This is a typical client Y-line no auto-connect, max 20 connections.
-Y:1:60:0:20:100000
-#
-# This is an example of a Y-line that could be used on your primary hub
-# 
-Y:40:120:60:1:3000000
-# This is an example of a Y-line that could be used for your secondary hubs
-Y:50:120:0:5:3000000
-#
-#
-#
-# I: MANDATORY.  These lines define who can connect to a server, without
-# them no one is able to connect to the server.
-#
-# I:IP mask:password:domain mask::Y-Line (optional)
-#
-# IP Mask is the numeric ip mask (ex 192.168.*) if Arbitrary text is 
-# entered such as NOMATCH the I-line will only match based on the domain 
-# name and will disallow clients from addresses that lack reverse lookup 
-# The password is optional, and is needed to connect using that I line.
-#
-# The domain name is the name returned from a reverse DNS lookup, the Y-line 
-# of course is the Y-line that you want to correspond to the connection.
-# NOTE: I-lines are read starting from the bottom passing though each one
-#       until a line is found that matches the client's connection if none
-#       is found access to the server is denied.  
-#
-# This is a I-line that will allow anyone at all to connect.
-I:*@*::*@*::1
-# 
-# 
-#
-# P: SUGGESTED. Allows you to add ports and addresses/domain sockets other 
-# than the one specified in the M-line.
-#
-# P:address:*:*:port
-#
-# To have the server bind all addresses available on a given port leave the
-# leave the field blank.
-# NOTE: if a given port is already in use the server will simply fail to
-# load.
-#
-# This line would have the server look for connections to port 7000 on
-# the address 127.0.0.1 
-#
-P:127.0.0.1:*:*:7000
-#
-#
-#
-# O: SUGGESTED. O-lines define who has Oper access on the server.  How
-# necessary these line are depend on how well your site is configured and the
-# philosophy of the network you are linked to.
-#
-# O:address:password:nick:flags:Y-line
-#
-# If the server is compiled to use encrypted Oper passwords then you must 
-# use mkpasswd to convert the password from plaintext.
-#
-# NOTE: Addresses using the numeric IP address will work even if it
-#       resolves.
+# NOTE: The config file is read top-to-bottom. For directives where order
+#       matters (such as allow blocks), the first matching block wins.
+#
+#
+#
+# me {} - MANDATORY
+# Specifies the basic information the server needs about itself.
+#
+#   name     - The server name shown to clients and other servers.
+#   info     - The server description shown to clients and other servers.
+#   address  - IP address to bind to. Use "*" or omit to bind all addresses.
+#   port     - Default port the server will listen on.
+#
+me {
+    name Server1.Serenity-IRC.Net;
+    info "Cool new server";
+    address *;
+    port 7000;
+};
+
+#
+# admin {} - MANDATORY
+# Administration info shown when someone types /admin.
+# Each "line" directive adds a line of output.
+#
+admin {
+    line "Lame1 colocations";
+    line "admin John Random";
+    line "email JRandom@lame1.com";
+};
+
+#
+# class {} - SUGGESTED
+# Defines connection class types and behavior.
+#
+#   name       - Class number (referenced by other blocks).
+#   pingfreq   - Duration between pings in seconds.
+#   connfreq   - How often to auto-connect (0 = never).
+#   maxlinks   - Maximum number of connections in this class.
+#   sendq      - Maximum send queue size in bytes.
+#
+# NOTE: The maxlinks for all classes should add up to the total number of
+#       connections the server can handle minus a few for internal use.
+#
+# Typical client class: no auto-connect, max 20 connections.
+class {
+    name 1;
+    pingfreq 60;
+    connfreq 0;
+    maxlinks 20;
+    sendq 100000;
+};
+
+# Primary hub class.
+class {
+    name 40;
+    pingfreq 120;
+    connfreq 60;
+    maxlinks 1;
+    sendq 3000000;
+};
+
+# Secondary hub class.
+class {
+    name 50;
+    pingfreq 120;
+    connfreq 0;
+    maxlinks 5;
+    sendq 3000000;
+};
+
+#
+# allow {} - MANDATORY (replaces I-lines)
+# Defines who can connect to this server. Without allow blocks, no one
+# can connect. The first matching block is used.
+#
+#   ip       - Numeric IP mask (e.g. 192.168.* or 2001:db8::*).
+#              Use "NOMATCH" to only match by hostname.
+#   host     - Hostname mask from reverse DNS lookup.
+#   password - Optional password required to connect.
+#   class    - Connection class number.
+#   port     - Optional: only match connections on this port.
+#
+# This block allows anyone to connect.
+allow {
+    ip *@*;
+    host *@*;
+    class 1;
+};
+
+#
+# listen {} - SUGGESTED (replaces P-lines)
+# Adds additional listen ports beyond the one in the me {} block.
+#
+#   address - IP address to bind (omit or "*" for all addresses).
+#   port    - Port number to listen on.
+#
+listen {
+    address 127.0.0.1;
+    port 7000;
+};
+
+# Listen on an IPv6 address:
+#listen {
+#    address "::1";
+#    port 7000;
+#};
+
+#
+# oper {} - SUGGESTED (replaces O-lines)
+# Defines who has Oper access on the server.
+#
+#   host     - Address mask the oper must connect from.
+#   password - Oper password (use mkpasswd if compiled with encryption).
+#   name     - Oper nickname.
+#   flags    - Access flags (see below).
+#   class    - Connection class number.
 #
 # flags:
-# a = can set +a Designates a services oper
-# A = can set +A to set server admin
-# b = can set temp klines using /kline
-# B = can remove temp klines using /unkline
-# c = can use /squit and /connect on locally connected servers
-# C = can use /squit and /connect on any server 
-# D = can shut down the server using /die
-# f = can see server flood kills by setting /umode +f
-# g = can send globop messages
-# h = can see helpop requests by setting /umode +h 
-# k = can kill clients connected locally
-# K = can kill any clients on the network
-# l = can send locop messages
-# n = can send local server messages (/msg $servername message to users) 
-# N = can send global server messages (/msg $*.net message to users)
-# o = designates a local oper includes flags: bBcfghklnruw
-# O = designates a global oper includes flags: CKNo
-# r = can /rehash the server
-# R = can /restart the server
-# u = can see local connects and disconnects using /umode +c
-# w = can send wallops (obsolete)
+# a = can set +a (services oper)    A = can set +A (server admin)
+# b = can set temp klines           B = can remove temp klines
+# c = local /squit and /connect     C = remote /squit and /connect
+# D = can /die the server           f = can see flood kills (+f)
+# g = can send globops              h = can see helpop requests (+h)
+# k = can kill local clients        K = can kill any client
+# l = can send locops               n = local server messages
+# N = global server messages        o = local oper (includes bBcfghklnruw)
+# O = global oper (includes CKNo)   r = can /rehash
+# R = can /restart                  u = can see connects (+c)
+# w = can send wallops
 #
 # NOTE: Access to everything is: AaDOR
 #
-O:192.168.2.*:notarealpassword:AnOper:OR:10
-#
-#
-#
-# X: SUGGESTED. Sets a password for the /die and /restart commands.
-#
-# X:Die password:Restart password
-#
-X:killme:restartme
-#
-#
-# 
-# C: NETWORKED. Connect lines, this line defines who your server can 
-# connect to. C-lines must be paired with an N line to work.
-# 
-# C:remote address:password:remote server name:port to auto-connect:Y-line
-# The address is the machine's Internet address, the server name is
-# the server's name on irc network as defined by the machine's M-line.  
-# They do not have to be the same.
-# NOTE: Using the numeric ip address for the machine's address is slightly 
-#       faster and more secure than using its domain name. 
-#
-C:192.168.2.1:notarealpassword:HubServer.Serenity-IRC.Net:7827:45
-#
-#
-#
-# N: NETWORKED. This line defines what servers your server can connect 
-# to yours. N-lines must be paired with an C line to work correctly.
-#
-# N:remote address:password:remote server name::Y-line
-# The address is the machine's address on the Internet, the server name is
-# the server's name on irc network as defined by the machine's M-line.
-# They do not have to be the same.
-#
-# NOTE: Using the numeric IP address for the machine's address is slightly
-#      faster and more secure than using its domain name.
-#
-N:192.168.2.1:notarealpassword:HubServer.Serenity-IRC.Net::40
-#
-#
-#
-# U: NETWORKED. Determines what servers may make changes to things like
-# channel modes without the server sounding alarms or attempting to stop
-# them.
-#
-# U:Server-Name:*:*
-#
-# This line allows services to have the access needed to do its job.
-#
-U:Services.Serenity-IRC.Net:*:*
-#
-#
-#
-# H: NETWORKED. Hub lines. These define what servers a given hub is permitted 
-# to connect to the network.
-# H:servers the hub is allowed connect::name
-#
-# NOTE: The name is the name defined in the hub's M-line it might not be the
-#       same as its address.
-#
-# This would allow HubServer.Serenity-IRC.Net to hub any servers while being directly
-# connected to your server.
-H:*::HubServer.Serenity-IRC.Net
-#
-# This line would only allow HubServer2 to have Mexican servers as leaves.
-H:*.mx.*:HubServer2.Serenity-IRC.Net
-#
-#
-#
-# G: OPTIONAL. General configuration options. These control runtime behavior
-# that was previously set at compile time.
-#
-# G:option
-#
-# Currently supported options:
-#   hub  - Enable hub mode. A hub server accepts multiple server connections
-#          and routes traffic between them. Without this option, the server
-#          runs as a leaf and will only maintain a single server link.
-#          This setting is re-evaluated on /rehash.
-#
-# Uncomment the following line if this server is a hub:
-#G:hub
-#
-#
-#
-# Q: OPTIONAL. Nick quarantine.  This line prevents non opers from using
-# nicks covered by the Q-line.
-#
-# Q::reason:nick
-#
-Q::users may not use services nicks:*serv*
-#
-#
-#
-# q: OBSOLETE. Server quarantine. This line quarantines the specified
-# server, the line must be on ALL SERVERS connected to the network or net
-# spits will occur.
-#
-# q::reason:server
-#
-q::I have no idea why:unknown.Serenity-IRC.Net
-#
-#
-#
-# K: OPTIONAL. Kill Line. K-lines are a means of denying access to 
-# certain clients. 
-#
-# K-lines work best for addresses that are likely to be banned long term 
-# and therefore a temporary kline or akill won't do.
-#
-# K:address:reason:ident
-#
-# Address is the address or mask of the client you wish to ban.
-# NOTE: If you ban an IP address and reverse DNS is successful the 
-#       domain name returned will be checked and NOT the IP address, any bans 
-#       on the IP address will be IGNORED.  This applies to both Klines
-#       and A-kills.
-#
-# The reason field is what is shown to the client on the receiving end 
-# of the kline.
-#
-# The ident is checked against what is returned by the users, keep in mind
-# that this is easily changeable, and not useful if you're trying to keep a
-# particular user off the network, although it is a lot more logical for temp 
-# K-lines or A-kills since they are more readily changed.
-# This will ban any user from lame.com and tell them it was for mass
-# advertising:
-K:*.lame.com:Mass advertising:*
-#
-# This will ban anyone using the ident Sphere and tell them to get 
-# a new script.
-K:*:Get a new script:Sphere
-#
-#
-# Z: DISCOURAGED. Disconnects the client at the earliest stage of connection.
-# useful for preventing annoying server messages by persistent attempted 
-# server connections, or getting rid of people ban evading using vhosts.
-#
-# NOTE: 1 The address MUST be a numeric IP or it won't work.
-# NOTE: 2 The last field must be a * or strange things may happen to the
-#         server.
-#
-# Z:address:reason:*
-# 
-Z:192.168.2.2:not a server:*
+oper {
+    host 192.168.2.*;
+    password notarealpassword;
+    name AnOper;
+    flags OR;
+    class 10;
+};
+
+#
+# drpass {} - SUGGESTED (replaces X-lines)
+# Sets passwords for the /die and /restart commands.
+#
+drpass {
+    die killme;
+    restart restartme;
+};
+
+#
+# connect {} - NETWORKED (replaces C/N-line pairs)
+# Defines a server this server can link with. Each connect block
+# combines the old C-line (outgoing) and N-line (incoming) into one.
+#
+#   host         - Remote server's IP address or hostname.
+#   password     - Link password (must match on both ends).
+#   name         - Remote server's IRC name (from their me {} block).
+#   port         - Port to auto-connect on (0 = no auto-connect).
+#   class        - Connection class number.
+#   hub          - Optional: mask of servers this link may introduce.
+#                  Use "*" to allow any. Omit for leaf-only links.
+#
+connect {
+    host 192.168.2.1;
+    password notarealpassword;
+    name HubServer.Serenity-IRC.Net;
+    port 7827;
+    class 40;
+    hub *;
+};
+
+# A connect block where the remote is only allowed to hub Mexican servers:
+#connect {
+#    host 192.168.2.2;
+#    password anotherpassword;
+#    name HubServer2.Serenity-IRC.Net;
+#    port 0;
+#    class 50;
+#    hub *.mx.*;
+#};
+
+#
+# uworld {} - NETWORKED (replaces U-lines)
+# Determines which servers may make privileged changes (e.g. services).
+#
+uworld {
+    name Services.Serenity-IRC.Net;
+};
+
+#
+# general {} - OPTIONAL (replaces G-lines)
+# General runtime configuration options.
+#
+#   hub  - Enable hub mode. A hub accepts multiple server connections
+#          and routes traffic between them. Without this, the server
+#          runs as a leaf with a single server link.
+#          Re-evaluated on /rehash.
+#
+# Uncomment the following to enable hub mode:
+#general {
+#    hub yes;
+#};
+
+#
+# quarantine {} - OPTIONAL (replaces Q-lines for nicks)
+# Prevents non-opers from using matching nicknames.
+#
+#   nick   - Nick mask to quarantine (can use wildcards).
+#   reason - Reason shown to users.
+#
+quarantine {
+    nick *serv*;
+    reason "users may not use services nicks";
+};
+
+#
+# squar {} - server quarantine (replaces q-lines)
+# Quarantines a server. Must be on ALL servers or netsplits will occur.
+#
+#   name   - Server name to quarantine.
+#   reason - Reason for quarantine.
+#
+#squar {
+#    name unknown.Serenity-IRC.Net;
+#    reason "I have no idea why";
+#};
+
+#
+# kline {} - OPTIONAL (replaces K-lines)
+# Denies access to matching clients. Multiple host entries can share
+# a single reason, avoiding repetition.
+#
+#   host   - Address or mask to ban. Can be repeated for multiple hosts.
+#   ident  - Ident/username mask (default: *).
+#   reason - Reason shown to the banned client.
+#
+# NOTE: If reverse DNS succeeds, the hostname is checked, not the IP.
+#       To ban by IP when DNS resolves, add both hostname and IP bans.
+#
+# Ban all users from lame.com:
+kline {
+    host *.lame.com;
+    ident *;
+    reason "Mass advertising";
+};
+
+# Ban a specific ident from anywhere:
+kline {
+    host *;
+    ident Sphere;
+    reason "Get a new script";
+};
+
+# Ban multiple hosts for the same reason (replaces many old K-lines):
+#kline {
+#    host *.spam1.example.com;
+#    host *.spam2.example.com;
+#    host *.spam3.example.com;
+#    ident *;
+#    reason "Spam network";
+#};
+
+#
+# zline {} - DISCOURAGED (replaces Z-lines)
+# Disconnects clients at the earliest stage of connection by IP.
+# Useful for persistent connection attempts or ban evasion via vhosts.
+#
+#   address - Numeric IP address (must be numeric, not hostname).
+#   reason  - Reason for the ban.
+#
+zline {
+    address 192.168.2.2;
+    reason "not a server";
+};
+
+# IPv6 zline example:
+#zline {
+#    address "2001:db8::dead:beef";
+#    reason "not welcome";
+#};
index 3c04e96f632526d62846229c33f1423500ecde19..04915a01ee5d62756c3ae00875df81dd1913ec3f 100644 (file)
@@ -123,7 +123,6 @@ extern  void    RemoveZLine(char *);
 
 extern         char    *MyMalloc (), *MyRealloc () ;
 extern char    *debugmode, *configfile, *sbrk0;
-extern char    *getfield (char *);
 extern void    get_sockhost (aClient *, char *);
 extern char    *rpl_str (int), *err_str (int);
 extern char    *strerror (int);
index 4b0a99a755d55f266750da8cb7a17930179108c7..1a59598ff854b15f1066b60e089d0ab04ead9eb9 100644 (file)
 
 #undef free
 #define        MyMalloc(x)     malloc(x)
-#define Reg
 
-static void new_class ();
-static char *getfield (), confchar ();
-static int openconf (), validate ();
-static aClass *get_class ();
-static aConfItem *initconf ();
-
-static int numclasses = 0, *classarr = (int *) NULL, debugflag = 0;
+static int debugflag = 0;
 static char *configfile = CONFIGFILE;
-static char nullfield[] = "";
-static char maxsendq[12];
+static int errors = 0;
+static int has_me = 0, has_admin = 0;
+static int num_chk_classes = 0;
+static int chk_classes[256];
 
-int main (int argc, char *argv[])
+static void note_class (int cn)
 {
-    new_class (0);
+    int i;
+    for (i = 0; i < num_chk_classes; i++)
+       if (chk_classes[i] == cn)
+           return;
+    if (num_chk_classes < 256)
+       chk_classes[num_chk_classes++] = cn;
+}
 
-    if (chdir (DPATH)) {
-       perror ("chdir");
-       exit (-1);
-    }
-    if (argc > 1 && !strncmp (argv[1], "-d", 2)) {
-       debugflag = 1;
-       if (argv[1][2])
-           debugflag = atoi (argv[1] + 2);
-       argc--, argv++;
+static int class_exists (int cn)
+{
+    int i;
+    for (i = 0; i < num_chk_classes; i++)
+       if (chk_classes[i] == cn)
+           return 1;
+    return 0;
+}
+
+/* Read entire file into a malloc'd buffer */
+static char *read_file (const char *path)
+{
+    struct stat st;
+    char *buf;
+    int fd, n, total = 0;
+
+    fd = open (path, O_RDONLY);
+    if (fd < 0)
+       return NULL;
+    if (fstat (fd, &st) < 0) {
+       close (fd);
+       return NULL;
     }
-    else if (argc > 1 && !strncmp (argv[1], "-h", 2)) {
-       printf ("chkconf [-d] [conf file]\n");
-       printf ("-d = debug mode\n");
-       printf ("conf file = path to ircd.conf\n\n");
-       exit (0);
+    buf = (char *) malloc (st.st_size + 1);
+    while (total < st.st_size) {
+       n = read (fd, buf + total, st.st_size - total);
+       if (n <= 0)
+           break;
+       total += n;
     }
-    if (argc > 1)
-       configfile = argv[1];
-    return validate (initconf ());
+    buf[total] = '\0';
+    close (fd);
+    return buf;
 }
 
-/*
- * openconf
- *
- * returns -1 on any error or else the fd opened from which to read the
- * configuration file from.
- */
-static int openconf ()
+static char *skip_ws (char *p)
 {
-    return open (configfile, O_RDONLY);
+    while (*p) {
+       if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')
+           p++;
+       else if (*p == '#') {
+           while (*p && *p != '\n')
+               p++;
+       } else
+           break;
+    }
+    return p;
 }
 
-static int oper_access[] = {
-    ~(OFLAG_ADMIN | OFLAG_SADMIN | OFLAG_ZLINE), '*',
-    OFLAG_LOCAL, 'o',
-    OFLAG_GLOBAL, 'O',
-    OFLAG_REHASH, 'r',
-    OFLAG_DIE, 'D',
-    OFLAG_RESTART, 'R',
-    OFLAG_GLOBOP, 'g',
-    OFLAG_LOCOP, 'l',
-    OFLAG_LROUTE, 'c',
-    OFLAG_GROUTE, 'C',
-    OFLAG_LKILL, 'k',
-    OFLAG_GKILL, 'K',
-    OFLAG_KLINE, 'b',
-    OFLAG_UNKLINE, 'B',
-    OFLAG_LNOTICE, 'n',
-    OFLAG_GNOTICE, 'N',
-    OFLAG_ADMIN, 'A',
-    OFLAG_SADMIN, 'a',
-    OFLAG_UMODEC, 'u',
-    OFLAG_UMODEF, 'f',
-    OFLAG_ZLINE, 'z',
-    0, 0
-};
+static char *get_token (char *p, char *dst, int dstsize)
+{
+    int i = 0;
+
+    p = skip_ws (p);
+    if (*p == '"') {
+       p++;
+       while (*p && *p != '"' && i < dstsize - 1) {
+           if (*p == '\\' && *(p + 1)) {
+               p++;
+               switch (*p) {
+               case 'n': dst[i++] = '\n'; break;
+               case 't': dst[i++] = '\t'; break;
+               case '\\': dst[i++] = '\\'; break;
+               case '"': dst[i++] = '"'; break;
+               default: dst[i++] = *p; break;
+               }
+               p++;
+           } else
+               dst[i++] = *p++;
+       }
+       if (*p == '"')
+           p++;
+    } else {
+       while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n'
+              && *p != ';' && *p != '{' && *p != '}' && i < dstsize - 1)
+           dst[i++] = *p++;
+    }
+    dst[i] = '\0';
+    return p;
+}
 
-/*
-   ** initconf() 
-   **    Read configuration file.
-   **
-   **    returns -1, if file cannot be opened
-   **             0, if file opened
- */
-static aConfItem * initconf (int opt)
+/* Count approximate line number at position p within buf */
+static int line_number (char *buf, char *p)
 {
-    int fd;
-    char line[512], *tmp, c[80], *s;
-    int ccount = 0, ncount = 0, dh, flags = 0;
-    int lineno;
-    aConfItem *aconf = NULL, *ctop = NULL;
+    int n = 1;
+    char *s;
+    for (s = buf; s < p; s++)
+       if (*s == '\n')
+           n++;
+    return n;
+}
 
-    (void) fprintf (stderr, "\nOpening %s as ircd configuration file\n\n",
-                   configfile);
-    if ((fd = openconf ()) == -1) {
-       return NULL;
+static int validate_conf (const char *path)
+{
+    char *filebuf, *p, *block_start;
+    char blocktype[512], key[512], value[512];
+    int has_connect_port;
+
+    filebuf = read_file (path);
+    if (!filebuf) {
+       fprintf (stderr, "ERROR: Cannot open %s\n", path);
+       return -1;
     }
-    lineno = 0;
-    (void) dgets (-1, NULL, 0);          /* make sure buffer is at empty pos */
-    while ((dh = dgets (fd, line, sizeof (line) - 1)) > 0) {
-       /* The +2 here is a silly hack, but it does cause the correct line number
-        * to be reported when it actually reaches the end of file case. This is
-        * the only place lineno should be incremented, otherwise we're causing
-        * an off by one error in the line number for each error reported. -Studded 
-        */
-       printf ("%u:\tEnd of file\r", (lineno++ + 2));
-       if (aconf) {
-           if (aconf->host)
-               (void) free (aconf->host);
-           if (aconf->passwd)
-               (void) free (aconf->passwd);
-           if (aconf->name)
-               (void) free (aconf->name);
-       }
-       else
-           aconf = (aConfItem *) malloc (sizeof (*aconf));
-       aconf->host = (char *) NULL;
-       aconf->passwd = (char *) NULL;
-       aconf->name = (char *) NULL;
-       aconf->class = (aClass *) NULL;
-       if ((tmp = (char *) strchr (line, '\n')))
-           *tmp = 0;
-       else
-           while (dgets (fd, c, sizeof (c) - 1))
-               if ((tmp = (char *) strchr (c, '\n'))) {
-                   *tmp = 0;
-                   break;
-               }
-       /*
-        * Do quoting of characters and # detection.
-        */
-       for (tmp = line; *tmp; tmp++) {
-           if (*tmp == '\\') {
-               switch (*(tmp + 1)) {
-               case 'n':
-                   *tmp = '\n';
-                   break;
-               case 'r':
-                   *tmp = '\r';
-                   break;
-               case 't':
-                   *tmp = '\t';
-                   break;
-               case '0':
-                   *tmp = '\0';
-                   break;
-               default:
-                   *tmp = *(tmp + 1);
-                   break;
-               }
-               if (!*(tmp + 1))
-                   break;
-               else
-                   for (s = tmp; (*s = *++s););
-               tmp++;
-           }
-           else if (*tmp == '#')
-               *tmp = '\0';
-       }
-       if (!*line || *line == '#' || *line == '\n' ||
-           *line == ' ' || *line == '\t')
-           continue;
 
-       if (line[1] != ':') {
-           (void) fprintf (stderr, "%u:\tERROR: Bad config line (%s)\n",
-                           lineno, line);
-           continue;
+    fprintf (stderr, "Checking %s\n\n", path);
+
+    /* Class 0 always exists */
+    note_class (0);
+
+    p = filebuf;
+    while (*(p = skip_ws (p))) {
+       block_start = p;
+       p = get_token (p, blocktype, sizeof (blocktype));
+       p = skip_ws (p);
+       if (*p != '{') {
+           fprintf (stderr, "Line %d: ERROR: Expected '{' after '%s'\n",
+                    line_number (filebuf, block_start), blocktype);
+           errors++;
+           break;
        }
+       p++;
+
        if (debugflag)
-           (void) printf ("\n%s\n", line);
-       (void) fflush (stdout);
+           printf ("Block: %s (line %d)\n", blocktype, line_number (filebuf, block_start));
+
+       /* Track fields seen in this block */
+       int has_name = 0, has_host = 0, has_port = 0, has_password = 0;
+       int has_class = 0, has_info = 0, has_line = 0;
+       int admin_lines = 0;
+       int class_num = -1;
+       int name_num = -1;
+       has_connect_port = 0;
+
+       while (*(p = skip_ws (p)) && *p != '}') {
+           char *key_pos = p;
+           p = get_token (p, key, sizeof (key));
+           if (!*key)
+               break;
+           p = get_token (p, value, sizeof (value));
+           p = skip_ws (p);
+           if (*p == ';')
+               p++;
+           else {
+               fprintf (stderr, "Line %d: WARNING: Missing ';' after '%s %s'\n",
+                        line_number (filebuf, key_pos), key, value);
+           }
 
-       tmp = getfield (line);
-       if (!tmp) {
-           (void) fprintf (stderr, "\tERROR: no fields found\n");
-           continue;
+           if (!mycmp (key, "name")) { has_name = 1; name_num = atoi (value); }
+           else if (!mycmp (key, "host")) has_host = 1;
+           else if (!mycmp (key, "address")) has_host = 1;
+           else if (!mycmp (key, "ip")) has_host = 1;
+           else if (!mycmp (key, "port")) { has_port = 1; has_connect_port = atoi (value); }
+           else if (!mycmp (key, "password") || !mycmp (key, "passwd")) has_password = 1;
+           else if (!mycmp (key, "class")) { has_class = 1; class_num = atoi (value); }
+           else if (!mycmp (key, "info")) has_info = 1;
+           else if (!mycmp (key, "line")) { has_line = 1; admin_lines++; }
+           else if (!mycmp (key, "flags")) ;
+           else if (!mycmp (key, "hub")) ;
+           else if (!mycmp (key, "ident")) ;
+           else if (!mycmp (key, "reason")) ;
+           else if (!mycmp (key, "nick")) has_name = 1;
+           else if (!mycmp (key, "die")) ;
+           else if (!mycmp (key, "restart")) ;
+           else if (!mycmp (key, "pingfreq")) ;
+           else if (!mycmp (key, "connfreq")) ;
+           else if (!mycmp (key, "maxlinks")) ;
+           else if (!mycmp (key, "sendq")) ;
+           else if (!mycmp (key, "time")) ;
+           else {
+               fprintf (stderr, "Line %d: WARNING: Unknown key '%s' in %s block\n",
+                        line_number (filebuf, key_pos), key, blocktype);
+           }
        }
-       aconf->status = CONF_ILLEGAL;
 
-       switch (*tmp) {
-       case 'A':                 /* Name, e-mail address of administrator */
-           aconf->status = CONF_ADMIN;
-           break;
-       case 'a':                 /* of this server. */
-           aconf->status = CONF_SADMIN;
-           break;
-       case 'C':                 /* Server where I should try to connect */
-       case 'c':                 /* in case of lp failures             */
-           ccount++;
-           aconf->status = CONF_CONNECT_SERVER;
-           break;
-       case 'E':                 /* Blocked DCC file type */
-           aconf->status = CONF_DCCBLOCK;
-           break;
-       case 'f':                 /* Temp Z-line time */
-       case 'F':
-           aconf->status = CONF_ZTIME;
-           break;
-       case 'H':                 /* Hub server line */
-       case 'h':
-           aconf->status = CONF_HUB;
-           break;
-       case 'I':                 /* Just plain normal irc client trying  */
-       case 'i':                 /* to connect me */
-           aconf->status = CONF_CLIENT;
-           break;
-       case 'K':                 /* Kill user line on irc.conf           */
-       case 'k':
-           aconf->status = CONF_KILL;
-           break;
-           /* Me. Host field is name used for this host */
-           /* and port number is the number of the port */
-       case 'M':
-       case 'm':
-           aconf->status = CONF_ME;
-           break;
-       case 'N':                 /* Server where I should NOT try to     */
-       case 'n':                 /* connect in case of lp failures     */
-           /* but which tries to connect ME        */
-           ++ncount;
-           aconf->status = CONF_NOCONNECT_SERVER;
-           break;
-       case 'O':
-           aconf->status = CONF_OPERATOR;
-           break;
-           /* Local Operator, (limited privs --SRB)
-            * Not anymore, OperFlag access levels. -Cabal95 */
-       case 'o':
-           aconf->status = CONF_OPERATOR;
-           break;
-       case 'P':                 /* listen port line */
-       case 'p':
-           aconf->status = CONF_LISTEN_PORT;
-           break;
-       case 'Q':                 /* a server that you don't want in your */
-       case 'q':                 /* network. USE WITH CAUTION! */
-           aconf->status = CONF_QUARANTINED_SERVER;
-           break;
-       case 'S':                 /* Service. Same semantics as   */
-       case 's':                 /* CONF_OPERATOR                */
-           aconf->status = CONF_SERVICE;
-           break;
-       case 'U':
-       case 'u':
-           aconf->status = CONF_UWORLD;
-           break;
-       case 'X':
-       case 'x':
-           aconf->status = CONF_DRPASS;
-           break;
-       case 'Y':
-       case 'y':
-           aconf->status = CONF_CLASS;
-           break;
-       case 'Z':
-       case 'z':
-           aconf->status = CONF_ZAP;
-           break;
-       default:
-           (void) fprintf (stderr,
-                           "%u:\tERROR: unknown conf line letter (%c)\n",
-                           lineno, *tmp);
-           break;
-       }
+       if (*p == '}')
+           p++;
+       p = skip_ws (p);
+       if (*p == ';')
+           p++;
 
-       if (IsIllegal (aconf))
-           continue;
+       /* Validate required fields per block type */
+       int bline = line_number (filebuf, block_start);
 
-       for (;;) {                /* Fake loop, that I can use break here --msa */
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           DupString (aconf->host, tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           DupString (aconf->passwd, tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           DupString (aconf->name, tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           if (aconf->status & CONF_OPERATOR) {
-               int *i, flag;
-               char *m = "*";
-               /*
-                * Now we use access flags to define  
-                * what an operator can do with their O.   
-                */
-               for (m = (*tmp) ? tmp : m; *m; m++) {
-                   for (i = oper_access; (flag = *i); i += 2)
-                       if (*m == (char) (*(i + 1))) {
-                           aconf->port |= flag;
-                           break;
-                       }
-                   if (flag == 0)
-                       fprintf (stderr,
-                                "%u:\tWARNING: Unknown oper access level '%c'\n",
-                                lineno, *m);
-               }
-               if (!(aconf->port & OFLAG_ISGLOBAL))
-                   aconf->status = CONF_LOCOP;
+       if (!mycmp (blocktype, "me")) {
+           if (has_me) {
+               fprintf (stderr, "Line %d: ERROR: Duplicate me {} block\n", bline);
+               errors++;
+           }
+           has_me = 1;
+           if (!has_name) {
+               fprintf (stderr, "Line %d: ERROR: me {} requires 'name'\n", bline);
+               errors++;
+           }
+           if (!has_port) {
+               fprintf (stderr, "Line %d: WARNING: me {} has no 'port'\n", bline);
            }
-           else
-               aconf->port = atoi (tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           if (!(aconf->status & CONF_CLASS))
-               aconf->class = get_class (atoi (tmp));
-           break;
        }
-       if (!aconf->class && (aconf->status & (CONF_CONNECT_SERVER |
-                                              CONF_NOCONNECT_SERVER |
-                                              CONF_OPS | CONF_CLIENT))) {
-           (void) fprintf (stderr, "%u:\tWARNING: No class.  Default 0\n",
-                           lineno);
-           aconf->class = get_class (0);
+       else if (!mycmp (blocktype, "admin")) {
+           if (has_admin) {
+               fprintf (stderr, "Line %d: ERROR: Duplicate admin {} block\n", bline);
+               errors++;
+           }
+           has_admin = 1;
+           if (admin_lines == 0) {
+               fprintf (stderr, "Line %d: WARNING: admin {} has no 'line' entries\n", bline);
+           }
        }
-       /* Check for bad Z-lines */
-       if (aconf->status == CONF_ZAP) {
-           char *tempc = aconf->host;
-           if (!tempc) {
-               fprintf (stderr, "%u:\tERROR: Bad Z-line\n", lineno);
+       else if (!mycmp (blocktype, "class")) {
+           if (!has_name && class_num < 0) {
+               fprintf (stderr, "Line %d: ERROR: class {} requires 'name'\n", bline);
+               errors++;
+           } else {
+               int cn = name_num >= 0 ? name_num : class_num;
+               note_class (cn);
+           }
+       }
+       else if (!mycmp (blocktype, "allow")) {
+           if (!has_host) {
+               fprintf (stderr, "Line %d: ERROR: allow {} requires 'ip' or 'host'\n", bline);
+               errors++;
+           }
+           if (has_class && !class_exists (class_num)) {
+               fprintf (stderr, "Line %d: WARNING: allow {} references undefined class %d\n",
+                        bline, class_num);
            }
-           for (; *tempc; tempc++)
-               if ((*tempc >= '0') && (*tempc <= '9'))
-                   goto zap_safe;
-           fprintf (stderr, "%u:\tERROR: Z-line mask too broad\n", lineno);
-         zap_safe:;
        }
-       /* Check for bad F lines */
-       if (aconf->status == CONF_ZTIME) {
-           if (!((aconf->host) && isdigit (*aconf->host)))
-               (void) fprintf (stderr,
-                               "%u:\tERROR: F-lines must contain a digit in the first slot\n",
-                               lineno);
+       else if (!mycmp (blocktype, "listen")) {
+           if (!has_port) {
+               fprintf (stderr, "Line %d: ERROR: listen {} requires 'port'\n", bline);
+               errors++;
+           }
        }
-       /*
-          ** If conf line is a class definition, create a class entry
-          ** for it and make the conf_line illegal and delete it.
-        */
-       if (aconf->status & CONF_CLASS) {
-           if (!aconf->host) {
-               (void) fprintf (stderr, "\tERROR: no class #\n");
-               continue;
+       else if (!mycmp (blocktype, "oper")) {
+           if (!has_host) {
+               fprintf (stderr, "Line %d: ERROR: oper {} requires 'host'\n", bline);
+               errors++;
+           }
+           if (!has_password) {
+               fprintf (stderr, "Line %d: ERROR: oper {} requires 'password'\n", bline);
+               errors++;
            }
-           if (!tmp) {
-               (void) fprintf (stderr,
-                               "%u:\tWARNING: missing sendq field\n",
-                               lineno);
-               (void) fprintf (stderr, "\t\t default: %d\n", MAXSENDQLENGTH);
-               (void) sprintf (maxsendq, "%d", MAXSENDQLENGTH);
+           if (!has_name) {
+               fprintf (stderr, "Line %d: ERROR: oper {} requires 'name'\n", bline);
+               errors++;
            }
-           else
-               (void) sprintf (maxsendq, "%d", atoi (tmp));
-           new_class (atoi (aconf->host));
-           aconf->class = get_class (atoi (aconf->host));
-           goto print_confline;
        }
-       if (aconf->status & CONF_LISTEN_PORT) {
-           if (!aconf->host)
-               (void) fprintf (stderr, "\tERROR: %s\n",
-                               "null host field in P-line");
-           else if (strchr (aconf->host, '/'))
-               (void) fprintf (stderr,
-                               "%u:\tWARNING: / present in P-line "
-                               "UNIXPORT configuration no longer supported\n",
-                               lineno);
-           aconf->class = get_class (0);
-           goto print_confline;
+       else if (!mycmp (blocktype, "connect")) {
+           if (!has_host) {
+               fprintf (stderr, "Line %d: ERROR: connect {} requires 'host'\n", bline);
+               errors++;
+           }
+           if (!has_password) {
+               fprintf (stderr, "Line %d: ERROR: connect {} requires 'password'\n", bline);
+               errors++;
+           }
+           if (!has_name) {
+               fprintf (stderr, "Line %d: ERROR: connect {} requires 'name'\n", bline);
+               errors++;
+           }
+           if (has_class && !class_exists (class_num)) {
+               fprintf (stderr, "Line %d: WARNING: connect {} references undefined class %d\n",
+                        bline, class_num);
+           }
        }
-       if (aconf->status & CONF_SERVER_MASK &&
-           (!aconf->host || strchr (aconf->host, '*') ||
-            strchr (aconf->host, '?'))) {
-           (void) fprintf (stderr, "\tERROR: bad host field\n");
-           continue;
+       else if (!mycmp (blocktype, "drpass")) {
+           /* Optional, no required fields */
        }
-       if (aconf->status & CONF_SERVER_MASK && BadPtr (aconf->passwd)) {
-           (void) fprintf (stderr, "\tERROR: empty/no password field\n");
-           continue;
+       else if (!mycmp (blocktype, "uworld")) {
+           if (!has_name) {
+               fprintf (stderr, "Line %d: ERROR: uworld {} requires 'name'\n", bline);
+               errors++;
+           }
        }
-       if (aconf->status & CONF_SERVER_MASK && !aconf->name) {
-           (void) fprintf (stderr, "\tERROR: bad name field\n");
-           continue;
+       else if (!mycmp (blocktype, "general")) {
+           /* All optional */
        }
-       if (aconf->status & (CONF_SERVER_MASK | CONF_OPS))
-           if (!strchr (aconf->host, '@')) {
-               char *newhost;
-               int len = 3;    /* *@\0 = 3 */
-
-               len += strlen (aconf->host);
-               newhost = (char *) MyMalloc (len);
-               (void) sprintf (newhost, "*@%s", aconf->host);
-               (void) free (aconf->host);
-               aconf->host = newhost;
+       else if (!mycmp (blocktype, "quarantine")) {
+           if (!has_name) {
+               fprintf (stderr, "Line %d: WARNING: quarantine {} has no 'nick'\n", bline);
            }
-
-       if (!aconf->class)
-           aconf->class = get_class (0);
-       (void) sprintf (maxsendq, "%d", aconf->class->class);
-
-       if (!aconf->name)
-           aconf->name = nullfield;
-       if (!aconf->passwd)
-           aconf->passwd = nullfield;
-       if (!aconf->host)
-           aconf->host = nullfield;
-       if (aconf->status & (CONF_ME | CONF_ADMIN)) {
-           if (flags & aconf->status)
-               (void) fprintf (stderr,
-                               "ERROR: multiple %c-lines\n",
-                               toupper (confchar (aconf->status)));
-           else
-               flags |= aconf->status;
        }
-      print_confline:
-       if (debugflag > 8)
-           (void) printf ("(%d) (%s) (%s) (%s) (%d) (%s)\n",
-                          aconf->status, aconf->host, aconf->passwd,
-                          aconf->name, aconf->port, maxsendq);
-       (void) fflush (stdout);
-       if (aconf->status & (CONF_SERVER_MASK | CONF_HUB)) {
-           aconf->next = ctop;
-           ctop = aconf;
-           aconf = NULL;
+       else if (!mycmp (blocktype, "squar")) {
+           if (!has_name) {
+               fprintf (stderr, "Line %d: WARNING: squar {} has no 'name'\n", bline);
+           }
+       }
+       else if (!mycmp (blocktype, "kline")) {
+           if (!has_host) {
+               fprintf (stderr, "Line %d: ERROR: kline {} requires 'host'\n", bline);
+               errors++;
+           }
+       }
+       else if (!mycmp (blocktype, "zline")) {
+           if (!has_host) {
+               fprintf (stderr, "Line %d: ERROR: zline {} requires 'address'\n", bline);
+               errors++;
+           }
+       }
+       else if (!mycmp (blocktype, "service")) {
+           /* Optional */
+       }
+       else if (!mycmp (blocktype, "dccblock")) {
+           /* Optional */
+       }
+       else {
+           fprintf (stderr, "Line %d: WARNING: Unknown block type '%s'\n",
+                    bline, blocktype);
        }
     }
-    printf ("\n");
-    (void) close (fd);
-    return ctop;
-}
 
-static aClass * get_class (int cn)
-{
-    static aClass cls;
-    int i = numclasses - 1;
-
-    cls.class = -1;
-    for (; i >= 0; i--)
-       if (classarr[i] == cn) {
-           cls.class = cn;
-           break;
-       }
-    if (i == -1)
-       (void) fprintf (stderr, "\tWARNING: class %d not found\n", cn);
-    return &cls;
-}
+    if (!has_me) {
+       fprintf (stderr, "ERROR: No me {} block found (required)\n");
+       errors++;
+    }
+    if (!has_admin) {
+       fprintf (stderr, "WARNING: No admin {} block found\n");
+    }
 
-static void new_class (int cn)
-{
-    numclasses++;
-    if (classarr)
-       classarr = (int *) realloc (classarr, sizeof (int) * numclasses);
+    fprintf (stderr, "\n");
+    if (errors)
+       fprintf (stderr, "%d error(s) found.\n", errors);
     else
-       classarr = (int *) malloc (sizeof (int));
-    classarr[numclasses - 1] = cn;
-}
+       fprintf (stderr, "Config file looks good.\n");
 
-/*
- * field breakup for ircd.conf file.
- */
-static char * getfield (char *newline)
-{
-    static char *line = NULL;
-    char *end, *field;
-
-    if (newline)
-       line = newline;
-    if (line == NULL)
-       return (NULL);
-
-    field = line;
-    if ((end = (char *) strchr (line, ':')) == NULL) {
-       line = NULL;
-       if ((end = (char *) strchr (field, '\n')) == NULL)
-           end = field + strlen (field);
-    }
-    else
-       line = end + 1;
-    *end = '\0';
-    return (field);
+    free (filebuf);
+    return errors ? -1 : 0;
 }
 
-static int validate (aConfItem *top)
+int main (int argc, char *argv[])
 {
-    Reg aConfItem *aconf, *bconf;
-    u_int otype = 0, valid = 0;
-
-    if (!top)
-       return 0;
-
-    for (aconf = top; aconf; aconf = aconf->next) {
-       if (aconf->status & CONF_MATCH)
-           continue;
-
-       if (aconf->status & CONF_SERVER_MASK) {
-           if (aconf->status & CONF_CONNECT_SERVER)
-               otype = CONF_NOCONNECT_SERVER;
-           else if (aconf->status & CONF_NOCONNECT_SERVER)
-               otype = CONF_CONNECT_SERVER;
-
-           for (bconf = top; bconf; bconf = bconf->next) {
-               if (bconf == aconf || !(bconf->status & otype))
-                   continue;
-               if (bconf->class == aconf->class &&
-                   !mycmp (bconf->name, aconf->name) &&
-                   !mycmp (bconf->host, aconf->host)) {
-                   aconf->status |= CONF_MATCH;
-                   bconf->status |= CONF_MATCH;
-                   break;
-               }
-           }
-       }
-       else
-           for (bconf = top; bconf; bconf = bconf->next) {
-               if ((bconf == aconf) || !(bconf->status & CONF_SERVER_MASK))
-                   continue;
-               if (!mycmp (bconf->name, aconf->name)) {
-                   aconf->status |= CONF_MATCH;
-                   break;
-               }
-           }
+    if (chdir (DPATH)) {
+       perror ("chdir");
+       exit (-1);
     }
-
-    (void) fprintf (stderr, "\n");
-    for (aconf = top; aconf; aconf = aconf->next)
-       if (aconf->status & CONF_MATCH)
-           valid++;
-       else
-           (void) fprintf (stderr, "Unmatched %c:%s:%s:%s\n",
-                           confchar (aconf->status), aconf->host,
-                           aconf->passwd, aconf->name);
-    return valid ? 0 : -1;
-}
-
-static char confchar (u_int status)
-{
-    static char letrs[] = "QICNoOMKARYSLPH";
-    char *s = letrs;
-
-    status &= ~(CONF_MATCH | CONF_ILLEGAL);
-
-    for (; *s; s++, status >>= 1)
-       if (status & 1)
-           return *s;
-    return '-';
+    if (argc > 1 && !strncmp (argv[1], "-d", 2)) {
+       debugflag = 1;
+       if (argv[1][2])
+           debugflag = atoi (argv[1] + 2);
+       argc--, argv++;
+    }
+    else if (argc > 1 && !strncmp (argv[1], "-h", 2)) {
+       printf ("chkconf [-d] [conf file]\n");
+       printf ("-d = debug mode\n");
+       printf ("conf file = path to ircd.conf\n\n");
+       exit (0);
+    }
+    if (argc > 1)
+       configfile = argv[1];
+    return validate_conf (configfile);
 }
 
 int outofmemory ()
index 883a40a788ad50ef41ec757e7227f3e83b967da7..74af8f6dce6d5a9dea6363dee4dea0d54578cf26 100644 (file)
@@ -381,31 +381,6 @@ int parse (aClient *cptr, char *buffer, char *bufend, struct Message *mptr)
 #endif
 }
 
-/*
- * field breakup for ircd.conf file.
- */
-char *getfield (char *newline)
-{
-    static char *line = NULL;
-    char *end, *field;
-
-    if (newline)
-       line = newline;
-    if (line == NULL)
-       return (NULL);
-
-    field = line;
-    if ((end = (char *) strchr (line, ':')) == NULL) {
-       line = NULL;
-       if ((end = (char *) strchr (field, '\n')) == NULL)
-           end = field + strlen (field);
-    }
-    else
-       line = end + 1;
-    *end = '\0';
-    return (field);
-}
-
 static int cancel_clients (aClient *cptr, aClient *sptr, char *cmd)
 {
     /*
index 58ead6bb260bdd44d08a53a29847aa1f0d4ce8bc..77dafc146e50c960a61bc8d2330794c70453d63b 100644 (file)
@@ -795,8 +795,6 @@ int openconf ()
 {
     return open (configfile, O_RDONLY);
 }
-extern char *getfield ();
-
 static int oper_access[] = {
     ~(OFLAG_ADMIN | OFLAG_SADMIN | OFLAG_ZLINE), '*',
     OFLAG_LOCAL, 'o',
@@ -823,348 +821,512 @@ static int oper_access[] = {
 };
 
 /*
-   ** initconf() 
-   **    Read configuration file.
-   **
-   **    returns -1, if file cannot be opened
-   **             0, if file opened
+ * Block config parser.
+ *
+ * The config file uses a block-based format:
+ *   blocktype {
+ *       key value;
+ *       key "quoted value";
+ *   };
+ *
+ * Comments start with # and run to end of line.
+ */
+
+/* Read entire config file into a malloc'd buffer */
+static char *conf_read_file (int fd)
+{
+    struct stat st;
+    char *buf;
+    int n, total = 0;
+
+    if (fstat (fd, &st) < 0)
+       return NULL;
+    if (st.st_size > 1048576)
+       return NULL;
+    buf = (char *) MyMalloc (st.st_size + 1);
+    while (total < st.st_size) {
+       n = read (fd, buf + total, st.st_size - total);
+       if (n <= 0)
+           break;
+       total += n;
+    }
+    buf[total] = '\0';
+    return buf;
+}
+
+static char *conf_skip_ws (char *p)
+{
+    while (*p) {
+       if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')
+           p++;
+       else if (*p == '#') {
+           while (*p && *p != '\n')
+               p++;
+       } else
+           break;
+    }
+    return p;
+}
+
+/* Parse a token (word or quoted string). Writes into dst. */
+static char *conf_get_token (char *p, char *dst, int dstsize)
+{
+    int i = 0;
+
+    p = conf_skip_ws (p);
+    if (*p == '"') {
+       p++;
+       while (*p && *p != '"' && i < dstsize - 1) {
+           if (*p == '\\' && *(p + 1)) {
+               p++;
+               switch (*p) {
+               case 'n': dst[i++] = '\n'; break;
+               case 't': dst[i++] = '\t'; break;
+               case '\\': dst[i++] = '\\'; break;
+               case '"': dst[i++] = '"'; break;
+               default: dst[i++] = *p; break;
+               }
+               p++;
+           } else
+               dst[i++] = *p++;
+       }
+       if (*p == '"')
+           p++;
+    } else {
+       while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n'
+              && *p != ';' && *p != '{' && *p != '}' && i < dstsize - 1)
+           dst[i++] = *p++;
+    }
+    dst[i] = '\0';
+    return p;
+}
+
+static int conf_parse_oper_flags (char *flags)
+{
+    int result = 0;
+    int *i, flag;
+    char *m;
+
+    for (m = flags; *m; m++)
+       for (i = oper_access; (flag = *i); i += 2)
+           if (*m == (char) (*(i + 1))) {
+               result |= flag;
+               break;
+           }
+    return result;
+}
+
+/* Add an aConfItem to the global conf list with post-processing */
+static void conf_add_item (aConfItem *aconf, int opt)
+{
+    if (aconf->status == CONF_ZAP) {
+       char *tempc = aconf->host;
+       if (!tempc) {
+           free_conf (aconf);
+           return;
+       }
+       for (; *tempc; tempc++)
+           if (*tempc >= '0' && *tempc <= '9')
+               goto zap_safe;
+       free_conf (aconf);
+       return;
+      zap_safe:;
+    }
+    if (aconf->status & (CONF_CLIENT_MASK | CONF_LISTEN_PORT)) {
+       if (Class (aconf) == 0)
+           Class (aconf) = find_class (0);
+       if (MaxLinks (Class (aconf)) < 0)
+           Class (aconf) = find_class (0);
+    }
+    if (aconf->status & (CONF_LISTEN_PORT | CONF_CLIENT)) {
+       aConfItem *bconf;
+
+       if ((bconf = find_conf_entry (aconf, aconf->status))) {
+           delist_conf (bconf);
+           bconf->status &= ~CONF_ILLEGAL;
+           if (aconf->status == CONF_CLIENT) {
+               bconf->class->links -= bconf->clients;
+               bconf->class = aconf->class;
+               if (bconf->class)
+                   bconf->class->links += bconf->clients;
+           }
+           free_conf (aconf);
+           aconf = bconf;
+       }
+       else if (aconf->host && aconf->status == CONF_LISTEN_PORT)
+           (void) add_listener (aconf);
+    }
+    if (aconf->status & CONF_SERVER_MASK)
+       if (!aconf->host || strchr (aconf->host, '*') ||
+           strchr (aconf->host, '?') || !aconf->name) {
+           free_conf (aconf);
+           return;
+       }
+    if (aconf->status & (CONF_SERVER_MASK | CONF_LOCOP | CONF_OPERATOR))
+       if (aconf->host && !strchr (aconf->host, '@') && *aconf->host != '/') {
+           char *newhost;
+           int len = 3;
+
+           len += strlen (aconf->host);
+           newhost = (char *) MyMalloc (len);
+           (void) sprintf (newhost, "*@%s", aconf->host);
+           MyFree (aconf->host);
+           aconf->host = newhost;
+       }
+    if (aconf->status & CONF_SERVER_MASK) {
+       if (BadPtr (aconf->passwd)) {
+           free_conf (aconf);
+           return;
+       }
+       if (!(opt & BOOT_QUICK))
+           (void) lookup_confhost (aconf);
+    }
+    if (aconf->status == CONF_ME) {
+       strncpyzt (me.info, aconf->name, sizeof (me.info));
+       if (me.name[0] == '\0' && aconf->host[0])
+           strncpyzt (me.name, aconf->host, sizeof (me.name));
+       if (aconf->passwd[0] && (aconf->passwd[0] != '*'))
+           me.ip.s_addr = inet_addr (aconf->passwd);
+       else
+           me.ip.s_addr = INADDR_ANY;
+       if (portnum < 0 && aconf->port >= 0)
+           portnum = aconf->port;
+    }
+    if (aconf->status == CONF_KILL)
+       aconf->tmpconf = KLINE_PERM;
+    (void) collapse (aconf->host);
+    (void) collapse (aconf->name);
+    Debug ((DEBUG_NOTICE,
+           "Read Init: (%d) (%s) (%s) (%s) (%d) (%d)",
+           aconf->status, aconf->host, aconf->passwd,
+           aconf->name, aconf->port, Class (aconf)));
+    aconf->next = conf;
+    conf = aconf;
+}
+
+/*
+ * initconf()
+ *    Read block-based configuration file.
+ *
+ *    returns -1, if file cannot be opened
+ *             0, if file opened
  */
 #define MAXCONFLINKS 150
+#define CONF_TOKLEN 512
+#define CONF_MAX_HOSTS 256
 
 int initconf (int opt)
 {
-    static char quotes[9][2] = {
-       {'b', '\b'},
-       {'f', '\f'},
-       {'n', '\n'},
-       {'r', '\r'},
-       {'t', '\t'},
-       {'v', '\v'},
-       {'\\', '\\'},
-       {0, 0}
-    };
-    char *tmp, *s;
-    int fd, i;
-    char line[512], c[80];
-    int ccount = 0, ncount = 0;
-    aConfItem *aconf = NULL;
+    int fd;
+    char *filebuf, *p;
+    char blocktype[CONF_TOKLEN], key[CONF_TOKLEN], value[CONF_TOKLEN];
 
     is_hub = 0;
     Debug ((DEBUG_DEBUG, "initconf(): ircd.conf = %s", configfile));
-    if ((fd = openconf ()) == -1) {
+    if ((fd = openconf ()) == -1)
        return -1;
-    }
-    (void) dgets (-1, NULL, 0);          /* make sure buffer is at empty pos */
-    while ((i = dgets (fd, line, sizeof (line) - 1)) > 0) {
-       line[i] = '\0';
-       if (line[i-2] == '\r') { /* Strip DOS type end of lines */
-            line[i-2] = '\n';
-            line[i-1] = '\0';
+
+    filebuf = conf_read_file (fd);
+    (void) close (fd);
+    if (!filebuf)
+       return -1;
+
+    p = filebuf;
+    while (*(p = conf_skip_ws (p))) {
+       p = conf_get_token (p, blocktype, sizeof (blocktype));
+       p = conf_skip_ws (p);
+       if (*p != '{') {
+           Debug ((DEBUG_ERROR, "Expected '{' after block type '%s'", blocktype));
+           break;
        }
-       if ((tmp = (char *) strchr (line, '\n')))
-           *tmp = 0;
-       else
-           while (dgets (fd, c, sizeof (c) - 1) > 0)
-               if ((tmp = (char *) strchr (c, '\n'))) {
-                   *tmp = 0;
-                   break;
-               }
-       /*
-        * Do quoting of characters and # detection.
-        */
-       for (tmp = line; *tmp; tmp++) {
-           if (*tmp == '\\') {
-               for (i = 0; quotes[i][0]; i++)
-                   if (quotes[i][0] == *(tmp + 1)) {
-                       *tmp = quotes[i][1];
-                       break;
-                   }
-               if (!quotes[i][0])
-                   *tmp = *(tmp + 1);
-               if (!*(tmp + 1))
-                   break;
+       p++;
+
+       /* Per-block field storage */
+       char b_name[CONF_TOKLEN] = "";
+       char b_host[CONF_TOKLEN] = "";
+       char b_passwd[CONF_TOKLEN] = "";
+       char b_info[CONF_TOKLEN] = "";
+       char b_flags[CONF_TOKLEN] = "";
+       char b_hub[CONF_TOKLEN] = "";
+       char b_die[CONF_TOKLEN] = "";
+       char b_restart[CONF_TOKLEN] = "";
+       char b_admin[3][CONF_TOKLEN];
+       char b_hosts[CONF_MAX_HOSTS][CONF_TOKLEN];
+       int b_port = 0, b_class = 0;
+       int b_admin_count = 0, b_host_count = 0;
+       int b_hub_mode = 0;
+       int b_pingfreq = 0, b_connfreq = 0, b_maxlinks = 0, b_sendq = 0;
+       int b_zline_time = 0;
+
+       memset (b_admin, 0, sizeof (b_admin));
+
+       /* Parse key-value pairs */
+       while (*(p = conf_skip_ws (p)) && *p != '}') {
+           p = conf_get_token (p, key, sizeof (key));
+           if (!*key)
+               break;
+           p = conf_get_token (p, value, sizeof (value));
+           p = conf_skip_ws (p);
+           if (*p == ';')
+               p++;
+
+           if (!mycmp (key, "name"))
+               strncpyzt (b_name, value, sizeof (b_name));
+           else if (!mycmp (key, "host")) {
+               /* In allow{} blocks, "host" is the hostname mask (-> b_name),
+                * while "ip" is the IP mask (-> b_host). In kline{} blocks,
+                * "host" can be repeated for multi-host bans. */
+               if (!mycmp (blocktype, "allow"))
+                   strncpyzt (b_name, value, sizeof (b_name));
                else
-                   for (s = tmp; (*s = *(s + 1)); s++);
+                   strncpyzt (b_host, value, sizeof (b_host));
+               if (b_host_count < CONF_MAX_HOSTS)
+                   strncpyzt (b_hosts[b_host_count++], value, CONF_TOKLEN);
            }
-           else if (*tmp == '#')
-               *tmp = '\0';
-       }
-       if (!*line || line[0] == '#' || line[0] == '\n' ||
-           line[0] == ' ' || line[0] == '\t')
-           continue;
-       /* Could we test if it's conf line at all?  -Vesa */
-       if (line[1] != ':') {
-           Debug ((DEBUG_ERROR, "Bad config line: %s", line));
-           continue;
+           else if (!mycmp (key, "password") || !mycmp (key, "passwd"))
+               strncpyzt (b_passwd, value, sizeof (b_passwd));
+           else if (!mycmp (key, "address"))
+               strncpyzt (b_host, value, sizeof (b_host));
+           else if (!mycmp (key, "port"))
+               b_port = atoi (value);
+           else if (!mycmp (key, "class"))
+               b_class = atoi (value);
+           else if (!mycmp (key, "flags"))
+               strncpyzt (b_flags, value, sizeof (b_flags));
+           else if (!mycmp (key, "info"))
+               strncpyzt (b_info, value, sizeof (b_info));
+           else if (!mycmp (key, "line")) {
+               if (b_admin_count < 3)
+                   strncpyzt (b_admin[b_admin_count++], value, CONF_TOKLEN);
+           }
+           else if (!mycmp (key, "ip"))
+               strncpyzt (b_host, value, sizeof (b_host));
+           else if (!mycmp (key, "hub")) {
+               if (!mycmp (blocktype, "general"))
+                   b_hub_mode = (!mycmp (value, "yes") || !mycmp (value, "1"));
+               else
+                   strncpyzt (b_hub, value, sizeof (b_hub));
+           }
+           else if (!mycmp (key, "ident"))
+               strncpyzt (b_name, value, sizeof (b_name));
+           else if (!mycmp (key, "reason"))
+               strncpyzt (b_passwd, value, sizeof (b_passwd));
+           else if (!mycmp (key, "nick"))
+               strncpyzt (b_name, value, sizeof (b_name));
+           else if (!mycmp (key, "die"))
+               strncpyzt (b_die, value, sizeof (b_die));
+           else if (!mycmp (key, "restart"))
+               strncpyzt (b_restart, value, sizeof (b_restart));
+           else if (!mycmp (key, "pingfreq"))
+               b_pingfreq = atoi (value);
+           else if (!mycmp (key, "connfreq"))
+               b_connfreq = atoi (value);
+           else if (!mycmp (key, "maxlinks"))
+               b_maxlinks = atoi (value);
+           else if (!mycmp (key, "sendq"))
+               b_sendq = atoi (value);
+           else if (!mycmp (key, "time"))
+               b_zline_time = atoi (value);
+           else
+               Debug ((DEBUG_ERROR, "Unknown key '%s' in block '%s'",
+                       key, blocktype));
        }
-       if (aconf)
-           free_conf (aconf);
-       aconf = make_conf ();
 
-       tmp = getfield (line);
-       if (!tmp)
-           continue;
-       switch (*tmp) {
-       case 'A':                 /* Name, e-mail address of administrator */
+       if (*p == '}')
+           p++;
+       p = conf_skip_ws (p);
+       if (*p == ';')
+           p++;
+
+       /* Create aConfItem(s) from block */
+
+       if (!mycmp (blocktype, "me")) {
+           /* M:servername:bindaddr:description:port */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_ME;
+           DupString (aconf->host, b_name);
+           DupString (aconf->passwd, b_host[0] ? b_host : "*");
+           DupString (aconf->name, b_info[0] ? b_info : b_name);
+           aconf->port = b_port;
+           conf_add_item (aconf, opt);
+       }
+       else if (!mycmp (blocktype, "admin")) {
+           /* A:line1:line2:line3 */
+           aConfItem *aconf = make_conf ();
            aconf->status = CONF_ADMIN;
-           break;
-       case 'a':                 /* of this server. */
-           aconf->status = CONF_SADMIN;
-           break;
-       case 'C':                 /* Server where I should try to connect */
-       case 'c':                 /* in case of lp failures             */
-           ccount++;
-           aconf->status = CONF_CONNECT_SERVER;
-           break;
-    case 'e':
-       case 'E':                 /* Blocking of DCC transfers -GZ */
-           aconf->status = CONF_DCCBLOCK;
-           break;
-       case 'f':
-       case 'F':
-           aconf->status = CONF_ZTIME;
-           break;
-       case 'G':
-       case 'g':
-           /* General config options */
-           aconf->status = CONF_CONFIG;
-           break;
-       case 'H':                 /* Hub server line */
-       case 'h':
-           aconf->status = CONF_HUB;
-           break;
-       case 'I':                 /* Just plain normal irc client trying  */
-       case 'i':                 /* to connect me */
+           DupString (aconf->host, b_admin_count > 0 ? b_admin[0] : "");
+           DupString (aconf->passwd, b_admin_count > 1 ? b_admin[1] : "");
+           DupString (aconf->name, b_admin_count > 2 ? b_admin[2] : "");
+           conf_add_item (aconf, opt);
+       }
+       else if (!mycmp (blocktype, "class")) {
+           /* Y:class:pingfreq:connfreq:maxlinks:sendq */
+           int classnum = b_class ? b_class : atoi (b_name);
+           add_class (classnum, b_pingfreq, b_connfreq, b_maxlinks, b_sendq);
+       }
+       else if (!mycmp (blocktype, "allow")) {
+           /* I:ipmask:password:hostmask:port:class
+            * ip= -> aconf->host, host= -> aconf->name */
+           aConfItem *aconf = make_conf ();
            aconf->status = CONF_CLIENT;
-           break;
-       case 'K':                 /* Kill user line on irc.conf           */
-       case 'k':
-           aconf->status = CONF_KILL;
-           break;
-           /* Me. Host field is name used for this host */
-           /* and port number is the number of the port */
-       case 'M':
-       case 'm':
-           aconf->status = CONF_ME;
-           break;
-       case 'N':                 /* Server where I should NOT try to     */
-       case 'n':                 /* connect in case of lp failures     */
-           /* but which tries to connect ME        */
-           ++ncount;
-           aconf->status = CONF_NOCONNECT_SERVER;
-           break;
-       case 'O':
-       case 'o':
-           aconf->status = CONF_OPERATOR;
-           break;
-       case 'P':                 /* listen port line */
-       case 'p':
+           DupString (aconf->host, b_host[0] ? b_host : b_name);
+           DupString (aconf->passwd, b_passwd);
+           DupString (aconf->name, b_name[0] ? b_name : b_host);
+           aconf->port = b_port;
+           if (b_class)
+               Class (aconf) = find_class (b_class);
+           conf_add_item (aconf, opt);
+       }
+       else if (!mycmp (blocktype, "listen")) {
+           /* P:address:*:*:port */
+           aConfItem *aconf = make_conf ();
            aconf->status = CONF_LISTEN_PORT;
-           break;
-       case 'Q':                 /* reserved nicks */
-           aconf->status = CONF_QUARANTINED_NICK;
-           break;
-       case 'q':                 /* a server that you don't want in your */
-                                 /* network. USE WITH CAUTION! */
-           aconf->status = CONF_QUARANTINED_SERVER;
-           break;
-       case 'S':                 /* Service. Same semantics as   */
-       case 's':                 /* CONF_OPERATOR                */
-           aconf->status = CONF_SERVICE;
-           break;
-       case 'U':                 /* Underworld server, allowed to hack modes */
-       case 'u':                 /* *Every* server on the net must define the same !!! */
-           aconf->status = CONF_UWORLD;
-           break;
-       case 'Y':
-       case 'y':
-           aconf->status = CONF_CLASS;
-           break;
-       case 'Z':
-       case 'z':
-           aconf->status = CONF_ZAP;
-           break;
-       case 'X':
-       case 'x':
+           DupString (aconf->host, b_host[0] ? b_host : "");
+           DupString (aconf->passwd, "*");
+           DupString (aconf->name, "*");
+           aconf->port = b_port;
+           conf_add_item (aconf, opt);
+       }
+       else if (!mycmp (blocktype, "oper")) {
+           /* O:host:passwd:nick:flags:class */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_OPERATOR;
+           DupString (aconf->host, b_host);
+           DupString (aconf->passwd, b_passwd);
+           DupString (aconf->name, b_name);
+           aconf->port = conf_parse_oper_flags (b_flags[0] ? b_flags : "*");
+           if (!(aconf->port & OFLAG_ISGLOBAL))
+               aconf->status = CONF_LOCOP;
+           if (b_class)
+               Class (aconf) = find_class (b_class);
+           conf_add_item (aconf, opt);
+       }
+       else if (!mycmp (blocktype, "drpass")) {
+           /* X:diepass:restartpass */
+           aConfItem *aconf = make_conf ();
            aconf->status = CONF_DRPASS;
-           break;
-       default:
-           Debug ((DEBUG_ERROR, "Error in config file: %s", line));
-           break;
+           DupString (aconf->host, b_die);
+           DupString (aconf->passwd, b_restart);
+           DupString (aconf->name, "");
+           conf_add_item (aconf, opt);
        }
-       if (IsIllegal (aconf))
-           continue;
-
-       for (;;) {                /* Parse remaining fields; break on missing field */
-           /* X: line format is X:restartpass:diepass
-            */
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           DupString (aconf->host, tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           DupString (aconf->passwd, tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           DupString (aconf->name, tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           if (aconf->status & CONF_OPS) {
-               int *i, flag;
-               char *m = "*";
-               /*
-                * Now we use access flags to define
-                * what an operator can do with their O.
-                */
-               for (m = (*tmp) ? tmp : m; *m; m++) {
-                   for (i = oper_access; (flag = *i); i += 2)
-                       if (*m == (char) (*(i + 1))) {
-                           aconf->port |= flag;
-                           break;
-                       }
-               }
-               if (!(aconf->port & OFLAG_ISGLOBAL))
-                   aconf->status = CONF_LOCOP;
+       else if (!mycmp (blocktype, "connect")) {
+           /* Creates C-line + N-line, optionally H-line */
+           aConfItem *cconf = make_conf ();
+           aConfItem *nconf = make_conf ();
+           cconf->status = CONF_CONNECT_SERVER;
+           nconf->status = CONF_NOCONNECT_SERVER;
+           DupString (cconf->host, b_host);
+           DupString (cconf->passwd, b_passwd);
+           DupString (cconf->name, b_name);
+           cconf->port = b_port;
+           if (b_class)
+               Class (cconf) = find_class (b_class);
+           DupString (nconf->host, b_host);
+           DupString (nconf->passwd, b_passwd);
+           DupString (nconf->name, b_name);
+           nconf->port = 0;
+           if (b_class)
+               Class (nconf) = find_class (b_class);
+           conf_add_item (cconf, opt);
+           conf_add_item (nconf, opt);
+           if (b_hub[0]) {
+               aConfItem *hconf = make_conf ();
+               hconf->status = CONF_HUB;
+               DupString (hconf->host, b_hub);
+               DupString (hconf->passwd, "");
+               DupString (hconf->name, b_name);
+               conf_add_item (hconf, opt);
            }
-           else
-               aconf->port = atoi (tmp);
-           if ((tmp = getfield (NULL)) == NULL)
-               break;
-           Class (aconf) = find_class (atoi (tmp));
-           break;
        }
-       /*
-          ** If conf line is a general config, just
-          ** see if we recognize the keyword, and set
-          ** the appropriate global.  We don't use a "standard"
-          ** config link here, because these are things which need
-          ** to be tested SO often that a simple global test
-          ** is much better!  -Aeto
-        */
-       if ((aconf->status & CONF_CONFIG) == CONF_CONFIG) {
-           if (aconf->host && !mycmp (aconf->host, "hub"))
-               is_hub = 1;
-           continue;
+       else if (!mycmp (blocktype, "uworld")) {
+           /* U:name:*:* */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_UWORLD;
+           DupString (aconf->host, b_name);
+           DupString (aconf->passwd, "*");
+           DupString (aconf->name, "*");
+           conf_add_item (aconf, opt);
        }
-       /* Check if Z:line time    -taz */
-       if (aconf->status & CONF_ZTIME) {
-           if ((aconf->host) && isdigit (*aconf->host))
-               socks_zline_time = (atoi (aconf->host) * 60);
-           continue;
+       else if (!mycmp (blocktype, "general")) {
+           if (b_hub_mode)
+               is_hub = 1;
+           if (b_zline_time)
+               socks_zline_time = b_zline_time * 60;
        }
-       /* Check for bad Z-lines masks as they are *very* dangerous
-          if not correct!!! */
-       if (aconf->status == CONF_ZAP) {
-           char *tempc = aconf->host;
-           if (!tempc) {
-               free_conf (aconf);
-               aconf = NULL;
-               continue;
-           }
-           for (; *tempc; tempc++)
-               if ((*tempc >= '0') && (*tempc <= '9'))
-                   goto zap_safe;
-           free_conf (aconf);
-           aconf = NULL;
-           continue;
-         zap_safe:;
+       else if (!mycmp (blocktype, "quarantine")) {
+           /* Q::reason:nick */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_QUARANTINED_NICK;
+           DupString (aconf->host, "");
+           DupString (aconf->passwd, b_passwd);
+           DupString (aconf->name, b_name);
+           conf_add_item (aconf, opt);
        }
-       /*
-          ** If conf line is a class definition, create a class entry
-          ** for it and make the conf_line illegal and delete it.
-        */
-       if (aconf->status & CONF_CLASS) {
-           add_class (atoi (aconf->host), atoi (aconf->passwd),
-                      atoi (aconf->name), aconf->port, tmp ? atoi (tmp) : 0);
-           continue;
+       else if (!mycmp (blocktype, "squar")) {
+           /* q::reason:servername */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_QUARANTINED_SERVER;
+           DupString (aconf->host, "");
+           DupString (aconf->passwd, b_passwd);
+           DupString (aconf->name, b_name);
+           conf_add_item (aconf, opt);
        }
-       /*
-          ** associate each conf line with a class by using a pointer
-          ** to the correct class record. -avalon
-        */
-       if (aconf->status & (CONF_CLIENT_MASK | CONF_LISTEN_PORT)) {
-           if (Class (aconf) == 0)
-               Class (aconf) = find_class (0);
-           if (MaxLinks (Class (aconf)) < 0)
-               Class (aconf) = find_class (0);
-       }
-       if (aconf->status & (CONF_LISTEN_PORT | CONF_CLIENT)) {
-           aConfItem *bconf;
-
-           if ((bconf = find_conf_entry (aconf, aconf->status))) {
-               delist_conf (bconf);
-               bconf->status &= ~CONF_ILLEGAL;
-               if (aconf->status == CONF_CLIENT) {
-                   bconf->class->links -= bconf->clients;
-                   bconf->class = aconf->class;
-                   if (bconf->class)
-                       bconf->class->links += bconf->clients;
+       else if (!mycmp (blocktype, "kline")) {
+           /* K:host:reason:ident - one aConfItem per host */
+           int i;
+           if (b_host_count == 0) {
+               aConfItem *aconf = make_conf ();
+               aconf->status = CONF_KILL;
+               DupString (aconf->host, b_host);
+               DupString (aconf->passwd, b_passwd);
+               DupString (aconf->name, b_name[0] ? b_name : "*");
+               conf_add_item (aconf, opt);
+           } else {
+               for (i = 0; i < b_host_count; i++) {
+                   aConfItem *aconf = make_conf ();
+                   aconf->status = CONF_KILL;
+                   DupString (aconf->host, b_hosts[i]);
+                   DupString (aconf->passwd, b_passwd);
+                   DupString (aconf->name, b_name[0] ? b_name : "*");
+                   conf_add_item (aconf, opt);
                }
-               free_conf (aconf);
-               aconf = bconf;
            }
-           else if (aconf->host && aconf->status == CONF_LISTEN_PORT)
-               (void) add_listener (aconf);
        }
-       if (aconf->status & CONF_SERVER_MASK)
-           if (ncount > MAXCONFLINKS || ccount > MAXCONFLINKS ||
-               !aconf->host || strchr (aconf->host, '*') ||
-               strchr (aconf->host, '?') || !aconf->name)
-               continue;
-
-       if (aconf->status & (CONF_SERVER_MASK | CONF_LOCOP | CONF_OPERATOR))
-           if (!strchr (aconf->host, '@') && *aconf->host != '/') {
-               char *newhost;
-               int len = 3;    /* *@\0 = 3 */
-
-               len += strlen (aconf->host);
-               newhost = (char *) MyMalloc (len);
-               (void) sprintf (newhost, "*@%s", aconf->host);
-               MyFree (aconf->host);
-               aconf->host = newhost;
-           }
-       if (aconf->status & CONF_SERVER_MASK) {
-           if (BadPtr (aconf->passwd))
-               continue;
-           else if (!(opt & BOOT_QUICK))
-               (void) lookup_confhost (aconf);
+       else if (!mycmp (blocktype, "zline")) {
+           /* Z:address:reason:* */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_ZAP;
+           DupString (aconf->host, b_host);
+           DupString (aconf->passwd, b_passwd);
+           DupString (aconf->name, "*");
+           conf_add_item (aconf, opt);
        }
-       /*
-          ** Own port and name cannot be changed after the startup.
-          ** (or could be allowed, but only if all links are closed
-          ** first).
-          ** Configuration info does not override the name and port
-          ** if previously defined. Note, that "info"-field can be
-          ** changed by "/rehash".
-        */
-       if (aconf->status == CONF_ME) {
-           strncpyzt (me.info, aconf->name, sizeof (me.info));
-           if (me.name[0] == '\0' && aconf->host[0])
-               strncpyzt (me.name, aconf->host, sizeof (me.name));
-
-           if (aconf->passwd[0] && (aconf->passwd[0] != '*'))
-               me.ip.s_addr = inet_addr (aconf->passwd);
-           else
-               me.ip.s_addr = INADDR_ANY;
-           if (portnum < 0 && aconf->port >= 0)
-               portnum = aconf->port;
-       }
-       if (aconf->status == CONF_KILL)
-           aconf->tmpconf = KLINE_PERM;
-       (void) collapse (aconf->host);
-       (void) collapse (aconf->name);
-       Debug ((DEBUG_NOTICE,
-               "Read Init: (%d) (%s) (%s) (%s) (%d) (%d)",
-               aconf->status, aconf->host, aconf->passwd,
-               aconf->name, aconf->port, Class (aconf)));
-       aconf->next = conf;
-       conf = aconf;
-       aconf = NULL;
+       else if (!mycmp (blocktype, "service")) {
+           /* S:hostmask:*:name */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_SERVICE;
+           DupString (aconf->host, b_host);
+           DupString (aconf->passwd, "*");
+           DupString (aconf->name, b_name);
+           if (b_class)
+               Class (aconf) = find_class (b_class);
+           conf_add_item (aconf, opt);
+       }
+       else if (!mycmp (blocktype, "dccblock")) {
+           /* E:filename:*:* */
+           aConfItem *aconf = make_conf ();
+           aconf->status = CONF_DCCBLOCK;
+           DupString (aconf->host, b_host[0] ? b_host : b_name);
+           DupString (aconf->passwd, "*");
+           DupString (aconf->name, "*");
+           conf_add_item (aconf, opt);
+       }
+       else
+           Debug ((DEBUG_ERROR, "Unknown block type '%s'", blocktype));
     }
-    if (aconf)
-       free_conf (aconf);
-    (void) dgets (-1, NULL, 0);          /* make sure buffer is at empty pos */
-    (void) close (fd);
+
+    MyFree (filebuf);
     check_class ();
     nextping = nextconnect = time (NULL);
     return 0;
diff --git a/tools/convert-conf.py b/tools/convert-conf.py
new file mode 100755 (executable)
index 0000000..180fe6b
--- /dev/null
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+"""
+Convert legacy colon-delimited ircd.conf to block-based format.
+
+Usage: python3 convert-conf.py < old-ircd.conf > new-ircd.conf
+
+This is a one-time migration tool. The new block format is the only
+format supported by the ircd going forward.
+"""
+
+import sys
+import re
+
+
+def parse_old_line(line):
+    """Parse a legacy colon-delimited config line into type and fields."""
+    line = line.rstrip('\n\r')
+    if not line or line.startswith('#') or line[0] in (' ', '\t'):
+        return None, None, line
+    if len(line) < 2 or line[1] != ':':
+        return None, None, line
+    linetype = line[0]
+    fields = line[2:].split(':')
+    return linetype, fields, line
+
+
+def quote_if_needed(s):
+    """Quote a string if it contains spaces or special characters."""
+    if not s:
+        return '""'
+    if ' ' in s or '\t' in s or '"' in s or ';' in s or '{' in s or '}' in s:
+        return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
+    return s
+
+
+def convert_line(linetype, fields):
+    """Convert a parsed legacy line to block format string."""
+    # Pad fields to at least 5 elements
+    while len(fields) < 5:
+        fields.append('')
+
+    host = fields[0]
+    passwd = fields[1]
+    name = fields[2]
+    port_or_flags = fields[3]
+    classnum = fields[4]
+
+    if linetype == 'M':
+        lines = ['me {']
+        lines.append(f'    name {quote_if_needed(host)};')
+        if passwd and passwd != '*':
+            lines.append(f'    address {quote_if_needed(passwd)};')
+        else:
+            lines.append('    address *;')
+        lines.append(f'    info {quote_if_needed(name)};')
+        if port_or_flags:
+            lines.append(f'    port {port_or_flags};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'A':
+        # A:line1:line2:line3 (free-form admin info)
+        lines = ['admin {']
+        if host:
+            lines.append(f'    line {quote_if_needed(host)};')
+        if passwd:
+            lines.append(f'    line {quote_if_needed(passwd)};')
+        if name:
+            lines.append(f'    line {quote_if_needed(name)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'Y':
+        # Y:class:pingfreq:connfreq:maxlinks:sendq
+        lines = ['class {']
+        lines.append(f'    name {host};')
+        if passwd:
+            lines.append(f'    pingfreq {passwd};')
+        if name:
+            lines.append(f'    connfreq {name};')
+        if port_or_flags:
+            lines.append(f'    maxlinks {port_or_flags};')
+        if classnum:
+            lines.append(f'    sendq {classnum};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'I':
+        # I:ipmask:password:hostmask:unused:class
+        lines = ['allow {']
+        lines.append(f'    ip {quote_if_needed(host)};')
+        if name:
+            lines.append(f'    host {quote_if_needed(name)};')
+        if passwd:
+            lines.append(f'    password {quote_if_needed(passwd)};')
+        if classnum:
+            lines.append(f'    class {classnum};')
+        if port_or_flags and port_or_flags != '0':
+            lines.append(f'    port {port_or_flags};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'P':
+        # P:address:*:*:port
+        lines = ['listen {']
+        if host:
+            lines.append(f'    address {quote_if_needed(host)};')
+        if port_or_flags:
+            lines.append(f'    port {port_or_flags};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'O' or linetype == 'o':
+        # O:host:passwd:nick:flags:class
+        lines = ['oper {']
+        lines.append(f'    host {quote_if_needed(host)};')
+        lines.append(f'    password {quote_if_needed(passwd)};')
+        lines.append(f'    name {quote_if_needed(name)};')
+        if port_or_flags:
+            lines.append(f'    flags {port_or_flags};')
+        if classnum:
+            lines.append(f'    class {classnum};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'X':
+        # X:diepass:restartpass
+        lines = ['drpass {']
+        lines.append(f'    die {quote_if_needed(host)};')
+        lines.append(f'    restart {quote_if_needed(passwd)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'U':
+        # U:servername:*:*
+        lines = ['uworld {']
+        lines.append(f'    name {quote_if_needed(host)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'Q':
+        # Q::reason:nick
+        lines = ['quarantine {']
+        lines.append(f'    nick {quote_if_needed(name)};')
+        if passwd:
+            lines.append(f'    reason {quote_if_needed(passwd)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'q':
+        # q::reason:server
+        lines = ['squar {']
+        lines.append(f'    name {quote_if_needed(name)};')
+        if passwd:
+            lines.append(f'    reason {quote_if_needed(passwd)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'K' or linetype == 'k':
+        # K:host:reason:ident
+        lines = ['kline {']
+        lines.append(f'    host {quote_if_needed(host)};')
+        if name:
+            lines.append(f'    ident {quote_if_needed(name)};')
+        if passwd:
+            lines.append(f'    reason {quote_if_needed(passwd)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'Z' or linetype == 'z':
+        # Z:address:reason:*
+        lines = ['zline {']
+        lines.append(f'    address {quote_if_needed(host)};')
+        if passwd:
+            lines.append(f'    reason {quote_if_needed(passwd)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'G' or linetype == 'g':
+        # G:option
+        if host and host.lower() == 'hub':
+            return 'general {\n    hub yes;\n};'
+        return f'# Unrecognized G-line option: {host}'
+
+    elif linetype == 'F' or linetype == 'f':
+        # F:minutes
+        if host:
+            return f'general {{\n    time {host};\n}};'
+        return ''
+
+    elif linetype == 'S' or linetype == 's':
+        # S:hostmask:*:name
+        lines = ['service {']
+        lines.append(f'    host {quote_if_needed(host)};')
+        lines.append(f'    name {quote_if_needed(name)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'E' or linetype == 'e':
+        # E:filename:*:*
+        lines = ['dccblock {']
+        lines.append(f'    name {quote_if_needed(host)};')
+        lines.append('};')
+        return '\n'.join(lines)
+
+    elif linetype == 'a':
+        # a: services admin - same as A but CONF_SADMIN
+        # Rarely used, just note it
+        return f'# Services admin (a-line): {host}:{passwd}:{name}'
+
+    else:
+        return None
+
+
+def main():
+    """Convert old ircd.conf from stdin to new format on stdout."""
+    # Collect C/N/H lines to merge into connect{} blocks
+    c_lines = {}  # keyed by server name
+    n_lines = {}
+    h_lines = {}  # keyed by server name
+    other_output = []
+    comments = []
+
+    input_lines = sys.stdin.readlines()
+
+    # First pass: collect C, N, H lines; convert everything else
+    for raw_line in input_lines:
+        linetype, fields, original = parse_old_line(raw_line)
+
+        if linetype is None:
+            # Comment or blank line
+            if original.startswith('#'):
+                comments.append(original)
+            else:
+                comments.append(original)
+            continue
+
+        # Flush accumulated comments before this block
+        if comments:
+            for c in comments:
+                other_output.append(c)
+            comments = []
+
+        while len(fields) < 5:
+            fields.append('')
+
+        if linetype in ('C', 'c'):
+            # C:host:passwd:servername:port:class
+            sname = fields[2]
+            c_lines[sname] = fields
+        elif linetype in ('N', 'n'):
+            # N:host:passwd:servername:unused:class
+            sname = fields[2]
+            n_lines[sname] = fields
+        elif linetype in ('H', 'h'):
+            # H:hubmask:unused:servername
+            sname = fields[2]
+            h_lines[sname] = fields
+        else:
+            result = convert_line(linetype, fields)
+            if result is not None:
+                other_output.append(result)
+            else:
+                other_output.append(f'# Unconverted: {original}')
+
+    # Flush trailing comments
+    if comments:
+        for c in comments:
+            other_output.append(c)
+
+    # Second pass: merge C/N/H into connect{} blocks
+    connect_blocks = []
+    all_servers = set(list(c_lines.keys()) + list(n_lines.keys()))
+    for sname in sorted(all_servers):
+        c = c_lines.get(sname)
+        n = n_lines.get(sname)
+        h = h_lines.get(sname)
+
+        lines = ['connect {']
+        # Prefer C-line fields, fall back to N-line
+        src = c if c else n
+        if src:
+            lines.append(f'    host {quote_if_needed(src[0])};')
+            lines.append(f'    password {quote_if_needed(src[1])};')
+            lines.append(f'    name {quote_if_needed(src[2])};')
+        if c and c[3] and c[3] != '0':
+            lines.append(f'    port {c[3]};')
+        else:
+            lines.append('    port 0;')
+        # Class from C-line or N-line
+        classnum = ''
+        if c and c[4]:
+            classnum = c[4]
+        elif n and n[4]:
+            classnum = n[4]
+        if classnum:
+            lines.append(f'    class {classnum};')
+        if h:
+            lines.append(f'    hub {quote_if_needed(h[0])};')
+        lines.append('};')
+        connect_blocks.append('\n'.join(lines))
+
+    # Output everything
+    print('# Converted from legacy colon-delimited format')
+    print('#')
+    print()
+    for item in other_output:
+        print(item)
+    if connect_blocks:
+        print()
+        print('# Server connections (merged from C/N/H lines)')
+        for block in connect_blocks:
+            print()
+            print(block)
+
+
+if __name__ == '__main__':
+    main()