/*
 * Copyright (c) 2003-2018
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/* 
 * RADIUS authentication
 * (Remote Authentication Dial In User Service)
 * RFC 2865, RFC 3579
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2018\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: local_radius_auth.c 2997 2018-01-30 01:01:03Z brachman $";
#endif

#include "dacs.h"

#include <radlib.h>

static const char *log_module_name = "local_radius_auth";

/* The only suitable (and available) choice at present. */
#define USE_LIBRADIUS 1

#ifndef PROG

#if defined(USE_LIBRADIUS)
/*
 * This is the API used by FreeBSD/NetBSD and various languages to support
 * their RADIUS modules, but apparently not by OpenBSD
 * (http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libradius/)
 */
int
local_radius_auth(char *username, char *password, char *aux,
				  char *radius_server, int radius_port, char *radius_secret,
				  char *radius_ok_msg, int radius_timeout, int radius_max_tries,
				  int radius_dead_time)
{
  int rc, req;
  char *ok_msg;
  struct in_addr bind_to;
  struct rad_handle *rad;

  if (username == NULL || password == NULL || radius_server == NULL
	  || radius_secret == NULL) {
    log_msg((LOG_ERROR_LEVEL, "Required authentication argument is NULL"));
    return(-1);
  }

  /*
   * The API declares these as integers but treats them as unsigned
   * internally.  So weed out some potential troublemakers here.
   * All times are in seconds.
   *
   * The API uses default values for these parameters when it loads them
   * from a text-based configuration file, but (except for the port number)
   * this is not the case when they are passed by rad_add_server*().
   * These default values are not documented, but the portable implementation
   * privately defines the constants: MAXTRIES = 3, TIMEOUT = 3, DEAD_TIME = 0.
   * A zero value for RADIUS_PORT implies server port 1812.
   */
  if (radius_port < 0 || radius_timeout <= 0 || radius_max_tries <= 0
	  || radius_dead_time < 0) {
    log_msg((LOG_ERROR_LEVEL, "Invalid integer argument"));
    return(-1);
  }

  ok_msg = NULL;

  if ((rad = rad_auth_open()) == NULL) {
    log_msg((LOG_ERROR_LEVEL, "rad_auth_open() failed"));
    return(-1);
  }

  /*
   * The API allows multiple servers to be configured, which will be
   * tried round-robin, but for now only a single server is supported.
   * The bind_to address can be used to set the source address for the
   * server requests, which might be useful on a multi-homed host (also not
   * yet supported here).
   */
  memset(&bind_to, 0, sizeof(bind_to));
  rc = rad_add_server_ex(rad, radius_server, radius_port, radius_secret,
						 radius_timeout, radius_max_tries, radius_dead_time,
						 &bind_to);
  if (rc == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_add_server_ex() failed"));
  err:
    if (ok_msg != NULL)
      free(ok_msg);

    rad_close(rad);
    return(-1);
  }

  /* Packet types and Attribute types are defined in radlib.h. */

  /* RFC 2865 S4.1 */
  req = rad_create_request(rad, RAD_ACCESS_REQUEST);
  if (req == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_create_request(): %s", rad_strerror(rad)));
    goto err;
  }

  /* RFC 2865 S5.1 -- UTF-8 */
  if (rad_put_string(rad, RAD_USER_NAME, username) == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_put_string(): %s", rad_strerror(rad)));
    goto err;
  }

  /* RFC 2865 S5.2 */
  if (rad_put_string(rad, RAD_USER_PASSWORD, password) == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_put_string(): %s", rad_strerror(rad)));
    goto err;
  }

  /* RFC 2865 S5.6 */
  if (rad_put_int(rad, RAD_SERVICE_TYPE, RAD_AUTHENTICATE_ONLY) == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_put_int(): %s", rad_strerror(rad)));
    goto err;
  }

  /* RFC 3579 S3.2 */
  if (rad_put_message_authentic(rad) == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_put_message_authentic(): %s",
			 rad_strerror(rad)));
    goto err;
  }

#ifdef NOTDEF
  if (rad_put_int(rad, RAD_EAP_MESSAGE, 1) == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_put_int(): %s", rad_strerror(rad)));
    goto err;
  }
