Search

Thursday, September 15, 2016

Free Online Public FORTUNE - COWSAY Server : Fun with TELNET and C/C++ Linux Multi-threaded Socket Server Programming (CentOS 7)

 ________________________________________
/ Necessity is the plea for every        \
| infringement of human freedom. It is   |
| the argument of tyrants; it is the     |
\ creed of slaves. -- William Pitt, 1783 /
 ----------------------------------------
    \               ,-----._
  .  \         .  ,'        `-.__,------._
 //   \      __\\'                        `-.
((    _____-'___))                           |
 `:='/     (alf_/                            |
 `.=|      |='                               |
    |)   O |                                  \
    |      |                               /\  \
    |     /                          .    /  \  \
    |    .-..__            ___   .--' \  |\   \  |
   |o o  |     ``--.___.  /   `-'      \  \\   \ |
    `--''        '  .' / /             |  | |   | \
                 |  | / /              |  | |   mmm
                 |  ||  |              | /| |
                 ( .' \ \              || | |
                 | |   \ \            // / /
                 | |    \ \          || |_|
                /  |    |_/         /_|
               /__/


Open a terminal or a command prompt, enter telnet sanyalnet-cloud-vps.freeddns.org and press Enter again quickly. A random fortune cookie message will be returned to you, formatted by cowsay.


Do it again. A different random cowsay formatted fortune cookie will be presented to you.


These are delivered by a little Linux server daemon "cowsayd" I wrote in C for really no good reason in the spirit of fortune and cowsay. Running on my CentOS 7 hobbyist VPS, the publicly accessible daemon serves cookies on the standard TELNET TCP port 23 from the Linux fortune database, which is based on Ken Arnold's BSD implementation.

After a connection is received and accepted, I require some input from the client (just pressing the Enter key suffices) before sending back a cowsay formatted fortune cookie. This is an attempt at gracefully handling the continuous connections from automated bots which are probably looking for a "username:", "login:" or some such prompt and rarely send any inputs. So I hope mostly humans will read what is shown on the screen ("Enter anything to hear the cow speak.") and react correctly to receive a fortune-cowsay message for fun.

The C / C++ program is based off my Brain-damaged http 410 server with the same features as discussed there, but with a couple of important differences:
  • This fortune/cowsay server does not need to listen to connections from more than one port, as it servers clients on just one port. Hence, the select() system call and associated family of macros are not needed by the fortune+cowsay server.
  • The server works by invoking a shell command that in turn executes fortune and cowsay and returns the output back to the server, which passes it on to the client and closes the connection. The shell command is invoked using popen() which is thread-safe in Linux, and works well for getting a message from fortune/cowsay.
  • The use of pthread_attr_setdetachstate with PTHREAD_CREATE_DETACHED is demonstrated. This server spawns a thread to service every connection and the spawned thread exits after servicing a connection. The main thread never waits to join the spawned threads. If we do not create detached threads for this scenario, memory usage will keep increasing every time a thread is spawned since the default attribute of PTHREAD_CREATE_JOINABLE will never release all service thread resources becuase it expecting the main thread to join and ask for the return value.
  • Obtaining the client IP address is demonstrated using the inet_ntop() function.
  • It logs into the same file as sshd (usually /var/log/secure) in a format designed for abusers to be picked up and banned by fail2ban if installed. fail2ban is configured on my server to also automatically email blocklist.de for more general dissemination of telnet spammers. I also maintain a blocklist file free for public use.
Both fortune and cowsay need be installed and working for this server to deliver the messages. I describe how to install them later in this article (see "How to install Fortune and Cowsay on CentOS 7" below.)

To compile the cowsayd server, we need to include the -lpthread switch on the gcc command line, since it is a multi-threaded server.

  • gcc -o cowsayd -lpthread cowsayd.c

    Once compiled, copy the binary to the directory  /usr/local/bin.

    The server is designed to run continuously. Starting it up at boot time is done by a custom init script in the /etc/init.d directory. The server logs to syslog, check /var/log/messages for log output for troubleshooting,


    Here is the complete source code of cowsayd.c.

    /* +++
    Supratim Sanyal's COWSAY server
    - If a connection is made to its network port, and if fortune and cowsay are
    - installed, this waits for some input and returns a random fortune cookie formatted by cowsay
    -
    - to build: gcc -o cowsayd -lpthread cowsayd.c
    - on Centos 7, install fortune with yum install fortune-mod
    - and install cowsay from rpm at http://www.melvilletheatre.com/articles/el7/
    -
    - derived from Brain Damaged web server (http://supratim-sanyal.blogspot.com/2016/07/httpd410server-tiny-free-web-server-to.html)
    -
    - I can be reached at http://mcaf.ee/sdlg9f
    - Posted under GPL 3.0 License - use and modify freely but retain this header
    --- */
    #include<assert.h>
    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    #include<sys/socket.h>
    #include<sys/types.h>
    #include<arpa/inet.h>
    #include<unistd.h>
    #include<pthread.h>
    #include<sys/time.h>
    #include<syslog.h>
    #include<errno.h>
    #include<fcntl.h>
    #include<netinet/tcp.h>
    #include<netinet/in.h>
    static const int MAXCLIENTS=10; // Should be less than /proc/sys/net/ipv4/tcp_max_syn_backlog
    static const int UID=99; // this uid is set for this program after bind() for security - 99 is nobody
    static const int GID=99; // this gid is set for this program after bind() for security - 99 is nobody
    static const int RECV_TIMEOUT=3; // seconds to wait for client to send something after connecting
    static const int MAX_RECV_RETRIES=3; // let recv timeout this many times before closing connection
    static const int TCP_PORT_NUMBER=23; // listen on telnet port
    static const int ZERO=0; // useful in setting socket options
    static const int ONE=1; // useful in setting socket options
    static const char *const banner1="COWSAY SERVER AT sanyalnet-cloud-vps.freeddns.org\n\n";
    static const char *const banner2="Enter anything to hear the cow speak.\n";
    int numclients=0;
    char cowsayresp[1024]="\0";
    const char *const shell_command="if [[ $(which cowsay > /dev/null ; echo $?) -eq 0 ]] && [[ $(which fortune > /dev/null ; echo $?) -eq 0 ]] ; then cowsay -f $(ls $(cowsay -l | awk 'NR==1 {print $4}' | sed 's/://') | shuf -n1) $(fortune); echo; fi";
    int throttle(const struct in_addr *const sin_addr, const unsigned short *const sin_port);
    //mutexes and thread functions
    pthread_mutex_t clientcount_mutex, shell_exec_mutex, param_mutex;
    pthread_attr_t thread_attr;
    pthread_t thread_id;
    void *connection_handler(void *);
    int cowsayexec()
    {
    FILE *fp;
    char c;
    int i=0,retval=0;
    fp = popen(shell_command, "r");
    if (NULL==fp)
    {
    syslog(LOG_NOTICE,"Failed to open pipe to command [%s]; error: %d/%s",shell_command, errno,strerror(errno));
    retval=-1;
    }
    else
    {
    memset(cowsayresp,0,sizeof(cowsayresp));
    for(i=0;i<(sizeof(cowsayresp)-1);i++)
    {
    if(EOF==(c=getc(fp)))
    {
    cowsayresp[i]='\0';
    break;
    }
    else
    {
    cowsayresp[i]=c;
    }
    } //for
    } //else
    pclose(fp);
    return retval;
    } //cowsayexec()
    int main(int argc , char *argv[])
    {
    int socket_desc, client_sock, c, flags;
    long long int totalserved=0; // 64 bit integer
    struct sockaddr_in server, client;
    char addrbuf[INET_ADDRSTRLEN]; /* defined in <netinet/in.h> */
    // We will enforce a timeout for clients to send something after connecting
    struct timeval recv_to_tv;
    recv_to_tv.tv_sec = RECV_TIMEOUT;
    recv_to_tv.tv_usec = 0;
    setlogmask (LOG_UPTO (LOG_INFO));
    openlog ("COWSAYD", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);
    syslog(LOG_NOTICE,"%s starting up",argv[0]);
    printf("%s: see system log for messages\n",argv[0]);
    if (pthread_mutex_init(&clientcount_mutex, NULL) != 0)
    {
    syslog(LOG_NOTICE,"failed to init clientcount_mutex: %s",strerror(errno));
    closelog();
    exit(1);
    }
    if (pthread_mutex_init(&shell_exec_mutex, NULL) != 0)
    {
    syslog(LOG_NOTICE,"failed to init shell_exec_mutex: %s",strerror(errno));
    closelog();
    exit(1);
    }
    if (pthread_mutex_init(&param_mutex, NULL) != 0)
    {
    syslog(LOG_NOTICE,"failed to init param_mutex: %s",strerror(errno));
    closelog();
    exit(1);
    }
    // Our service threads will be detached threads
    // If we don't do this, threads will default to PTHREAD_CREATE_JOINABLE which
    // will keep using up memory because the thread resources are not released at
    // thread exit since they are expecting to be joined by the main thread to
    // retrieve return status etc.
    if(pthread_attr_init(&thread_attr) != 0)
    {
    syslog(LOG_NOTICE,"failed to init thread_attr: %s",strerror(errno));
    closelog();
    exit(1);
    }
    if(pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED)!= 0)
    {
    syslog(LOG_NOTICE,"failed to set detached thread attr: %s",strerror(errno));
    closelog();
    exit(1);
    }
    //Create socket
    socket_desc = socket(AF_INET , SOCK_STREAM , 0);
    if (socket_desc == -1)
    {
    syslog(LOG_NOTICE,"Could not create socket: %s", strerror(errno));
    closelog();
    exit(1);
    }
    //Prepare the sockaddr_in structure
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons( TCP_PORT_NUMBER );
    if (setsockopt(socket_desc, SOL_SOCKET, SO_REUSEADDR, &ONE, sizeof(int)) < 0)
    {
    syslog(LOG_NOTICE,"Could not set SO_REUSEADDR on socket: %s", strerror(errno));
    }
    //Bind
    if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0)
    {
    syslog(LOG_NOTICE,"Could not bind socket: %s", strerror(errno));
    closelog();
    exit(1);
    }
    if(0!=setgid(GID)) // set gid first because it cannot be done after setuid
    {
    syslog(LOG_NOTICE,"Could not setgid to [%d], proceeding regardless: %d[%s]", GID, errno, strerror(errno));
    }
    else
    {
    syslog(LOG_NOTICE,"setgid to [%d] ok", GID);
    }
    if(0!=setuid(UID))
    {
    syslog(LOG_NOTICE,"Could not setuid to [%d], proceeding regardless: %d[%s]", UID, errno, strerror(errno));
    }
    else
    {
    syslog(LOG_NOTICE,"setuid to [%d] ok", UID);
    }
    //Listen and Accept
    listen(socket_desc, 1+MAXCLIENTS); // keep queue size small to try avoid DOS flood; maximum is in proc/sys/net/ipv4/tcp_max_syn_backlog
    c = sizeof(struct sockaddr_in);
    while(client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c))
    {
    if (client_sock < 0)
    {
    syslog(LOG_NOTICE,"Could not accept connection: %s", strerror(errno));
    shutdown (socket_desc, SHUT_RDWR);
    close(socket_desc);
    closelog();
    exit(1);
    }
    memset(addrbuf,0,sizeof(addrbuf));
    if (NULL==inet_ntop(AF_INET, &client.sin_addr, addrbuf, INET_ADDRSTRLEN))
    {
    syslog(LOG_NOTICE,"Could not get client address: %d[%s]", errno,strerror(errno));
    shutdown (client_sock, SHUT_RDWR);
    close(client_sock);
    }
    else
    {
    totalserved++;
    syslog(LOG_NOTICE,"ACCEPTED Connection %lld from IP %s PORT %d", totalserved, addrbuf, ntohs(client.sin_port));
    // The following lines sends an entry that looks like SSH logon failure to syslogd, to be picked up
    // by fail2ban if configured, which will ban the ip and report to blocklist.de for multiple connections quickly
    // from same IP (i.e. source is telnet DOS/spam)
    // Aug 28 21:04:36 sanyalnet-cloud-vps sshd[17400]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=static-200-105-171-226.acelerate.net
    closelog();
    openlog ("sshd", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);
    syslog(LOG_AUTHPRIV|LOG_WARNING,"pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=telnet ruser= rhost=%s", addrbuf);
    closelog();
    openlog ("COWSAYD", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);
    pthread_mutex_lock(&param_mutex); // Lock the client_sock descriptor until connection_handler thread
    // has copied to local variable
    // set receive timeout on the accepted connection
    setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&recv_to_tv,sizeof(struct timeval));
    if(throttle(&client.sin_addr,&client.sin_port)) // TBD ... some sort of anti-DOS throttle for IP address would be nice...
    {
    syslog(LOG_NOTICE,"THROTTLED IP: %s PORT %d", addrbuf, ntohs(client.sin_port));
    shutdown (client_sock, SHUT_RDWR);
    close(client_sock);
    }
    else if( pthread_create( &thread_id , &thread_attr, connection_handler , (void*) &client_sock) < 0)
    {
    syslog(LOG_NOTICE,"Could not create service thread: %d[%s]", errno, strerror(errno));
    shutdown (client_sock, SHUT_RDWR);
    close(client_sock);
    }
    }
    } //while
    exit(0); // unreachable
    } //main()
    // Handle each connection in this created thread
    void *connection_handler(void *socket_desc)
    {
    //Get the socket descriptor
    int sock = *(int*)socket_desc;
    int i=0,read_size=0, thisclient=0, myerrno=0, recv_tries=0;
    char client_message[128]="\0";
    memset(client_message,0,sizeof(client_message));
    pthread_mutex_unlock(&param_mutex);
    pthread_mutex_lock(&clientcount_mutex);
    numclients++;
    thisclient=numclients;
    pthread_mutex_unlock(&clientcount_mutex);
    if(thisclient>MAXCLIENTS) // Client numbers start at 1 due to ++ above
    {
    syslog(LOG_NOTICE,"Active Clients %d FD %d | Server full, MAXCLIENTS reached",thisclient,sock);
    }
    else
    {
    // Send the banners
    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *) &ONE, sizeof(int)); // setting TCP_NODELAY on send
    if(send(sock,banner1,strlen(banner1),0)<0)
    {
    syslog(LOG_NOTICE,"Active Clients %d FD %d | Could not send banner 1: %d/%s",thisclient,sock,errno,strerror(errno));
    }
    else
    {
    if(send(sock,banner2,strlen(banner2),0)<0)
    {
    syslog(LOG_NOTICE,"Active Clients %d FD %d | Could not send banner 2: %d/%s",thisclient,sock,errno,strerror(errno));
    }
    }
    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *) &ZERO, sizeof(int));
    //read and log whatever the client sends
    recv_tries=0;
    do
    {
    memset(client_message,0,sizeof(client_message));
    read_size = recv(sock, client_message, sizeof(client_message)-1, 0);
    myerrno=errno;
    if(read_size < 0)
    {
    if (EAGAIN != myerrno)
    {
    syslog(LOG_NOTICE,"Active Clients %d FD %d | Could not read message: recv error %d",thisclient,sock,myerrno);
    read_size=0;
    break;
    }
    else
    {
    syslog(LOG_NOTICE,"Active Clients %d FD %d | no data yet",thisclient,sock);
    recv_tries++;
    if(recv_tries>=MAX_RECV_RETRIES)
    {
    syslog(LOG_NOTICE,"Active Clients %d FD %d | max recv retries reached, nothing received ",thisclient,sock);
    read_size=0;
    }
    }
    }
    } while(read_size<0);
    if(read_size<=0) // read_size is zero here - empty message or unexpected client disconnet - do nothing
    {
    // bots which do not send anything after connect are not served a cowsay message
    syslog(LOG_NOTICE,"Active Clients %d FD %d | recvd zero bytes", thisclient,sock);
    }
    else
    {
    if(read_size>sizeof(client_message))read_size=sizeof(client_message);
    client_message[read_size-1]='\0'; // just some unnecessary defensive coding!
    for(i=0;i<strlen(client_message);i++) if( ('\n'==client_message[i])||('\r'==client_message[i]) ) client_message[i]=' '; // Replace tabs and returns with space for logging
    syslog(LOG_NOTICE,"Active Clients %d FD %d | RECEIVED: [%s]",thisclient,sock,client_message);
    pthread_mutex_lock(&shell_exec_mutex); // One expensive shell exec at a time please
    if(0==cowsayexec()) // get the wisdom
    {
    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *) &ONE, sizeof(int)); // setting TCP_NODELAY on send
    if(send(sock,cowsayresp,strlen(cowsayresp),0)<0)
    {
    syslog(LOG_NOTICE,"Active Clients %d FD %d | Could not send response: %d/%s",thisclient,sock,errno,strerror(errno));
    }
    else
    {
    syslog(LOG_NOTICE,"Sent Cowsay [%s] to FD %d",cowsayresp,sock);
    }
    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *) &ZERO, sizeof(int));
    }
    pthread_mutex_unlock(&shell_exec_mutex);
    }
    }
    syslog(LOG_NOTICE,"Active Clients %d FD %d | Goodbye", thisclient,sock);
    shutdown (sock, SHUT_RDWR);
    close(sock);
    pthread_mutex_lock(&clientcount_mutex);
    numclients--;
    pthread_mutex_unlock(&clientcount_mutex);
    return;
    } //connection_handler()
    // TBD - return 1 if IP/Port is to be throttled based on DOS attempt or abuse, 0 if not
    int throttle(const struct in_addr *const sin_addr, const unsigned short *const sin_port)
    {
    return 0;
    }
    view raw cowsayd.c hosted with ❤ by GitHub


    The init script I use to start, restart or stop the cowsayd daemon follows. Copy this to /etc/init.d/ and make it executable using chmod +x /etc/init.d/cowsayd and then chkconfig --add cowsayd to add it to the chkconfig daemon management system. If correctly done, chkconfig --list will show the cowsayd daemon and the runlevels it is enabled for. chkconfig cowsayd on will enable starting up the daemon on reboot.

    The /etc/init.d/cowsayd script is below:

    #!/bin/sh
    #
    # cowsayd Start/Stop the cowsayd daemon.
    #
    # chkconfig: 2345 90 60
    # description: cowsayd is a minimal telnet server to return a fortune+cowsay cookie and exit
    # Supratim Sanyal - supratim at riseup dot net
    # Copy this to /etc/init.d, chmod +x and chkconfig --add.
    #
    # Source function library.
    . /etc/init.d/functions
    PIDFILE=/var/run/cowsayd.pid
    start() {
    echo -n "Starting cowsayd"
    if [ -f $PIDFILE ]; then
    PID=`cat $PIDFILE`
    echo already running: $PID
    exit 2;
    else
    #daemon --user=root --pidfile=$PIDFILE exec /usr/local/bin/cowsayd>/dev/null 2>&1 &
    daemon --check cowsayd --user=root --pidfile=$PIDFILE /usr/local/bin/cowsayd>/dev/null 2>&1 &
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && touch /var/lock/subsys/cowsayd
    return $RETVAL
    fi
    }
    stop() {
    echo -n "Shutting down cowsayd"
    echo
    killproc cowsayd
    echo
    rm -f /var/lock/subsys/cowsayd
    return 0
    }
    case "$1" in
    start)
    start
    ;;
    stop)
    stop
    ;;
    status)
    status cowsayd
    ;;
    restart)
    stop
    start
    ;;
    *)
    echo "Usage: {start|stop|status|restart}"
    exit 1
    ;;
    esac
    exit $?
    view raw cowsayd hosted with ❤ by GitHub


    HOW TO INSTALL FORTUNE and COWSAY on CentOS 7

     _________________________________________
    / Did you hear that Captain Crunch, Sugar \
    | Bear, Tony the Tiger, and Snap, Crackle |
    | and Pop were all murdered recently...   |
    | Police suspect the work of a cereal     |
    \ killer!                                 /
     -----------------------------------------
        \               ,-----._
      .  \         .  ,'        `-.__,------._
     //   \      __\\'                        `-.
    ((    _____-'___))                           |
     `:='/     (alf_/                            |
     `.=|      |='                               |
        |)   O |                                  \
        |      |                               /\  \
        |     /                          .    /  \  \
        |    .-..__            ___   .--' \  |\   \  |
       |o o  |     ``--.___.  /   `-'      \  \\   \ |
        `--''        '  .' / /             |  | |   | \
                     |  | / /              |  | |   mmm
                     |  ||  |              | /| |
                     ( .' \ \              || | |
                     | |   \ \            // / /
                     | |    \ \          || |_|
                    /  |    |_/         /_|
                   /__/


    All CentOS package installations are usually via the yum tool. Use yum install <rpm-filename> to install RPM packages.

    The Fortune RPM fortune-mod-1.99.1-17.el7.x86_64.rpm is easily available from the EPEL repository. If you have not added the EPEL repository already follow these steps:

    • head over to http://dl.fedoraproject.org/pub/epel/7/x86_64/e and download the latest epel-release*.rpm  (epel-release-7-8.noarch.rpm at the time of writing).
    • Install and enable the EPEL release repository: 
      • rpm -Uvh epel-release*rpm
    With EPEL repository enabled, fortune can be simply installed using:
    • yum install fortune-mod
    Cowsay is implemented in perl, and is therefore a set of perl scripts with no dependency on architecture as long as there is a usable perl interpreter installed, which should be the case with most installations of CentOS. If perl is not available, install perl first using yum install perl.

    To install cowsay, you need cowsay-3.03-20.el7.noarch.rpm, and optionally cowsay-beefymiracle-1.0-6.fc24.noarch.rpm and cowsay-morecows-1.0-11.4.casjay.el7.x86_64.rpm. The latter two provide various additional cows and other assorted animal art.

    With the EPEL repository enabled, cowsay can be easily installed using:

    • yum install cowsay

    Then yum install the beefymiracle and morecows RPMs - if the links above do not work, use the download link to my google drive at the bottom of this post.

    HOW TO ENABLE FORTUNE - COWSAY MESSAGE ON LOGIN

     _________________________________________
    / We are preparing to think about         \
    | contemplating preliminary work on plans |
    | to develop a schedule for producing the |
    | 10th Edition of the Unix Programmers    |
    \ Manual. -- Andrew Hume                  /
     -----------------------------------------
     \                   .,
       \         .      .TR   d'
         \      k,l    .R.b  .t .Je
           \   .P q.   a|.b .f .Z%
               .b .h  .E` # J: 2`     .
          .,.a .E  ,L.M'  ?:b `| ..J9!`.,
           q,.h.M`   `..,   ..,""` ..2"`
           .M, J8`   `:       `   3;
       .    Jk              ...,   `^7"90c.
        j,  ,!     .7"'`j,.|   .n.   ...
       j, 7'     .r`     4:      L   `...
      ..,m.      J`    ..,|..    J`  7TWi
      ..JJ,.:    %    oo      ,. ....,
        .,E      3     7`g.M:    P  41
       JT7"'      O.   .J,;     ``  V"7N.
       G.           ""Q+  .Zu.,!`      Z`
       .9.. .         J&..J!       .  ,:
          7"9a                    JM"!
             .5J.     ..        ..F`
                78a..   `    ..2'
                    J9Ksaw0"'
                   .EJ?A...a.
                   q...g...gi
                  .m...qa..,y:
                  .HQFNB&...mm
                   ,Z|,m.a.,dp
                .,?f` ,E?:"^7b
                `A| . .F^^7'^4,
                 .MMMMMMMMMMMQzna,
             ...f"A.JdT     J:    Jp,
              `JNa..........A....af`
                   `^^^^^'`


    To get a mood-lifting fortune-cowsay message every time you log in, create a file /etc/profile.d/custom.sh with the following contents.

    # /etc/profile.d/custom.sh - get a fortune-cowsay message on logging in
    if [[ $(which cowsay > /dev/null ; echo $?) -eq 0 ]] && [[ $(which fortune > /dev/null ; echo $?) -eq 0 ]] ; then
    cowsay -f $(ls $(cowsay -l | awk 'NR==1 {print $4}' | sed 's/://') | shuf -n1) $(fortune)
    echo
    fi
    view raw custom.sh hosted with ❤ by GitHub


    DOWNLOAD

    You can download everything you need for free, including the cowsayd source, binary, init script, login script and Fortune and Cowsay RPMs for CentOS 7 from my google drive.



    No comments:

    Post a Comment

    "SEO" link builders: move on, your spam link will not get posted.

    Note: Only a member of this blog may post a comment.

    Recommended Products from Amazon