summaryrefslogblamecommitdiffstats
path: root/daemon/logind/logind.c
blob: 161410f657501af1dec7c2ec62c27e7b08c77b7a (plain) (tree)






















































































































































































































































































































































                                                                                          
                                      










































































































































































                                                                                                       





                                                                    









































































































































































































































































                                                                                
                                         































































































                                                                             


                                                                      




































































































































                                                                                           



                                                       












                                                                                              
                                                   









                                                                            
                               


             


                            




                                                              


                             
                                                         

                     




                                            


                                 



                          

                 
                                                                                                         








                     
                  
     

                                                   
     




                                                          






                                                                     


























                                                                             
 












                                                                             






                                                 
// The High Performance Login Daemon (works with tunnel mode)
// $Id: logind.c,v 1.1 2009/06/09 13:13:42 piaip Exp piaip $
//
// Create:       Hung-Te Lin <piaip@csie.ntu.edu.tw>
// Contributors: wens, kcwu
// Initial Date: 2009/06/01
//
// Copyright (C) 2009, Hung-Te Lin <piaip@csie.ntu.edu.tw>
// All rights reserved

// TODO:
// 1. cache guest's usernum and check if too many guests online
// 2. change close connection to 'wait until user hit then close' (drop?)
// 3. make original port information in margs(mbbsd)?

#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <signal.h>
#include <event.h>

#define _BBS_UTIL_C_
#include "bbs.h"
#include "banip.h"
#include "logind.h"

#ifndef OPTIMIZE_SOCKET
#define OPTIMIZE_SOCKET(sock) do {} while(0)
#endif

#ifndef SOCKET_QLEN
#define SOCKET_QLEN (10)
#endif

#ifndef AUTHFAIL_SLEEP_SEC
#define AUTHFAIL_SLEEP_SEC  (15)
#endif

#ifndef OVERLOAD_SLEEP_SEC
#define OVERLOAD_SLEEP_SEC  (60)
#endif

#ifndef BAN_SLEEP_SEC
#define BAN_SLEEP_SEC       (60)
#endif

#define MAX_TEXT_SCREEN_LINES   (24)

///////////////////////////////////////////////////////////////////////
// global variables
int g_tunnel;           // tunnel for service daemon
int g_reload_data = 1;  // request to reload data

// server status
int g_overload = 0;
int g_banned   = 0;

///////////////////////////////////////////////////////////////////////
// login context, constants and states

enum {
    LOGIN_STATE_START  = 1,
    LOGIN_STATE_USERID,
    LOGIN_STATE_PASSWD,
    LOGIN_STATE_AUTH,

    LOGIN_HANDLE_WAIT = 1,
    LOGIN_HANDLE_BEEP,
    LOGIN_HANDLE_OUTC,
    LOGIN_HANDLE_BS,
    LOGIN_HANDLE_PROMPT_PASSWD,
    LOGIN_HANDLE_START_AUTH,

    AUTH_RESULT_STOP   = -2,
    AUTH_RESULT_FREEID = -1,
    AUTH_RESULT_FAIL   = 0,
    AUTH_RESULT_RETRY  = AUTH_RESULT_FAIL,
    AUTH_RESULT_OK     = 1,
};

#ifdef  CONVERT
#define IDBOXLEN    (IDLEN+2)   // one extra char for encoding
#else
#define IDBOXLEN    (IDLEN+1)
#endif

typedef struct {
    int  state;
    int  retry;
    int  encoding;
    int  t_lines;
    int  t_cols;
    Fnv32_t client_code;
    char userid [IDBOXLEN];
    char passwd [PASSLEN+1];
    char hostip [IPV4LEN+1];
} login_ctx;

typedef struct {
    struct bufferevent *bufev;
    struct event ev;
    TelnetCtx    telnet;
    login_ctx    ctx;
} login_conn_ctx;

void 
login_ctx_init(login_ctx *ctx)
{
    assert(ctx);
    memset(ctx, 0, sizeof(login_ctx));
    ctx->client_code = FNV1_32_INIT;
    ctx->state = LOGIN_STATE_START;
}

int 
login_ctx_retry(login_ctx *ctx)
{
    assert(ctx);
    ctx->state = LOGIN_STATE_START;
    ctx->encoding = 0;
    memset(ctx->userid, 0, sizeof(ctx->userid));
    memset(ctx->passwd, 0, sizeof(ctx->passwd));
    // do not touch hostip, client code, t_*
    ctx->retry ++;
    return ctx->retry;
}

