config.c 15.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/*
 * This file is part of blabouncer (https://www.blatech.co.uk/l_bratch/blabouncer).
 * Copyright (C) 2019 Luke Bratch <luke@bratch.co.uk>.
 *
 * Blabouncer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.
 *
 * Blabouncer is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with blabouncer. If not, see <http://www.gnu.org/licenses/>.
*/

18 19
#include "config.h"

20 21 22 23 24 25
// Sets 'dest' to the value of the configuration option with name
// 'confname' from configuration file 'filename'.
// Returns 1 for success or 0 for error/failure.
int getconfstr(char *confname, char *filename, char* dest) {
  FILE *fp;
  char str[MAXCHAR];
26
  int found = 0; // Have we found the configuration option?
27

28
  // Set string to zero-length to begin
29
  dest[0] = '\0';
30

31 32
  // Length of requested configuration option name
  long int namelen = strlen(confname);
33

34 35 36 37
  fp = fopen(filename, "r");

  if (fp == NULL) {
    printf("error: could not open configuration file '%s'.\n", filename);
38
    debugprint(DEBUG_CRIT, "error: could not open configuration file '%s'.\n", filename);
39
    exit(1);
40
  }
41 42 43
  // Loop through the whole file, looking for the requested configuration option
  while (fgets(str, MAXCHAR, fp) != NULL) {
    char substr[MAXCHAR];
44

45 46
    // Check if the next character after the length of the requested option
    // name is an equals sign, a space, or a tab
47
    if (str[namelen] != '=' && str[namelen] != ' ' && str[namelen] != '\t') {
48 49 50
      // If it isn't this can't have been our option
      continue;
    }
51

52 53 54 55
    // Copy the number of characters that the requested option name is long
    // to a temporary string
    strncpy(substr, str, namelen);
    substr[namelen] = '\0';
56

57 58
    // If the resulting temporary string contains the requested option name,
    // we have found our configuration option and it is in the current 'str'
59
    if (strstr(substr, confname)) {
60
      found = 1;
61 62
      break;
    }
63 64
  }

65 66
  // If we got here, then either we found the configuration option or we ran out of file

67
  if (!found) {
68
    debugprint(DEBUG_SOME, "Error reading configuration option '%s', did not find it in configuration file '%s'.\n", confname, filename);
69
    fclose(fp);
70 71
    return 0;
  }
72

73
  long int pos = 0;
74
  char conf[MAXCHAR]; // Temporary string to build configuration value in
75

76 77 78
  // Starting from the end of the option name, find the position of the start of the configuration value
  // (including its double quotes) by skipping over everything that isn't an equals sign, a space, or a tab
  for (size_t i = namelen; i < strlen(str); i++) {
79
    if (str[i] == '=' || str[i] == ' ' || str[i] == '\t') {
80 81
      continue;
    } else {
82
      // Record current/final position in string
83 84 85 86 87
      pos = i;
      break;
    }
  }

88 89
  strncpy(conf, str + pos, strlen(str) - pos - 1); // Copy remainder to new string and lop off the newline
  conf[strlen(str) - pos - 1] = '\0'; // Null terminate
90 91 92

   // Check for start and end quotes
  if (conf[0] != '"' || conf[strlen(conf) - 1] != '"') {
93
    printf("Error reading configuration option '%s', not enclosed in double quotes in configuration file '%s'!\n", confname, filename);
94
    debugprint(DEBUG_CRIT, "Error reading configuration option '%s', not enclosed in double quotes in configuration file '%s'!\n", confname, filename);
95 96 97
    exit(1);
  }

98 99
  strncpy(dest, conf + 1, strlen(conf) - 2); // Copy result to destination string without quotes
  dest[strlen(conf) - 2] = '\0'; // Null terminate
100

101
  debugprint(DEBUG_FULL, "getconfstr(): returning '%s'.\n", dest);
102

103
  // Close fine and return success
104 105 106
  fclose(fp);
  return 1;
}
107

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
// Populates 'dest' with the values of the configuration array option
// with name 'confname' from configuration file 'filename'.
// Returns 1 on success, 0 on reading no values, or -1 on an error.
// On error, dest[0] is set to the error string for later retrieval.
int getconfarr(char *confname, char *filename, char dest[MAXCONFARR][MAXDATASIZE]) {
  debugprint(DEBUG_FULL, "getconfarr(): '%s', '%s' and a dest array.\n", confname, filename);

  FILE *fp;
  char line[MAXCHAR];
  int found = 0; // Have we found the configuration option?
  int valuecount = 0; // Which element in the configuration array we are on

  // Set strings to zero-length to begin
  for (int i = 0; i < MAXCONFARR; i++) {
    dest[i][0] = '\0';
  }

  // Length of requested configuration array name
  long int namelen = strlen(confname);

  fp = fopen(filename, "r");

  if (fp == NULL) {
    debugprint(DEBUG_CRIT, "error: could not open configuration file '%s'.\n", filename);
    exit(1);
  }
  // Loop through the whole file, looking for the requested configuration array
  while (fgets(line, MAXCHAR, fp) != NULL) {
    // Don't bother with any of this if this line is a comment
    int comment = 0;
    for (size_t i = 0; i < strlen(line); i++) {
      // Ignore spaces/tabs
      if (line[i] == ' ' || line[i] == '\t') {
        continue;
      } else if (line[i] == '#') {
        // If it's a comment, ignore the line
        comment = 1;
        break;
      } else {
        // Found something else before a comment, so carry on
        break;
      }
    }

    if (comment) {
      continue;
    }

    // If we haven't found our array yet, try to find the opening line
    if (!found) {
      char substr[MAXCHAR];

      // Check if the next character after the length of the requested array
      // name is an equals sign, a space, or a tab
      if (line[namelen] != '=' && line[namelen] != ' ' && line[namelen] != '\t') {
        // If it isn't this can't have been our array
        continue;
      }

      // Copy the number of characters that the requested array name is long
      // to a temporary string
      strncpy(substr, line, namelen);
      substr[namelen] = '\0';

      // If the resulting temporary string contains the requested array name,
      // we have found our configuration array
      if (strstr(substr, confname)) {
        // Make sure it is a valid start of array line
        for (size_t i = namelen; i < strlen(line); i++) {
          if (line[i] == ' ' || line[i] == '\t' || line[i] == '=') {
            // Ignore spaces, tabs and equals signs
            continue;
          } else if (line[i] == '{') {
            // Success, found an opening brace
            // Ignore anything else on this line
            found = 1;
            break;
          } else {
            // Unexpected character found, return failure
            snprintf(dest[0], MAXDATASIZE, "Unexpected character '%c' found on configuration array opening line for '%s'.\n", line[i], confname);
            fclose(fp);
            return -1;
          }
        }
      }
    // If we have found our array, extract the value from each line in it
    } else {
      int valuelen = 0;
      int inquotes = 0;
      for (size_t i = 0; i < strlen(line); i++) {
        // If we've on the closing brace line, then we're done
        if (line[i] == '}') {
          for (int i = 0; i < valuecount; i++) {
            debugprint(DEBUG_FULL, "getconfstr(): returning '%s'.\n", dest[i]);
          }

          // Close fine and return success (or 0 if no values found in an otherwise valid array)
          fclose(fp);
          if (valuecount) {
            return 1;
          } else {
            return 0;
          }
        }

        // If not in the quotes yet
        if (!inquotes) {
          // Skip over initial spaces and tabs
          if (line[i] == ' ' || line[i] == '\t') {
            continue;
          } else if (line[i] == '"') {
            // Quotes found, we're now reading the value
            inquotes = 1;
            continue;
          } else {
            // Unexpected character found, return failure
            snprintf(dest[0], MAXDATASIZE, "Unexpected character '%c' found before opening quotes on array line for '%s'.\n", line[i], confname);
            fclose(fp);
            return -1;
          }
          // If inside the quotes (so, we've got to the actual value)
        } else if (inquotes) {
          // If we're on the last character and it isn't a closing quote, something is wrong
          if (i == strlen(line) - 1 && line[i] != '"') {
            snprintf(dest[0], MAXDATASIZE, "Reached end of line without finding closing quotes on array line for '%s'.\n", confname);
            fclose(fp);
            return -1;
          }

          // If we've found too many values, return an error
          if (valuecount > MAXCONFARR) {
            snprintf(dest[0], MAXDATASIZE, "Too many elements defined for configuration array '%s', maximum number is '%d'.\n", confname, MAXCONFARR);
            fclose(fp);
            return -1;
          }

          // Otherwise, copy everything that isn't the closing quotes to the current element in the dest array
          if (line[i] != '"') {
            dest[valuecount][valuelen] = line[i];
            valuelen++;
            continue;
          } else {
            // When we find the closing quotes, the value is read, so terminate the dest array element string
            dest[valuecount][valuelen] = '\0';
            // We're done with this value, ignore anything that may be after the closing quotes on this line
            valuecount++;
            break;
          }
        }
      }
    }
  }

  // If we get this far, then something went wrong
  snprintf(dest[0], MAXDATASIZE, "getconfarr(): didn't find any configuration array for '%s'.\n", confname);
  fclose(fp);
  return 0;
}

