From 6554cba9441de42e0c5c455b16fb5f6b39c19e28 Mon Sep 17 00:00:00 2001 From: David Sommerseth Date: Sat, 8 Jun 2013 02:32:34 +0200 Subject: auth: Added socket-auth module This can authenticate username/passwords via a file socket to an authentication service. A simple authentication service written in Python is added as well. Signed-off-by: David Sommerseth --- CMakeLists.txt | 2 +- auth/socket/CMakeLists.txt | 35 +++++++ auth/socket/demo-auth-server.py | 222 +++++++++++++++++++++++++++++++++++++++ auth/socket/socket-auth.c | 224 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 auth/socket/CMakeLists.txt create mode 100755 auth/socket/demo-auth-server.py create mode 100644 auth/socket/socket-auth.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1297b7f..3a07c83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,7 +181,7 @@ SET_PROPERTY(TARGET common PROPERTY IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/common # Start the building. First build the common library, and then the requested eurephia modules IF(DATABASE OR PLUGIN OR FIREWALL OR EUREPHIADM) - SUBDIRS(common auth/dummy auth/flatfile ${subdirs}) + SUBDIRS(common auth/dummy auth/flatfile auth/socket ${subdirs}) ENDIF(DATABASE OR PLUGIN OR FIREWALL OR EUREPHIADM) # Compile Doxygen docs at the end if requested diff --git a/auth/socket/CMakeLists.txt b/auth/socket/CMakeLists.txt new file mode 100644 index 0000000..63da08a --- /dev/null +++ b/auth/socket/CMakeLists.txt @@ -0,0 +1,35 @@ +# cmake rules for eurephia - dummy-auth plug-in +# +# This module is only for testing purpose. +# DO NO USE THIS PLUG-IN IN A PRODUCTION ENVIRONMENT +# +# GPLv2 only - Copyright (C) 2008 - 2012 +# David Sommerseth +# +# This program 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 2 +# of the License. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +PROJECT(socket-auth C) +cmake_minimum_required(VERSION 2.6) + +# Compiler settigns +INCLUDE_DIRECTORIES(. ../../common .. ../../database) + +ADD_LIBRARY(socket-auth SHARED + socket-auth.c +) +TARGET_LINK_LIBRARIES(socket-auth common) +SET_TARGET_PROPERTIES(socket-auth PROPERTIES COMPILE_FLAGS -fPIC) +SET_TARGET_PROPERTIES(socket-auth PROPERTIES OUTPUT_NAME socket-auth PREFIX "") + diff --git a/auth/socket/demo-auth-server.py b/auth/socket/demo-auth-server.py new file mode 100755 index 0000000..d985fa3 --- /dev/null +++ b/auth/socket/demo-auth-server.py @@ -0,0 +1,222 @@ +#!/usr/bin/python2 +# +# demo-auth-server.py - simplistic demo server for the auth-socket module +# +# Copyright (C) 2013 David Sommerseth +# +# This program 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, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 this program. If not, see . +# +# DESCRIPTION: +# +# This is a fairly simple eurephia socket-auth authentication service +# +# The eurephiaAuthService class can be reused and the real demo +# program in the '__main__' section provides a small example how +# to use this service class. +# +# This demo takes one argument, which is the file path of the socket to create. +# This server needs to be started before OpenVPN+eurephia when the auth-socket +# module is used. +# + +import socket, os, os.path, sys, signal + +STATUS_PASS = 1 +STATUS_FAIL = 2 +STATUS_ERROR = 3 + +class eurephiaAuthService(object): + """Simple socket based authentication service for eurephia. +This is to be used together with the eurephia socket-auth.so module""" + + def __init__(self, socketname, auth_callback_fnc, socket_umask=007): + """Initiates the authentication service. The socketname variable +is a filename to where the file based socket will be created for socket-auth.so +to connect to. The auth_callback_fnc is the function which will be called +when a authentication request arrives.""" + self.__socketname = socketname + self.__umask = socket_umask + self.__authcallback = auth_callback_fnc + self.__socket = None + self.__keeprunning = None + + + def __debug(self, msg): + if self.__dbg: + sys.stdout.write(msg) + sys.stdout.flush() + + + def __prepare_socket(self): + # Clean up old sockets, if present + if os.path.exists(self.__socketname): + os.remove(self.__socketname) + + # Create new socket + prev_umask = os.umask(self.__umask) + self.__socket = socket.socket( socket.AF_UNIX, socket.SOCK_STREAM ) + self.__socket.bind(self.__socketname) + os.umask(prev_umask) + + # Prepare for just a single connection + self.__socket.listen(1) + + + def __get_connection(self): + while True: + try: + self.__restart = False + self.__debug("Waiting for eurephia socket-auth.so to connect ... ") + conn, client = self.__socket.accept() + self.__debug("Connected\n") + return conn + except socket.error, e: + if self.__keeprunning and not self.__restart: + raise e + if not self.__keeprunning: + self.__debug(" Aborting\n") + return None + + + def __read_data(self, conn): + try: + # First byte is the length of the data + msglen = conn.recv( 1 ) + if not msglen: + return None + + # Read the data + msg = conn.recv( ord(msglen[0]) ) + + if msg and msg == "***SHUTDOWN***": + self.__debug("OpenVPN is disonnecting\n") + self.__restart = True + return None + return msg + + except socket.error, e: + if self.__keeprunning and not self.__restart: + raise e + if not self.__keeprunning: + self.__debug(" Aborting\n") + return None + + + def __send_response(self, conn, status): + # A response consist only of 4 bytes, + if status == STATUS_PASS: + self.__debug("PASS\n") + conn.send("PASS") + elif status == STATUS_FAIL: + self.__debug("FAIL\n") + conn.send("FAIL") + elif status == STATUS_ERROR: + self.__debug("Internal error\n") + conn.send("IERR") + else: + self.__debug("FATAL\n") + raise ValueError("Invalid status code") + + def __main_loop(self): + self.__restart = False + conn = self.__get_connection() + if not conn: + return + while self.__keeprunning and not self.__restart: + username = self.__read_data(conn) + if not username: + if self.__keeprunning and not self.__restart: + self.__debug("** ERROR ** Failed to read username. Aborting\n") + break + + passwd = self.__read_data(conn) + if not passwd: + if self.__keeprunning and not self.__restart: + self.__debug("** ERROR ** Failed to read password for '%s'. Aborting\n" % username) + break + + self.__debug("Authenticating '%s' ... " % username) + res = self.__authcallback(username, passwd) + self.__send_response(conn, res) + conn.close() + self.__debug("Closed connection\n") + + + def __sighandler(self, sig, frame): + if sig == signal.SIGINT or sig == signal.SIGTERM: + self.__keeprunning = False + signal.signal(signal.SIGINT, self.__sighandler) + signal.signal(signal.SIGTERM, self.__sighandler) + elif sig == signal.SIGHUP: + self.__debug("Caught SIGHUP\n") + self.__restart = True + signal.signal(signal.SIGHUP, self.__sighandler) + + + def Run(self, debug = False): + "Starts the authentication service and loops until a shutdown signal is caught" + self.__dbg = debug + self.__prepare_socket() + self.__keeprunning = True + + # Prepare signal handling + signal.signal(signal.SIGINT, self.__sighandler) + signal.signal(signal.SIGTERM, self.__sighandler) + signal.signal(signal.SIGHUP, self.__sighandler) + + while self.__keeprunning: + try: + self.__main_loop() + except KeyboardInterrupt: + # Fallback if the signal handler doesn't catch it + self.__debug("\nCaught SIGINT\n") + + # Complete the shutdown by removing the socket file + self.__socket.close() + os.remove(self.__socketname) + + +if __name__ == "__main__": + # + # + # Demo authentication service. This SHOULD NOT be used in production, + # but can be used as a template on how to use socket-auth.so and + # the eurephiaAuthentication class. + # + # + + + # Authentication callback - used by eurephiaAuthService + def auth_callback(username, password): + "Stupid authentication callback demo" + + print " [auth_callback('%s', '%s')] " % (username, password), + + if username == 'foo' and password == 'bar': + return STATUS_PASS + else: + return STATUS_FAIL + + # Simple arugment parser ... takes only one argument, the socket file + # eurephia socket-auth.so is supposed to connect to + if len(sys.argv) != 2: + print "Usage: %s " % sys.argv[0] + sys.exit(1) + socketname = sys.argv[1] + + # Prepare authentication service, using the callback function above + authserv = eurephiaAuthService(socketname, auth_callback) + # Start running with some debug info + authserv.Run(debug=True) + diff --git a/auth/socket/socket-auth.c b/auth/socket/socket-auth.c new file mode 100644 index 0000000..41396fb --- /dev/null +++ b/auth/socket/socket-auth.c @@ -0,0 +1,224 @@ +/* socket-auth.c -- Authenticates user/password against a socket service + * + * The socket service retrieves the username and password and replies with + * PASS or FAIL. See the demo-auth-server.py for more info + * + * Copyright (C) 2013 David Sommerseth + * + * This program 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, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + * + */ + +/** + * @file socket-auth.c + * @author David Sommerseth + * @date 2013-06-08 + * + * @brief Authenticate users using a socket based auth server + */ + +/* + * The wire protocol for the socket service is: + * + * The client sends: + * 1 byte - lenght of username + * x bytes - the username + * 1 byte - lenght of password + * x bytes - the password + * + * And now the server is expected to reply with: + * 4 bytes - containing the string FAIL or PASS + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * INTERNAL VARIABLES AND FUNCTIONS + */ + +static int authservfd = -1; +static struct sockaddr_un sockaddr; + +/** + * Send data to the remote end according to the wire protocol + * where the first byte is the length of data to be sent. Maximum + * 127 bytes can be transferred. + * + * @param fd Socket fd to send the data to + * @param msg Pointer to the string to be sent + * + * @return Returns 1 on success, otherwise 0 + */ +static int send_data(int fd, const char *msg) +{ + int msglen = strlen(msg); + char buf[130]; + + memset(&buf, 0, 130); + if( msglen > 127 ) { + msglen = 127; + } + snprintf(buf, msglen+1, "%c", (int) strlen(msg)); + if (write(fd, buf, strlen(buf)) != strlen(buf)) { + return 0; + } + + snprintf(buf, msglen+1, "%s", msg); + if (write(fd, buf, strlen(buf)) != strlen(buf)) { + return 0; + } + return 1; +} + + +/** + * Reads the server response and parses it. Anything except of 'PASS' is + * handled as a failure. But it expects the response to alwyas be 4 bytes. + * + * @param fd Socket fd where to read the response from + * + * @returns Returns 1 on successful authentication, otherwise 0. If less than 4 bytes + * was read it will return -1. In this case, it might be wise to reconnect to + * the server. + */ +static int parse_result(int fd) +{ + char buf[6]; + + memset(&buf, 0, 6); + int r = read(fd, &buf, 4); + + if( r == 4 ) { + return strcmp(buf, "PASS") == 0; + } + return -1; +} + + +/** + * Sends username and password via the socket to the authentication server and + * parses the result back. + * + * @param fd Socket fd to the authentication server + * @param username Username to be authenticated + * @param passwd Matching password to the username + * + * @returns See parse_result() for valid result codes. + */ +static int authenticate_user(int fd, const char *username, const char *passwd) +{ + if( !send_data(fd, username) ) { + return 0; + } + if( !send_data(fd, passwd) ) { + return 0; + } + return parse_result(fd); +} + + +/* + * Required plug-in functions + */ + +static ePluginInfo pluginfo = { .name = "socket-auth plug-in", + .version = "1.0", + .copyright = "2013 (C) David Sommerseth ", + .pluginType = eptAUTH, + .APIversion = 1 }; + + +/** + * @copydoc PluginInfo() + */ +ePluginInfo * PluginInfo() +{ + return &pluginfo; +} + +/** + * @copydoc PluginInit() + */ +int PluginInit(eurephiaCTX *ctx, const char *args) +{ + authservfd = socket(AF_UNIX, SOCK_STREAM, 0); + if ( authservfd == -1 ) { + eurephia_log(ctx, LOG_FATAL, 0, "Failed to initialise socket to the socket-auth server"); + return 0; + } + + memset(&sockaddr, 0, sizeof(struct sockaddr_un)); + sockaddr.sun_family = AF_UNIX; + strncpy(sockaddr.sun_path, args, sizeof(sockaddr.sun_path)-1); + + if( connect(authservfd, (struct sockaddr*)&sockaddr, sizeof(struct sockaddr_un)) == -1) { + eurephia_log(ctx, LOG_FATAL, 0, "Failed to connect to the socket-auth server"); + close(authservfd); + authservfd = -1; + return 0; + } + + eurephia_log(ctx, LOG_INFO, 0, "socket-auth connected via %s", args); + return 1; +} + + +/** + * @copydoc PluginClose() + */ +void PluginClose(eurephiaCTX *ctx) +{ + send_data(authservfd, "***SHUTDOWN***"); + close(authservfd); + eurephia_log(ctx, LOG_INFO, 0, "Disconnected from auth-socket"); +} + + +/** + * @copydoc AuthenticateUser() + */ +eAuthResult * AuthenticateUser(eurephiaCTX *ctx, const char *username, const char *passwd) +{ + eAuthResult *ret = NULL; + int authres = -2; + + DEBUG(ctx, 20, "socket-auth:AuthenticateUser('%s', xxxxxx)", username); + + ret = malloc_nullsafe(ctx, sizeof(eAuthResult)+2); + authres = authenticate_user(authservfd, username, passwd); + if( authres < 0 ) { + ret->status = eAUTH_PLGERROR; + ret->msg = strdup("Failed communicating with auth-server"); + } else if( authres == 0 ) { + ret->status = eAUTH_FAILED; + ret->msg = strdup("Wrong password"); + } else if( authres == 1 ) { + ret->status = eAUTH_SUCCESS; + } else { + // This should never ever happen + ret->status = eAUTH_PLGERROR; + ret->msg = strdup("Unknown result code"); + } + return ret; +} -- cgit