int 
login_ctx_handle(login_ctx *ctx, int c)
{
    int l;

    assert(ctx);
    switch(ctx->state)
    {
        case LOGIN_STATE_START:
        case LOGIN_STATE_USERID:
            l = strlen(ctx->userid);

            if (c == KEY_ENTER)
            {
                ctx->state = LOGIN_STATE_PASSWD;
                return LOGIN_HANDLE_PROMPT_PASSWD;
            }
            if (c == KEY_BS)
            {
                if (!l)
                    return LOGIN_HANDLE_BEEP;
                ctx->userid[l-1] = 0;
                return LOGIN_HANDLE_BS;
            }

            if (!isascii(c) || !isprint(c) || 
                l+1 >= sizeof(ctx->userid))
                return LOGIN_HANDLE_BEEP;

            ctx->userid[l] = c;

            return LOGIN_HANDLE_OUTC;

        case LOGIN_STATE_PASSWD:
            l = strlen(ctx->passwd);

            if (c == KEY_ENTER)
            {
                // no matter what, apply the passwd
                ctx->state = LOGIN_STATE_AUTH;
                return LOGIN_HANDLE_START_AUTH;
            }
            if (c == KEY_BS)
            {
                if (!l)
                    return LOGIN_HANDLE_BEEP;
                ctx->passwd[l-1] = 0;
                return LOGIN_HANDLE_WAIT;
            }

            if (!isascii(c) || !isprint(c) || 
                l+1 >= sizeof(ctx->passwd))
                return LOGIN_HANDLE_BEEP;

            ctx->passwd[l] = c;

            return LOGIN_HANDLE_WAIT;

        default:
            break;
    }
    return LOGIN_HANDLE_BEEP;
}

///////////////////////////////////////////////////////////////////////
// I/O

static ssize_t 
_buff_write(login_conn_ctx *conn, const void *buf, size_t nbytes)
{
    return bufferevent_write(conn->bufev, buf, nbytes);
}

///////////////////////////////////////////////////////////////////////
// Mini Terminal

static void 
_mt_bell(login_conn_ctx *conn)
{
    static const char b = Ctrl('G');
    _buff_write(conn, &b, 1);
}

static void 
_mt_bs(login_conn_ctx *conn)
{
    static const char cmd[] = "\b \b";
    _buff_write(conn, cmd, sizeof(cmd)-1);
}

static void 
_mt_home(login_conn_ctx *conn)
{
    static const char cmd[] = ESC_STR "[H";
    _buff_write(conn, cmd, sizeof(cmd)-1);
}

static void 
_mt_clrtoeol(login_conn_ctx *conn)
{
    static const char cmd[] = ESC_STR "[K";
    _buff_write(conn, cmd, sizeof(cmd)-1);
}

static void 
_mt_clear(login_conn_ctx *conn)
{
    static const char cmd[] = ESC_STR "[2J";
    _mt_home(conn);
    _buff_write(conn, cmd, sizeof(cmd)-1);
}

static void 
_mt_move_yx(login_conn_ctx *conn, const char *mcmd)
{
    static const char cmd1[] = ESC_STR "[",
                      cmd2[] = "H";
    _buff_write(conn, cmd1, sizeof(cmd1)-1);
    _buff_write(conn, mcmd, strlen(mcmd));
    _buff_write(conn, cmd2, sizeof(cmd2)-1);
}

///////////////////////////////////////////////////////////////////////
// Telnet Protocol

static void 
_telnet_resize_term_cb(void *resize_arg, int w, int h)
{
    login_ctx *ctx = (login_ctx*) resize_arg;
    assert(ctx);
    ctx->t_lines = h;
    ctx->t_cols  = w;
}

static void 
_telnet_update_cc_cb(void *cc_arg, unsigned char c)
{
    login_ctx *ctx = (login_ctx*) cc_arg;
    assert(ctx);
    // fprintf(stderr, "update cc: %08lX", (unsigned long)ctx->client_code);
    FNV1A_CHAR(c, ctx->client_code);
    // fprintf(stderr, "-> %08lX\r\n", (unsigned long)ctx->client_code);
}

static void 
_telnet_write_data_cb(void *write_arg, int fd, const void *buf, size_t nbytes)
{
    login_conn_ctx *conn = (login_conn_ctx *)write_arg;
    _buff_write(conn, buf, nbytes);
}