#endif

  if ((rc = rad_send_request(rad)) == -1) {
	log_msg((LOG_ERROR_LEVEL, "rad_send_request: %s", rad_strerror(rad)));
    goto err;
  }
  else if (rc != RAD_ACCESS_ACCEPT) {
    if (rc == RAD_ACCESS_REJECT)
      log_msg((LOG_TRACE_LEVEL, "Auth failed for username=\"%s\"", username));
    else
      log_msg((LOG_ERROR_LEVEL, "Unexpected response: rad_send_request=%d", rc));
    goto err;
  }

  log_msg((LOG_TRACE_LEVEL, "Auth succeeded for username=\"%s\"", username));

  while (1) {
    const void *attr_val;
    size_t attr_len;

    attr_len = 0;
	rc = rad_get_attr(rad, (const void **) &attr_val, &attr_len);
	if (rc == -1) {
	  /* A malformed attribute or some other unusual error occurred. */
	  log_msg((LOG_ERROR_LEVEL, "rad_get_attr(): %s", rad_strerror(rad)));
	  goto err;
	}

    if (rc == 0)
      break;

    if (rc == RAD_REPLY_MESSAGE) {
      if (ok_msg != NULL) {
        log_msg((LOG_ERROR_LEVEL, "Server sent multiple reply messages"));
        goto err;
      }
      ok_msg = rad_cvt_string((char *) attr_val, attr_len); 
    }
  }

  /* If the server is expected to send a particular reply, validate it. */
  if (radius_ok_msg != NULL) {
	if (ok_msg == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Server did not send expected reply message"));
	  goto err;
	}
	if (!strcaseeq(ok_msg, radius_ok_msg)) {
	  log_msg((LOG_ERROR_LEVEL, "Invalid server message received"));
	  goto err;
	}
	log_msg((LOG_TRACE_LEVEL, "Server sent expected reply message"));
  }

  if (ok_msg != NULL)
    free(ok_msg);

  rad_close(rad);

  return(0);
}
#elif defined(USE_RADCLI)
/*
 * Alternate implementation using the FreeRADIUS project's client library, e.g.,
 * freeradius-client-1.1.7.tar.gz
 * I had hoped it could be used but, unfortunately, its documentation is
 * terrible, the design and implementation are questionable, and it
 * acknowledges:
 * "This code has not yet been fully audited by the
 * FreeRADIUS project, as it has only recently been adopted by the FreeRADIUS
 * project to continue development and support."
 * http://ftp.ticklers.org/ftp.freeradius.org/
 */
#error "Sorry, the FreeRADIUS client API is not supported!"
#else
#error "No available RADIUS library has been configured!"
#endif

#else