267 268
// Returns the value of the configuration option with name
// 'confname' from configuration file 'filename'.
269
// Sets errno to 0 on success, or ECONFINT if it fails, in which case the return value is undefined.
270
int getconfint(char *confname, char *filename) {
271
  errno = 0;
272 273
  char result[MAXCHAR];
  if (!getconfstr(confname, filename, result)) {
274
    debugprint(DEBUG_CRIT, "getconfint(): error getting configuration option '%s' from configuration file '%s'.\n", confname, filename);
275
    errno = ECONFINT;
276 277
  }

278
  return strtol(result, NULL, 10); // Convert resulting string to an integer, base 10
279
}
280

281 282 283 284 285 286 287 288 289 290 291 292 293
// Create the default configuration file.
// Return 1 on success, 0 on failure.
int createconfigfile(char *filename) {
  char *dirtmp;
  char *dir;
  dirtmp = strdup(filename);
  dir = strdup(dirname(dirtmp));

  // Make sure the parent directory exists
  struct stat st = {0};
  if (stat(dir, &st) == -1) {
    if (mkdir(dir, 0700)) {
      printf("Error creating config directory '%s'.\n", dir);
294
      debugprint(DEBUG_CRIT, "Error creating config directory '%s'.\n", dir);
295 296 297 298 299 300 301 302 303 304 305 306
      exit(1);
    } else {
      printf("Created config directory '%s'.\n", dir);
    }
  }

  FILE *fp;

  fp = fopen(filename, "a");

  if (fp == NULL) {
    printf("error: could not open default configuration file '%s' for writing.\n", filename);
307
    debugprint(DEBUG_CRIT, "error: could not open default configuration file '%s' for writing.\n", filename);
308 309 310 311 312 313
    exit(1);
  }

  // Prepare the string
  char *string =
  "# blabouncer configuration file\n"
314
  "#\n"
315
  "# Normal entries must be in the form:\n"
316 317 318
  "# option name, space, equals sign, space, double quote, option value, double quote\n"
  "# e.g.\n"
  "# realname = \"Mr Bla Bouncer\"\n"
319
  "#\n"
320 321 322 323 324 325 326 327 328 329 330
  "# Array entries must be in the form:\n"
  "# option name, space, equals sign, space, open brace\n"
  "# (optional indentation,) double quote, element value, double quoute\n"
  "# (optional multiple values to be repeated after the first one(s))\n"
  "# close brace\n"
  "# e.g.\n"
  "# connectcommands = {\n"
  "#   \"PRIVMSG NickServ IDENTIFY bananas\"\n"
  "#   \"PRIVMSG myfriend I'm online!\"\n"
  "# }\n"
  "#\n"
331
  "# Shell expansion is not supported, so do not try and specify e.g.\n"
332
  "# \"~/.blabouncer/\" or \"$HOME/.blabouncer/\", instead use \"/home/foo/.blabouncer\"\n"
333 334
  "#\n"
  "# Some settings can be reloaded at runtime, please refer to README for details.\n"
335
  "\n"
336 337 338 339 340 341 342 343
  "# Nick(s) to use when connecting - will be cycled through in order in the event of\n"
  "# a nick being in use or invalid\n"
  "nicks = {\n"
  "  \"blabounce\"\n"
  "  \"bbounce2\"\n"
  "  \"bbounce3\"\n"
  "}\n"
  "\n"
344
  "username = \"bounceusr\"\n"
345 346
  "realname = \"Mr Bla Bouncer\"\n"
  "\n"
347
  "# Channels to automatically join (comma-separated list, defaults to none)\n"
348 349
  "# Put channel keywords/passwords after channel names following a space.\n"
  "#channels = \"#blabouncer keyword,#test\"\n"
350
  "\n"
351 352 353 354
  "# Auto replay mode upon a bouncer client connecting\n"
  "# \"none\" = Don't auto replay\n"
  "# \"time\" = Always send the last \"replayseconds\" worth of logs\n"
  "# \"lastspoke\" = All messages since your current nick last spoke\n"
355 356
  "# \"noclients\" = All messages since you last had no clients connected\n"
  "# \"lastchange\" = All messages since your last client connection/disconnection\n"
357
  "# \"perclient\" = All messages since the current client last disconnected (see README)\n"
358 359 360
  "replaymode = \"time\"\n"
  "\n"
  "# How many seconds of replay log should be sent to connecting clients if replaymode = \"time\"\n"
361
  "replayseconds = \"600\"\n"
362
  "\n"
363 364 365
  "# Should replay log timestamps include the date when replaying? (\"1\" for yes or \"0\" for no)\n"
  "replaydates = \"0\"\n"
  "\n"
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
  "# Connect password clients must provided to connect\n"
  "password = \"bananas\"\n"
  "\n"
  "# Port the bouncer should listen on\n"
  "clientport = \"1234\"\n"
  "\n"
  "# Enable TLS for clients connecting to the bouncer (\"1\" for yes or \"0\" for no)\n"
  "# If \"0\" then certfile and keyfile need not be set\n"
  "clienttls = \"1\"\n"
  "\n"
  "# Enable TLS for the bouncer connecting to the IRC server (\"1\" for yes or \"0\" for no)\n"
  "servertls = \"1\"\n"
  "\n"
  "# Real IRC server the bouncer connects to\n"
  "ircserver = \"irc.blatech.net\"\n"
  "\n"
  "# Real IRC server port\n"
  "ircserverport = \"6697\"\n"
384 385 386
  "\n"
  "# Real IRC server password\n"
  "#ircserverpassword = \"apples\"\n"
387
  "\n"
388 389 390 391 392
  "# Command(s) to send to the server upon completing registration (e.g. a NickServ password)\n"
  "#connectcommands = {\n"
  "#  \"PRIVMSG NickServ IDENTIFY bananas\"\n"
  "#  \"PRIVMSG myfriend I'm online!\"\n"
  "#}\n"
393
  "\n"
394 395 396 397 398
  "# Base directory (defaults to $HOME/.blabouncer/)\n"
  "# Things such as the logs directory will be placed below this\n"
  "#basedir = \"/home/foo/.blabouncer/\"\n"
  "\n"
  "# Certificate file (defaults to <basedir>/cert.pem)\n"
399
  "# If clienttls = \"0\" then this need not be set\n"
400
  "#certfile = \"/home/foo/.blabouncer/cert.pem\"\n"
401
  "\n"
402
  "# Certificate key file (defaults to <basedir>/key.pem)\n"
403
  "# If clienttls = \"0\" then this need not be set\n"
404
  "#keyfile = \"/home/foo/.blabouncer/key.pem\"\n"
405
  "\n"
406 407 408 409 410 411
  "# Enable logging (\"1\" for yes or \"0\" for no)\n"
  "# Logs go to basedir/logs/ with one file per channel/nick\n"
  "logging = \"1\"\n"
  "\n"
  "# Enable replay logging (\"1\" for yes or \"0\" for no)\n"
  "# Replay log goes to basedir/replay.log\n"
412 413
  "replaylogging = \"1\"\n"
  "\n"
414 415
  "# Debug verbosity (\"0\" for critical only, \"1\" for some extra info, \"2\" for full debug mode)\n"
  "# (All output goes to <basedir>/debug.txt)\n"
416 417 418 419
  "debug = \"2\"\n"
  "\n"
  "# Number of debug logs to keep\n"
  "debugkeep = \"5\"\n";
420 421

  // Write complete string to file
422
  if ((fprintf(fp, "%s", string)) < 0) {
423
    debugprint(DEBUG_CRIT, "error: could not write to replay log file.\n");
424 425 426
  }

  fclose(fp);
427 428
  free(dirtmp);
  free(dir);
429 430
  return 0;
}