const static struct TelnetCallback 
telnet_callback = {
    _telnet_write_data_cb,
    _telnet_resize_term_cb,
#ifdef DETECT_CLIENT
    _telnet_update_cc_cb,
#else
    NULL,
#endif
};

///////////////////////////////////////////////////////////////////////
// Socket Option

static int 
_set_connection_opt(int sock)
{
    const int szrecv = 1024, szsend=4096;
    const struct linger lin = {0};

    // keep alive: server will check target connection. (around 2hr)
    const int on = 1;
    setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (void*)&on, sizeof(on));
   
    // fast close
    setsockopt(sock, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
    // adjust transmission window
    setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&szrecv, sizeof(szrecv));
    setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&szsend, sizeof(szsend));
    OPTIMIZE_SOCKET(sock);

    return 0;
}

static int
_set_bind_opt(int sock)
{
    const int on = 1;

    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void*)&on, sizeof(on));
    _set_connection_opt(sock);

    return 0;
}

///////////////////////////////////////////////////////////////////////
// Draw Screen

#ifdef STR_GUEST
# define MSG_GUEST ",或以[" STR_GUEST "]參觀"
#else
# define MSG_GUEST ""
#endif

#ifdef STR_REGNEW
# define MSG_REGNEW ",或以[new]註冊"
#else
# define MSG_REGNEW
#endif

#define BOTTOM_YX           "24;1"
#define LOGIN_PROMPT_MSG    ANSI_RESET "請輸入代號" MSG_GUEST MSG_REGNEW ": " ANSI_REVERSE
#define LOGIN_PROMPT_YX     "21;1"
#define LOGIN_PROMPT_END    ANSI_RESET
#define PASSWD_PROMPT_MSG   ANSI_RESET MSG_PASSWD
#define PASSWD_PROMPT_YX    "22;1"
#define PASSWD_CHECK_MSG    ANSI_RESET "正在檢查帳號與密碼..."
#define PASSWD_CHECK_YX     PASSWD_PROMPT_YX
#define AUTH_SUCCESS_MSG    ANSI_RESET "密碼正確! 開始登入系統...\r\n"
#define AUTH_SUCCESS_YX     PASSWD_PROMPT_YX
#define FREEAUTH_SUCCESS_MSG    ANSI_RESET "開始登入系統...\r\n"
#define FREEAUTH_SUCCESS_YX AUTH_SUCCESS_YX
#define AUTH_FAIL_MSG       ANSI_RESET ERR_PASSWD
#define AUTH_FAIL_YX        PASSWD_PROMPT_YX
#define USERID_EMPTY_MSG    ANSI_RESET "請重新輸入。"
#define USERID_EMPTY_YX     PASSWD_PROMPT_YX
#define SERVICE_FAIL_MSG    ANSI_COLOR(0;1;31) "抱歉,系統目前無法使用,請稍候再試。" ANSI_RESET "\r\n"
#define SERVICE_FAIL_YX     BOTTOM_YX
#define OVERLOAD_CPU_MSG    "系統過載, 請稍後再來...\r\n"
#define OVERLOAD_CPU_YX     BOTTOM_YX
#define OVERLOAD_USER_MSG   "由於人數過多,請您稍後再來...\r\n"
#define OVERLOAD_USER_YX    BOTTOM_YX

#define FN_WELCOME          BBSHOME "/etc/Welcome"
#define FN_GOODBYE          BBSHOME "/etc/goodbye"
#define FN_BAN              BBSHOME "/" BAN_FILE

static char *welcome_screen, *goodbye_screen, *ban_screen;

static void
load_text_screen_file(const char *filename, char **pptr)
{
    FILE *fp;
    off_t sz, wsz, psz;
    char *p, *s = NULL;
    int max_lines = MAX_TEXT_SCREEN_LINES;

    sz = dashs(filename);
    if (sz < 1)
    {
        free(*pptr);
        *pptr = NULL;
        return;
    }
    wsz = sz*2 +1; // *2 for cr+lf, extra one byte for safe strchr().

    // XXX TODO use realloc?
    assert(pptr);
    s = *pptr;
    s = realloc(s, wsz);  
    *pptr = s;
    if (!s)
        return;

    memset(s, 0, wsz);
    p = s;
    psz = wsz;

    fp = fopen(filename, "rt");
    if (!fp)
    {
        free(s);
        return;
    }
    while ( max_lines-- > 0 &&
            fgets(p, psz, fp))
    {
        psz -= strlen(p);
        p += strlen(p);
        *p ++ = '\r';
    }
    fclose(fp);
    *pptr = s;
}