int
main(int argc, char **argv)
{
  int emitted_dtd, i, radius_max_tries, radius_port, radius_timeout;
  int radius_dead_time;
  char *errmsg, *jurisdiction, *username, *password, *aux;
  char *radius_ok_msg, *radius_secret, *radius_server;
  Auth_reply_ok ok;
  Kwv *kwv;

  errmsg = "internal";
  emitted_dtd = 0;
  username = password = aux = jurisdiction = NULL;

  radius_timeout = -1;
  radius_dead_time = -1;
  radius_max_tries = -1;

  radius_server = NULL;
  radius_port = -1;
  radius_secret = NULL;
  radius_ok_msg = NULL;

  if (dacs_init(DACS_LOCAL_SERVICE, &argc, &argv, &kwv, &errmsg) == -1) {
	/* If we fail here, we may not have a DTD with which to reply... */
  fail:
	if (password != NULL)
	  strzap(password);
	if (aux != NULL)
	  strzap(aux);
	if (emitted_dtd) {
	  printf("%s\n", make_xml_auth_reply_failed(NULL, NULL));
	  emit_xml_trailer(stdout);
	}
	if (errmsg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Failed: reason=%s", errmsg));

	exit(1);
  }

  /* This must go after initialization. */
  emitted_dtd = emit_xml_header(stdout, "auth_reply");

  if (argc > 1) {
	errmsg = "Usage: unrecognized parameter";
	goto fail;
  }

  for (i = 0; i < kwv->nused; i++) {
	if (streq(kwv->pairs[i]->name, "USERNAME") && username == NULL)
	  username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "PASSWORD") && password == NULL)
	  password = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUXILIARY") && aux == NULL)
	  aux = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_JURISDICTION")
			 && jurisdiction == NULL)
	  jurisdiction = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_VERSION"))
	  ;
	else if (streq(kwv->pairs[i]->name, "RADIUS_SERVER")
			 && radius_server == NULL)
	  radius_server = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "RADIUS_SECRET")
			 && radius_secret == NULL)
	  radius_secret = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "RADIUS_OK_MSG")
			 && radius_ok_msg == NULL)
	  radius_ok_msg = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "RADIUS_PORT")
			 && radius_port == -1) {
	  if (strnum(kwv->pairs[i]->val, STRNUM_I, &radius_port) == -1) {
		log_msg((LOG_ALERT_LEVEL, "Invalid RADIUS_PORT: \"%s\"",
				 kwv->pairs[i]->val));
		errmsg = "Invalid RADIUS_PORT parameter";
		goto fail;
	  }
	}
	else if (streq(kwv->pairs[i]->name, "RADIUS_TIMEOUT")
			 && radius_timeout == -1) {
	  if (strnum(kwv->pairs[i]->val, STRNUM_I, &radius_timeout) == -1) {
		log_msg((LOG_ALERT_LEVEL, "Invalid RADIUS_TIMEOUT: \"%s\"",
				 kwv->pairs[i]->val));
		errmsg = "Invalid RADIUS_TIMEOUT parameter";
		goto fail;
	  }
	}
	else if (streq(kwv->pairs[i]->name, "RADIUS_DEAD_TIME")
			 && radius_dead_time == -1) {
	  if (strnum(kwv->pairs[i]->val, STRNUM_I, &radius_dead_time) == -1) {
		log_msg((LOG_ALERT_LEVEL, "Invalid RADIUS_DEAD_TIME: \"%s\"",
				 kwv->pairs[i]->val));
		errmsg = "Invalid RADIUS_DEAD_TIME parameter";
		goto fail;
	  }
	}
	else if (streq(kwv->pairs[i]->name, "RADIUS_MAX_TRIES")
			 && radius_max_tries == -1) {
	  if (strnum(kwv->pairs[i]->val, STRNUM_I, &radius_max_tries) == -1) {
		log_msg((LOG_ALERT_LEVEL, "Invalid RADIUS_MAX_TRIES: \"%s\"",
				 kwv->pairs[i]->val));
		errmsg = "Invalid RADIUS_MAX_TRIES parameter";
		goto fail;
	  }
	}
	else
	  log_msg((LOG_TRACE_LEVEL, "Parameter: '%s'", kwv->pairs[i]->name));
  }

  /* Verify that we're truly responsible for DACS_JURISDICTION. */
  if (dacs_verify_jurisdiction(jurisdiction) == -1) {
	errmsg = "Missing or incorrect DACS_JURISDICTION";
	goto fail;
  }

  if (username == NULL) {
	errmsg = "No USERNAME specified";
	goto fail;
  }

  if (password == NULL) {
	errmsg = "No PASSWORD specified";
	goto fail;
  }

  if (radius_server == NULL) {
	errmsg = "No RADIUS_SERVER specified";
	goto fail;
  }

  if (radius_secret == NULL) {
	errmsg = "No RADIUS_SECRET specified";
	goto fail;
  }

  if (radius_port == -1) {
	log_msg((LOG_TRACE_LEVEL, "Using default RADIUS port"));
	radius_port = 0;
  }

  if (radius_timeout == -1)
	radius_timeout = RADIUS_DEFAULT_TIMEOUT_SECS;
  if (radius_dead_time == -1)
	radius_dead_time = RADIUS_DEFAULT_DEAD_TIME_SECS;
  if (radius_max_tries == -1)
	radius_max_tries = RADIUS_DEFAULT_MAX_TRIES;

  log_msg((LOG_TRACE_LEVEL, "radius_server=\"%s\"", radius_server));
  log_msg((LOG_TRACE_LEVEL, "radius_port=\"%d\"", radius_port));
  log_msg((LOG_TRACE_LEVEL, "radius_secret=\"%s\"",
		   (radius_secret != NULL) ? "non-NULL" : "NULL"));
  log_msg((LOG_TRACE_LEVEL, "radius_timeout=\"%d\"", radius_timeout));
  log_msg((LOG_TRACE_LEVEL, "radius_dead_time=\"%d\"", radius_dead_time));
  log_msg((LOG_TRACE_LEVEL, "radius_max_tries=\"%d\"", radius_max_tries));

  if (local_radius_auth(username, password, aux, radius_server, radius_port,
						radius_secret, radius_ok_msg, radius_timeout,
						radius_max_tries, radius_dead_time) == -1) {
	errmsg = "Username/Password/Aux incorrect";
	goto fail;
  }

  if (password != NULL)
	strzap(password);
  if (aux != NULL)
	strzap(aux);

  ok.username = username;
  /* If this wasn't specified, dacs_authenticate will use the default. */
  ok.lifetime = kwv_lookup_value(kwv, "CREDENTIALS_LIFETIME_SECS");
  ok.roles_reply = NULL;
  printf("%s\n", make_xml_auth_reply_ok(&ok));

  emit_xml_trailer(stdout);
  exit(0);
}
#endif