static void regular_check();

static void
reload_data()
{
    regular_check();

    if (!g_reload_data)
        return;

    fprintf(stderr, "start reloading data.\r\n");
    g_reload_data = 0;
    load_text_screen_file(FN_WELCOME, &welcome_screen);
    load_text_screen_file(FN_GOODBYE, &goodbye_screen);
    load_text_screen_file(FN_BAN,     &ban_screen);
}

static void
draw_text_screen(login_conn_ctx *conn, const char *scr)
{
    const char *ps, *pe;
    char buf[64];
    time4_t now;

    _mt_clear(conn);
    if (!scr || !*scr)
        return;

    // draw the screen from text file
    // XXX Because the text file may contain a very small subset of escape sequence
    // *t[Cdate] and *u[SHM->UTMPnumber], we implement a tiny version of 
    // expand_esc_star here.

    ps = pe = scr;
    while(pe && *pe)
    {
        // find a safe range between (ps, pe) to print
        pe = strchr(pe, ESC_CHR);
        if (!pe)
        {
            // no more escapes, print all.
            _buff_write(conn, ps, strlen(ps));
            break;
        }

        // let's look ahead
        pe++;

        // if not esc_star, search for next.
        if (*pe != '*')
            continue;

        // flush previous data
        _buff_write(conn, ps, pe - ps - 1);

        buf[0] = 0; pe++;

        // expand the star
        switch(*pe)
        {
            case 't':   // current time
                // strcpy(buf, "[date]");
                now = time(0);
                strlcpy(buf, Cdate(&now), sizeof(buf));
                break;

            case 'u':   // current online users
                // strcpy(buf, "[SHM->UTMPnumber]");
                snprintf(buf, sizeof(buf), "%d", SHM->UTMPnumber);
                break;
        }

        if(buf[0])
            _buff_write(conn, buf, strlen(buf));
        pe ++;
        ps = pe;
    }
}

static void
draw_goodbye(login_conn_ctx *conn)
{
    draw_text_screen(conn, goodbye_screen);
}

static void 
draw_userid_prompt(login_conn_ctx *conn)
{
    char box[IDBOXLEN];

    _mt_move_yx(conn, LOGIN_PROMPT_YX);  _mt_clrtoeol(conn);
    _buff_write(conn, LOGIN_PROMPT_MSG, sizeof(LOGIN_PROMPT_MSG)-1);
    // draw input box
    memset(box, ' ', sizeof(box));
    _buff_write (conn, box,   sizeof(box));
    memset(box, '\b',sizeof(box));
    _buff_write (conn, box,   sizeof(box));
}

static void
draw_userid_prompt_end(login_conn_ctx *conn)
{
    _buff_write(conn, LOGIN_PROMPT_END, sizeof(LOGIN_PROMPT_END)-1);
}

static void
draw_passwd_prompt(login_conn_ctx *conn)
{
    _mt_move_yx(conn, PASSWD_PROMPT_YX); _mt_clrtoeol(conn);
    _buff_write(conn, PASSWD_PROMPT_MSG, sizeof(PASSWD_PROMPT_MSG)-1);
}

static void
draw_empty_userid_warn(login_conn_ctx *conn)
{
    _mt_move_yx(conn, USERID_EMPTY_YX); _mt_clrtoeol(conn);
    _buff_write(conn, USERID_EMPTY_MSG, sizeof(USERID_EMPTY_MSG)-1);
}

static void 
draw_check_passwd(login_conn_ctx *conn)
{
    _mt_move_yx(conn, PASSWD_CHECK_YX); _mt_clrtoeol(conn);
    _buff_write(conn, PASSWD_CHECK_MSG, sizeof(PASSWD_CHECK_MSG)-1);
}

static void
draw_auth_success(login_conn_ctx *conn, int free)
{
    if (free)
    {
        _mt_move_yx(conn, FREEAUTH_SUCCESS_YX); _mt_clrtoeol(conn);
        _buff_write(conn, FREEAUTH_SUCCESS_MSG, sizeof(FREEAUTH_SUCCESS_MSG)-1);
    } else {
        _mt_move_yx(conn, AUTH_SUCCESS_YX); _mt_clrtoeol(conn);
        _buff_write(conn, AUTH_SUCCESS_MSG, sizeof(AUTH_SUCCESS_MSG)-1);
    }
}

static void
draw_auth_fail(login_conn_ctx *conn)
{
    _mt_move_yx(conn, AUTH_FAIL_YX); _mt_clrtoeol(conn);
    _buff_write(conn, AUTH_FAIL_MSG, sizeof(AUTH_FAIL_MSG)-1);
}

static void
draw_service_failure(login_conn_ctx *conn)
{
    _mt_move_yx(conn, SERVICE_FAIL_YX); _mt_clrtoeol(conn);
    _buff_write(conn, SERVICE_FAIL_MSG, sizeof(SERVICE_FAIL_MSG)-1);
}

static void
draw_overload(login_conn_ctx *conn, int type)
{
    if (type == 1)
    {
        _mt_move_yx(conn, OVERLOAD_CPU_MSG); _mt_clrtoeol(conn);
        _buff_write(conn, OVERLOAD_CPU_MSG, sizeof(OVERLOAD_CPU_MSG)-1);
    } 
    else if (type == 2)
    {
        _mt_move_yx(conn, OVERLOAD_USER_MSG); _mt_clrtoeol(conn);
        _buff_write(conn, OVERLOAD_USER_MSG, sizeof(OVERLOAD_USER_MSG)-1);
    } 
    else {
        assert(false);
        _mt_move_yx(conn, OVERLOAD_CPU_MSG); _mt_clrtoeol(conn);
        _buff_write(conn, OVERLOAD_CPU_MSG, sizeof(OVERLOAD_CPU_MSG)-1);
    }
}

///////////////////////////////////////////////////////////////////////
// BBS Logic

#define REGULAR_CHECK_DURATION (5)

static void
regular_check()
{
    // cache results
    static time_t last_check_time = 0;

    time_t now = time(0);

    if ( now - last_check_time < REGULAR_CHECK_DURATION)
        return;

    last_check_time = now;
    g_overload = 0;
    g_banned   = 0;

    if (cpuload(NULL) > MAX_CPULOAD)
    {
        g_overload = 1;
    }
    else if (SHM->UTMPnumber >= MAX_ACTIVE
#ifdef DYMAX_ACTIVE
            || (SHM->GV2.e.dymaxactive > 2000 &&
                SHM->UTMPnumber >= SHM->GV2.e.dymaxactive)
#endif
        )
    {
        ++SHM->GV2.e.toomanyusers;
        g_overload = 2;
    }

    if (dashf(FN_BAN))
    {
        g_banned = 1;
        load_text_screen_file(FN_BAN, &ban_screen);
    }
}

static int 
check_banip(char *host)
{
    uint32_t thisip = ipstr2int(host);
    return uintbsearch(thisip, &banip[1], banip[0]) ? 1 : 0;
}

static const char *
auth_is_free_userid(const char *userid)
{
#if defined(STR_GUEST) && !defined(NO_GUEST_ACCOUNT_REG)
    if (strcasecmp(userid, STR_GUEST) == 0)
        return STR_GUEST;
#endif

#ifdef STR_REGNEW 
    if (strcasecmp(userid, STR_REGNEW) == 0)
        return STR_REGNEW;
#endif

    return NULL;
}

// NOTE ctx->passwd will be destroyed (must > PASSLEN+1)
// NOTE ctx->userid may be changed (must > IDLEN+1)
static int
auth_user_challenge(login_ctx *ctx)
{
    char *uid = ctx->userid,
         *passbuf = ctx->passwd;

    const char *free_uid = auth_is_free_userid(uid);

    if (free_uid)
    {
        strlcpy(ctx->userid, free_uid, sizeof(ctx->userid));
        return AUTH_RESULT_FREEID;
    }

    // reuse cuser
    memset(&cuser, 0, sizeof(cuser));
    if( initcuser(uid) < 1 || !cuser.userid[0] ||
        !checkpasswd(cuser.passwd, passbuf) )
    {
        if (cuser.userid[0])
            strcpy(uid, cuser.userid);
        return AUTH_RESULT_FAIL;
    }

    // normalize user id
    strcpy(uid, cuser.userid);
    return AUTH_RESULT_OK;
}

static int 
start_service(int fd, login_ctx *ctx)
{
    login_data ld = {0};
    int ack = 0;

    if (!g_tunnel)
        return 0;

    ld.cb = sizeof(ld);
    strlcpy(ld.userid, ctx->userid, sizeof(ld.userid));
    strlcpy(ld.hostip, ctx->hostip, sizeof(ld.hostip));
    ld.encoding = ctx->encoding;
    ld.client_code = ctx->client_code;
    ld.t_lines  = 25;
    ld.t_cols   = 80;
    if (ctx->t_lines > ld.t_lines)
        ld.t_lines = ctx->t_lines;
    if (ctx->t_cols > ld.t_cols)
        ld.t_cols = ctx->t_cols;

    fprintf(stderr, "start new service!\r\n");

    // deliver the fd to hosting service
    if (send_remote_fd(g_tunnel, fd) < 0)
        return ack;
   
    // deliver the login data to hosting servier
    if (towrite(g_tunnel, &ld, sizeof(ld)) <= 0)
        return ack;

    // wait service to response
    read(g_tunnel, &ack, sizeof(ack));
    return ack;
}

static int 
auth_start(int fd, login_conn_ctx *conn)
{
    login_ctx *ctx = &conn->ctx;
    int isfree = 1, was_valid_uid = 0;
    draw_check_passwd(conn);

    if (is_validuserid(ctx->userid))
    {
        // ctx content may be changed.
        was_valid_uid = 1;
        switch (auth_user_challenge(ctx))
        {
            case AUTH_RESULT_FAIL:
                logattempt(ctx->userid , '-', time(0), ctx->hostip);
                break;

            case AUTH_RESULT_OK:
                isfree = 0;
                logattempt(ctx->userid , ' ', time(0), ctx->hostip);
                // share FREEID case, no break here!
            case AUTH_RESULT_FREEID:
                draw_auth_success(conn, isfree);

                if (!start_service(fd, ctx))
                {
                    // too bad, we can't start service.
                    draw_service_failure(conn);
                    return AUTH_RESULT_STOP;
                }
                return AUTH_RESULT_OK;

            default:
                assert(false);
                break;
        }

    }

    // auth fail.
    _mt_bell(conn);

    // if fail, restart
    if (login_ctx_retry(ctx) >= LOGINATTEMPTS)
    {
        // end retry.
        draw_goodbye(conn);
        return AUTH_RESULT_STOP;

    }

    // prompt for retry
    if (was_valid_uid)
        draw_auth_fail(conn);
    else
        draw_empty_userid_warn(conn);

    ctx->state = LOGIN_STATE_USERID;
    draw_userid_prompt(conn);
    return AUTH_RESULT_RETRY;
}



///////////////////////////////////////////////////////////////////////
// Event callbacks

static struct event ev_sighup, ev_tunnel;

static void 
sighup_cb(int signal, short event, void *arg)
{
    fprintf(stderr, "caught sighup (request to reload)\r\n");
    g_reload_data = 1;
}

static void 
endconn_cb(int fd, short event, void *arg)
{
    login_conn_ctx *conn = (login_conn_ctx*) arg;
    fprintf(stderr, "login_conn_remove: removed connection #%d...", fd);
    event_del(&conn->ev);
    bufferevent_free(conn->bufev);
    close(fd);
    free(conn);
    fprintf(stderr, " done.\r\n");
}

static void 
login_conn_remove(login_conn_ctx *conn, int fd, int sleep_sec)
{
    if (!sleep_sec)
    {
        endconn_cb(fd, EV_TIMEOUT, (void*) conn);
    } else {
        struct timeval tv = { sleep_sec, 0};
        event_del(&conn->ev);
        event_set(&conn->ev, fd, EV_PERSIST, endconn_cb, conn);
        event_add(&conn->ev, &tv);
        fprintf(stderr, 
                "login_conn_remove: stop conn #%d in %d seconds later.\r\n", 
                fd, sleep_sec);
    }
}

static void 
client_cb(int fd, short event, void *arg)
{
    login_conn_ctx *conn = (login_conn_ctx*) arg;
    int i, len, r;
    unsigned char buf[32], ch;

    // ignore clients that timeout
    if (event & EV_TIMEOUT)
        return;

    // XXX will this happen?
    if (!(event & EV_READ))
        return;

    if ( (len = read(fd, buf, sizeof(buf) - 1)) <= 0)
    {
         if (errno == EINTR || errno == EAGAIN)
             return;

        // close connection
        login_conn_remove(conn, fd, 0);
        return;
    }

    len = telnet_process(&conn->telnet, buf, len);

    for (i = 0; i < len; i++)
    {
        int c = (unsigned char)buf[i];
        
        // quick convert
        if      (c == KEY_CR) 
            c = KEY_ENTER;
        else if (c == KEY_LF) 
            continue;   // ignore LF
        else if (c == '\b' || c == Ctrl('H') || c == 127) 
            c = KEY_BS;

        // deal with context
        switch ( login_ctx_handle(&conn->ctx, c) )
        {
            case LOGIN_HANDLE_WAIT:
                break;

            case LOGIN_HANDLE_BEEP:
                _mt_bell(conn);
                break;

            case LOGIN_HANDLE_BS:
                _mt_bs(conn);
                break;

            case LOGIN_HANDLE_OUTC:
                ch = c;
                _buff_write(conn, &ch, 1);
                break;

            case LOGIN_HANDLE_PROMPT_PASSWD:
                // because prompt would reverse attribute, reset here.
                draw_userid_prompt_end(conn);

#ifdef DETECT_CLIENT
                // stop detection
                conn->telnet.cc_arg = NULL;
#endif
                if (conn->ctx.userid[0])
                {
                    char *uid = conn->ctx.userid;
                    char *uid_lastc = uid + strlen(uid)-1;

                    draw_passwd_prompt(conn);
#ifdef CONVERT
                    // convert encoding if required
                    switch(*uid_lastc)
                    {
                        case '.':   // GB mode
                            conn->ctx.encoding = CONV_GB;
                            *uid_lastc = 0;
                            break;
                        case ',':   // UTF-8 mode
                            conn->ctx.encoding = CONV_UTF8;
                            *uid_lastc = 0;
                            break;
                    }
#endif
                    // accounts except free_auth [guest / new]
                    // require passwd.
                    if (!auth_is_free_userid(uid))
                        break;
                }

                // force changing state
                conn->ctx.state = LOGIN_STATE_AUTH;
                // XXX share start auth, no break here.
            case LOGIN_HANDLE_START_AUTH:
                if ((r = auth_start(fd, conn)) != AUTH_RESULT_RETRY)
                {
                    login_conn_remove(conn, fd,
                            (r == AUTH_RESULT_OK) ? 0 : AUTHFAIL_SLEEP_SEC);
                    return;
                }
                break;
        }
    }
}

static void 
listen_cb(int fd, short event, void *arg)
{
    int cfd;
    struct sockaddr_in xsin = {0};
    socklen_t szxsin = sizeof(xsin);
    login_conn_ctx *conn;

    if ((cfd = accept(fd, (struct sockaddr *)&xsin, &szxsin)) < 0 )
        return;

    if ((conn = malloc(sizeof(login_conn_ctx))) == NULL) {
        close(cfd);
        return;
    }
    memset(conn, 0, sizeof(login_conn_ctx));

    if ((conn->bufev = bufferevent_new(cfd, NULL, NULL, NULL, NULL)) == NULL) {
        free(conn);
        close(cfd);
        return;
    }

    reload_data();
    login_ctx_init(&conn->ctx);

    // initialize telnet protocol
    telnet_ctx_init(&conn->telnet, &telnet_callback, cfd);
    telnet_ctx_set_write_arg (&conn->telnet, (void*) conn); // use conn for buffered events
    telnet_ctx_set_resize_arg(&conn->telnet, (void*) &conn->ctx);
#ifdef DETECT_CLIENT
    telnet_ctx_set_cc_arg(&conn->telnet, (void*) &conn->ctx);
#endif
    // better send after all parameters were set
    telnet_ctx_send_init_cmds(&conn->telnet);

    // getremotename
    inet_ntop(AF_INET, &xsin.sin_addr, conn->ctx.hostip, sizeof(conn->ctx.hostip));

    // set events
    event_set(&conn->ev, cfd, EV_READ|EV_PERSIST, client_cb, conn);
    event_add(&conn->ev, NULL);

    // check ban here?  XXX can we directly use xsin.sin_addr instead of ASCII form?
    if (g_banned || check_banip(conn->ctx.hostip) )
    {
        // draw ban screen, if available. (for banip, this is empty).
        draw_text_screen(conn, ban_screen);
        login_conn_remove(conn, fd, BAN_SLEEP_SEC);
        return;
    }

    // draw banner
#ifdef INSCREEN
    _buff_write(conn, INSCREEN, sizeof(INSCREEN));
#endif

    // XXX check system load here.
    if (g_overload)
    {
        draw_overload(conn, g_overload);
        login_conn_remove(conn, fd, OVERLOAD_SLEEP_SEC);
        return;

    } else {
        draw_text_screen(conn, welcome_screen);
        draw_userid_prompt(conn);
    }
}

static void 
tunnel_cb(int fd, short event, void *arg)
{
    int cfd;
    if ((cfd = accept(fd, NULL, NULL)) < 0 )
        return;

    _set_connection_opt(cfd);

    // got new tunnel
    if (g_tunnel) 
        close(g_tunnel);
    g_tunnel = cfd;
}

///////////////////////////////////////////////////////////////////////
// Main 

#ifndef LOGIND_TUNNEL_PATH
#define LOGIND_TUNNEL_PATH BBSHOME "/run/logind.tunnel"
#endif

#ifndef FN_BINDLIST
#define FN_BINDLIST         BBSHOME "/etc/logind_ports"  // a file with list of ports to bind.
#endif

static int 
bind_port(int port)
{
    char buf[STRLEN];
    int sfd;
    struct event *pev_listen = NULL;

    snprintf(buf, sizeof(buf), "*:%d", port);

    fprintf(stderr,"binding to port: %d...", port);
    if ( (sfd = tobindex(buf, SOCKET_QLEN, _set_bind_opt, 1)) < 0 )
    {
        fprintf(stderr, "cannot bind to port: %d. abort.\r\n", port);
        return -1;
    }
    pev_listen = malloc (sizeof(struct event));
    assert(pev_listen);

    event_set(pev_listen, sfd, EV_READ | EV_PERSIST, listen_cb, pev_listen);
    event_add(pev_listen, NULL);
    fprintf(stderr,"ok. \r\n");
    return 0;
}

int 
main(int argc, char *argv[])
{
    int     ch, port = 0, bound_ports = 0, tfd, as_daemon = 1;
    FILE   *fp;
    const char *tunnel_path = LOGIND_TUNNEL_PATH;
    const char *config_file = FN_BINDLIST;


    Signal(SIGPIPE, SIG_IGN);

    while ( (ch = getopt(argc, argv, "f:p:t:hD")) != -1 )
    {
        switch( ch ){
        case 'f':
            config_file = optarg;
            break;
        case 'p':
            if (optarg) port = atoi(optarg);
            break;
        case 't':
            tunnel_path = optarg;
            break;
        case 'D':
            as_daemon = 0;
            break;
        case 'h':
        default:
            fprintf(stderr, "usage: %s [-D] [-f bindlist_file] [-p port] [-t tunnel_path]\r\n", argv[0]);
            return 1;
        }
    }

    chdir(BBSHOME);
    attach_SHM();

    reload_data();

    if (as_daemon)
    {
        fprintf(stderr, "start daemonize\r\n");
        daemonize(BBSHOME "/run/logind.pid", NULL);
    }

    event_init();
    signal_set(&ev_sighup, SIGHUP, sighup_cb, &ev_sighup);
    signal_add(&ev_sighup, NULL);

    // bind ports
    if (port && bind_port(port) < 0)
    {
        fprintf(stderr, "cannot bind to port: %d. abort.\r\n", port);
        return 3;
    }
    if (port)
        bound_ports++;

    // bind from port list file
    if( NULL != (fp = fopen(config_file, "rt")) )
    {
        char buf [STRLEN];
        while (fgets(buf, sizeof(buf), fp))
        {
            port = atoi(strtok(buf, " #\r\n"));
            if (!port)
                continue;

            if (bind_port(port) < 0)
            {
                fprintf(stderr, "cannot bind to port: %d. abort.\r\n", port);
                return 3;
            }
            bound_ports++;
        }
        fclose(fp);
    }

    if (!bound_ports)
    {
        fprintf(stderr, "error: no ports to bind. abort.\r\n");
        return 4;
    }

    /* Give up root privileges: no way back from here */
    setgid(BBSGID);
    setuid(BBSUID);

    // create tunnel
    if ( (tfd = tobindex(tunnel_path, 1, _set_bind_opt, 1)) < 0)
    {
        fprintf(stderr, "cannot create tunnel: %s. abort.\r\n", tunnel_path);
        return 2;
    }
    event_set(&ev_tunnel, tfd, EV_READ | EV_PERSIST, tunnel_cb, &ev_tunnel);
    event_add(&ev_tunnel, NULL);

    fprintf(stderr, "start event dispatch.\r\n");
    event_dispatch();

    return 0;
}

// vim:et