Tag - bash

Entries feed

Tuesday 27 September 2011

Nagios plugin to check SSL certificates

Here a Nagios plugin I wrote for checking SSL certificates for expiry. You can set it up for emitting a WARNING or a CRITICAL state N days before expiry.

/root/bin/nagios-check-crt.sh :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/bin/bash
 
# Written by Alexis Bezverkhyy <alexis@grapsus.net> in september 2011
# This is free and unencumbered software released into the public domain.
# For more information, please refer to <http://unlicense.org/>
 
function PRINT_USAGE(){
  echo "This Nagios plugin checks SSL certificates for expiration :
  -c HOST:PORT host and port to connect
  -d DAYS  minimum days before expiry, otherwise a WARNING is issued
  -D DAYS  minimum days before expiry, otherwise a CRITICAL is issued
  -h    prints out this help"
  exit 0
}
 
CONNECT='';WDAYS=0;CDAYS=0;
declare -i CDAYS 
declare -i WDAYS
while true ; do
  getopts 'c:d:D:h' OPT 
  if [ "$OPT" = '?' ] ; then break; fi; 
  case "$OPT" in
    "c") CONNECT="$OPTARG";;
    "d") WDAYS="$OPTARG";;
    "D") CDAYS="$OPTARG";;
    "h") PRINT_USAGE;;
  esac
done
 
if [ -z "$CONNECT" -o '(' "$WDAYS" = '0' -a "$CDAYS" = '0' ')' ] ; then
  PRINT_USAGE
fi
 
function get_crt_expiry
{
        # connect to host with OpenSSL client, filter CRT, parse CRT,
        # get expiry time, convert to traditionnal y-m-d h:s
        echo -n '' | openssl s_client -connect "$1" 2>/dev/null \
                | awk 'BEGIN { p = 0 }
                                         /BEGIN CERT/ { p = 1 }
                                         { if (p) print $0 }
                                         /END CERT/ { p = 0 }' \
                | openssl asn1parse 2>/dev/null \
                | grep 'UTCTIME' \
                | awk '{ print $7 }' \
                | tr -d 'Z:' \
                | tail -n 1 \
                | sed -r 's/^(..)(..)(..)(..)(..).*$/\1-\2-\3 \4:\5/'
}
 
EXPIRY=$(get_crt_expiry "$CONNECT")
if [ -z "$EXPIRY" ] ; then
        echo "WARNING - cannot get expiry date for $CONNECT"
        exit 1
fi
EPOCH_EXPIRY=$(date -d "$EXPIRY" +%s)
EPOCH_NOW=$(date +%s)
let "REM_DAYS = (EPOCH_EXPIRY - EPOCH_NOW)/(24*3600)"
 
if [ "$CDAYS" -gt 0 -a "$REM_DAYS" -lt "$CDAYS" ] ; then
  echo "CRITICAL - $CONNECT crt expries on $EXPIRY ($REM_DAYS days left)" 
        exit 2
fi
 
if [ "$WDAYS" -gt 0 -a "$REM_DAYS" -lt "$WDAYS" ] ; then
  echo "WARNING - $CONNECT crt expries on $EXPIRY ($REM_DAYS days left)" 
        exit 1
fi
  
echo "OK - $CONNECT crt expries on $EXPIRY ($REM_DAYS days left)"

Here's the configuration to check a simple HTTPS service.

commands.cfg :

1
2
3
4
define command {
  command_name check_crt
  command_line /root/bin/nagios-check-crt.sh -c $ARG1$ -d $ARG2$ -D $ARG3$
}

myhost.cfg :

1
2
3
4
5
6
define service {
        use             generic-service
        host_name  myhost
        service_description HTTPS-CRT
        check_command check_crt!myhost.com:443!60!30
}

Saturday 1 January 2011

A script for splitting videos using ffmpeg

Here is a small bash script for automatically cutting a video file into smaller chunks of fixed length. ffmpeg cannot output multiple files, but it has start offset and duration parameters which my script uses to actually split the file.

For example, if we have a video called 'video.mpeg' which is 3000 seconds long and we run

ffsplit.sh video.mpeg 1200

we will obtain three files : video-001.mpeg (1200 seconds), video-002.mpeg (1200 seconds) and video-003.mpeg (600 seconds).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash
 
# Written by Alexis Bezverkhyy <alexis@grapsus.net> in 2011
# This is free and unencumbered software released into the public domain.
# For more information, please refer to <http://unlicense.org/>
 
function usage {
        echo "Usage : ffsplit.sh input.file chunk-duration [output-filename-format]"
        echo -e "\t - input file may be any kind of file reconginzed by ffmpeg"
        echo -e "\t - chunk duration must be in seconds"
        echo -e "\t - output filename format must be printf-like, for example myvideo-part-%04d.avi"
        echo -e "\t - if no output filename format is given, it will be computed\
 automatically from input filename"
}
 
IN_FILE="$1"
OUT_FILE_FORMAT="$3"
typeset -i CHUNK_LEN
CHUNK_LEN="$2"
 
DURATION_HMS=$(ffmpeg -i "$IN_FILE" 2>&1 | grep Duration | cut -f 4 -d ' ')
DURATION_H=$(echo "$DURATION_HMS" | cut -d ':' -f 1)
DURATION_M=$(echo "$DURATION_HMS" | cut -d ':' -f 2)
DURATION_S=$(echo "$DURATION_HMS" | cut -d ':' -f 3 | cut -d '.' -f 1)
let "DURATION = ( DURATION_H * 60 + DURATION_M ) * 60 + DURATION_S"
 
if [ "$DURATION" = '0' ] ; then
        echo "Invalid input video"
        usage
        exit 1
fi
 
if [ "$CHUNK_LEN" = "0" ] ; then
        echo "Invalid chunk size"
        usage
        exit 2
fi
 
if [ -z "$OUT_FILE_FORMAT" ] ; then
        FILE_EXT=$(echo "$IN_FILE" | sed 's/^.*\.\([a-zA-Z0-9]\+\)$/\1/')
        FILE_NAME=$(echo "$IN_FILE" | sed 's/^\(.*\)\.[a-zA-Z0-9]\+$/\1/')
        OUT_FILE_FORMAT="${FILE_NAME}-%03d.${FILE_EXT}"
        echo "Using default output file format : $OUT_FILE_FORMAT"
fi
 
N='1'
OFFSET='0'
let 'N_FILES = DURATION / CHUNK_LEN + 1'
 
while [ "$OFFSET" -lt "$DURATION" ] ; do
        OUT_FILE=$(printf "$OUT_FILE_FORMAT" "$N")
        echo "writing $OUT_FILE ($N/$N_FILES)..."
        ffmpeg -i "$IN_FILE" -vcodec copy -acodec copy -ss "$OFFSET" -t "$CHUNK_LEN" "$OUT_FILE"
        let "N = N + 1"
        let "OFFSET = OFFSET + CHUNK_LEN"
done

Wednesday 15 September 2010

Lightweight HTTP server in BASH with PHP support

No kidding, I wrote this HTTP server in Bourne Shell. It supports most of HTTP 1.0 headers, Keep-alive requests, directory listing and PHP scripts. By its nature, this piece of software is not secure (it is fun though) and isn't intended for production purposes : <insert the usual NO WARRANTY boilerplate bullshit here>.

I tested it with PHPMyAdmin which I consider to be heavy PHP software and it works pretty well. It is not well commented, really I just wrote it for fun, learning BASH and HTTP protocol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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
#!/bin/bash
 
# Written by Alexis Bezverkhyy <alexis@grapsus.net> in 2008
# This is free and unencumbered software released into the public domain.
# For more information, please refer to <http://unlicense.org/>
 
# This script should be run via inetd, first parameter is WWW root path
 
# Uncomment for debugging
#exec 2>/tmp/log ; set -x
 
NUM="$RANDOM"
DOCUMENT_ROOT="$1"
KEEP_ALIVE="keep-alive"
 
while [ "$KEEP_ALIVE" == "keep-alive" ] ; do
KEEP_ALIVE="close"
 
for i in seq 1 5; do
  read -t 5 line
  if [ -n "$line" ] ; then break; fi
done
 
if grep -sqv 'HTTP' <<< "$line" ; then exit ; fi
#echo `date`" BEGIN $line" >> /tmp/"$NUM"-log
 
REQUEST_METHOD=`cut -d ' ' -f 1 <<< "$line"`
REQUEST_URI=`cut -d ' ' -f 2 <<< "$line" | sed 's/%20/ /'`
SCRIPT_NAME=`cut -d '?' -f 1 <<< "$REQUEST_URI"`
SCRIPT_FILENAME=`sed -e 's#//#/#' -e 's#/$##' <<< "$DOCUMENT_ROOT$SCRIPT_NAME"`
QUERY_STRING=''
if grep -sq '?' <<< "$REQUEST_URI" ; then
  QUERY_STRING=`cut -d '?' -f 2 <<< "$REQUEST_URI"`
fi
 
while read -t 1 line ; do
  line=`strings <<< "$line"`
  if grep -sqi '^Content-length' <<< "$line" ; then
    CONTENT_LENGTH=`cut -d ' ' -f 2 <<< "$line"`
  elif grep -sqi '^Content-type' <<< "$line" ; then
    CONTENT_TYPE=`cut -d ' ' -f 2 <<< "$line"`
  elif grep -sqi '^Connection' <<< "$line" ; then
    KEEP_ALIVE=`cut -d ' ' -f 2 <<< "$line"`
  elif grep -sqi '^Cookie' <<< "$line" ; then
    HTTP_COOKIE=`sed 's/Cookie:[ ]*//i' <<< "$line"`
  fi
  if [ -z "$line" -a "$REQUEST_METHOD" == "POST" -a -n "$CONTENT_LENGTH" ] ; then
    read -n "$CONTENT_LENGTH" line
    echo "$line" > /tmp/"$NUM"-post
    break
  elif [ -z "$line" ] ; then
    break
  fi
done
 
# some security
if grep -sq '\.\.' <<< "$SCRIPT_FILENAME" || ( namei "$SCRIPT_FILENAME" | grep -sq '\->') ; 
then
  SCRIPT_FILENAME='./'  
fi
 
if [ -d "$SCRIPT_FILENAME" ] ; then
  echo -en 'HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n'
  dir=`sed 's#'"$DOCUMENT_ROOT"'##' <<< "$SCRIPT_FILENAME"`
  if [ -z "$dir" ] ; then
    dir='/' ; parent='/'
  else
    parent=`sed 's#/[^/]\+$##' <<< "$dir"`
    if [ -z "$parent" ] ; then parent='/' ; fi
  fi
  echo "<html><head><title>Index of $dir</title></head>
  <body><h3>Index of $dir</h3>
  <table>
    <tr>
      <td><b>Name</b></td>
      <td><b>Last modified</b></td>
      <td><b>Size</b></td>
    </tr>
    <tr><td colspan=\"3\">[D] <a href=\"$parent\">..</a></td></tr>"
  for item in "$SCRIPT_FILENAME"/* ; do
    if [ "$item" == "$SCRIPT_FILENAME"'/*' ] ; then break ; fi
    name=`basename "$item"`
    link=`sed 's#'"$DOCUMENT_ROOT"'##' <<< "$item"`
    stat=`ls -lhd --time-style='+%d-%m-%y#%H:%m' "$item"`
    mtime=`cut -d ' ' -f 6 <<< "$stat" | sed 's/#/ /'`
    size=`cut -d ' ' -f 5 <<< "$stat"`
    echo "<tr><td>"
    if [ -L "$item" ] ; then
      echo "[S] $name<br/>"
    elif [ -d "$item" ] ; then
      echo '[D] <a href="'"$link"'">'"$name"'</a><br/>'
    else
      echo '[F] <a href="'"$link"'">'"$name"'</a><br/>'
    fi
    echo "</td><td>$mtime</td><td>$size</td></tr>"
  done
  echo "</table></body></html>"
elif [ -f "$SCRIPT_FILENAME" ] ; then
  mime='text/html'
  if grep -Esqv '\.(php|htm|html)$' <<< "$SCRIPT_FILENAME" ; then
    mime=`file -b --mime-type $SCRIPT_FILENAME`
  fi
  if grep -sq '\.php$' <<< "$SCRIPT_FILENAME" ; then
    for var in `env | cut -d '=' -f 1` ; do
      if [ "$var" != "PATH" -a "$var" != "PWD" -a "$var" != "LANG" -a "$var" != "SHLVL" ] ; then
        export -n "$var"
      fi
    done
    export REQUEST_URI REQUEST_METHOD QUERY_STRING DOCUMENT_ROOT SCRIPT_FILENAME \
    SCRIPT_NAME CONTENT_LENGTH CONTENT_TYPE GATEWAY_INTERFACE='CGI/1.1' \
    HTTP_HOST=`hostname -i` HTTP_COOKIE REDIRECT_STATUS=1
    if [ "$REQUEST_METHOD" == "GET" ] ; then
      php-cgi $SCRIPT_FILENAME \
      `tr '&' ' ' <<< "$QUERY_STRING"` > /tmp/"$NUM"-php
    else
      php-cgi $SCRIPT_FILENAME \
      `tr '&' ' ' <<< "$QUERY_STRING"` > /tmp/"$NUM"-php < /tmp/"$NUM"-post
    fi
    HTTP_STATUS=`grep -i '^Status: .*$' /tmp/"$NUM"-php | cut -d ' ' -f 2`
    if [ -z "$HTTP_STATUS" ] ; then
      HTTP_STATUS='200'
    fi
    OUT="head"
    cat /tmp/"$NUM"-php | while read ; do
      if [ "$OUT" = 'head' ] ; then
        REPLY=$(strings <<< "$REPLY")
        if [ -z "$REPLY" ] ; then
          OUT='body'
          continue
        fi
      fi
      echo "$REPLY" >> /tmp/"$NUM"-php-"$OUT"
    done
    echo -en "HTTP/1.0 $HTTP_STATUS OK\r\nContent-type: $mime\r\nContent-length:"\
    `ls -l /tmp/"$NUM"-php-body | cut -d ' ' -f 5`"\r\nConnection: $KEEP_ALIVE\r\n"
    cat /tmp/"$NUM"-php-head
    echo -en "\r\n"
    cat /tmp/"$NUM"-php-body
  else
    echo -en "HTTP/1.0 200 OK\r\nContent-type: $mime\r\nContent-length: "\
    `ls -l "$SCRIPT_FILENAME" | cut -d ' ' -f 5`"\r\nConnection: $KEEP_ALIVE\r\n\r\n"
    cat "$SCRIPT_FILENAME"
  fi
  rm -f /tmp/"$NUM"-php /tmp/"$NUM"-php-body /tmp/"$NUM"-php-head /tmp/"$NUM"-post 
  # /tmp/"$NUM"-log
else
  echo -en 'HTTP/1.0 404 NOT FOUND\n\rContent-type: text/plain\r\n\r\n404 File not found'
fi
#echo `date`" END" >> /tmp/"$NUM"-log
done

Here's the inetd configuration I use to run it :

8080	stream	tcp	nowait	grapsus	/usr/sbin/tcpd /home/grapsus/bin/http.sh /home/grapsus/www

I know BASH supports sockets, but this support is disabled in most Unix distributions (especially on Debian).

Let me know what you think about it or the improvements you made.

Wednesday 25 August 2010

The perfect Eclipse, PDT (PHP developpement tools) and Xdebug setup !

Eclipse is a great IDE and Xdebug adds a lot of usefull features for PHP debugging. With PDT plugin for Eclipse, you can use Eclipse as a debug client for Xdebug and debug live PHP code !

I won't describe here how to setup each of these tools. You may find many better written articles about that (with lots of screenshots and all other fancy stuff). I will simply expose how to solve a few very annoying bugs in this configuration which can drive you crazy.

No output when debugging in browser

When you choose to debug a PHP script in an external browser, the output of your script isn't sent to the browser until the debug session is terminated ! I verified it with Wireshark, the browser gets absolutely nothing (not even the HTTP headers) and keeps waiting. None of the *ob_* or *flush* functions seem to help it. I kept trying different PHP options and even editing Xdebug source code until I found the implicit_flush setting which makes it work !!! Just add

implicit_flush = On

to your php.ini and you'll be able to see your output in live !

I really think it's a bug because when you normally run a PHP script without output buffering, the headers and the content are sent to the client before the end of execution. Maybe Xdebug messes up some internal PHP configuration when it sets up a debugging session.

Very annoying and useless DEBUG SESSION ENDED new page

As you terminate a browser debug session, a new browser window pops up saying DEBUG SESSION ENDED. WTF ?! Why do we need to make a HTTP query to stop the debug session ?! The DBGP protocol used by PDT has a stop command. One more time with Wireshark I saw that this command is issued by PDT and Xdebug answers ok to it !

The only solution I found was to make a wrapper for Firefox to ignore these queries. It works very well. Even better, I found a way to close the Firefox window with your application when you terminate the debug session !

Eclipse opens web browsers with additionnal parameters you cannot disable !

Here comes another Eclipse bug, when you set up an external browser some implicit parameters are passed to it and there is no way to disable it. For example, it doesn't execute

firefox %URL%

but

firefox -remote openURL(%URL%)

WFT ?! I don't want it to grab my existing Firefox instance and mess around with it ! So here's the wrapper to solve the two problems above. Create a new Firefox profile named eclipse by running

firefox -profile-manager -no-remote

Now set up Eclipse to use the following script as web browser. Debug sessions will be opened in a new window and when you terminate it, this window will automatically close !

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
 
URL=$(grep -o 'http://[^)]*' <<< "$@")
echo "$URL" >> /tmp/url
if !(grep -q 'XDEBUG_SESSION_STOP_NO_EXEC' <<< "$URL") ; then
        firefox -no-remote -P eclipse "$URL" &
        echo "$!" > /tmp/eclipse-firefox.pid
else
        kill $(cat /tmp/eclipse-firefox.pid)
        rm -f /tmp/eclipse-firefox.pid
fi

Notes

I made my tests on PHP 5.3, Xdebug 2.1, Xdebug 2.2-dev, Eclipse Ganymede, Eclipse Galileo, Eclipse Helios and PDT 2.1 and PDT 2.2 on Debian SID (amd64).

Let me know if these workarounds did it for you or if you found more elegant solutions to those problems.

Edit : here's a port of this wrapper on Windows.

Thursday 5 August 2010

Nagios plugin to check backup folders

Here's a plugin I wrote for Nagios to check directories where some scripts regularly store backup files. You may specify a directory to scan, an optional pattern for the filename and the minimal age and size you expect for the latest file found in that directory and matching the pattern. The script can output warnings and critical alerts with different thresholds.

/root/bin//nagios-check-backup.sh :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/bin/bash
 
# Written by Alexis Bezverkhyy <alexis@grapsus.net> in july 2010
# This is free and unencumbered software released into the public domain.
# For more information, please refer to <http://unlicense.org/>
 
 
function PRINT_USAGE(){
  echo "This Nagios plugin checks backup folders :
  -d DIRECTORY  the directory to search for backup files
  -p PATTERN  an optionnal pattern for backup files
  -t HOURS  maximal age in hours for the latest backup before a warning is issued
  -T HOURS  maximal age in hours for the latest backup before a critical alert is issued
  -s KBYTES maximal size in kilo bytes for the latest backup before a warning is issued
  -S KBYTES maximal size in kilo bytes for the latest backup before a critical alert is issued
  -h    prints out this help
You must at least specify a directory and a minimal size or a minimal age."
  exit 0
}
 
WTIME=0;CTIME=0;WSIZE=0;CSIZE=0;DIR='';PATTERN=''
declare -i CTIME 
declare -i WTIME
declare -i CSIZE
declare -i WSIZE
while true ; do
  getopts 't:T:s:S:d:p:h' OPT 
  if [ "$OPT" = '?' ] ; then break; fi; 
  case "$OPT" in
    "t") WTIME="$OPTARG";;
    "T") CTIME="$OPTARG";;
    "s") WSIZE="$OPTARG";;
    "S") CSIZE="$OPTARG";;
    "d") DIR="$OPTARG";;
    "p") PATTERN="$OPTARG";;
    "h") PRINT_USAGE;;
  esac
done
 
if [ -z "$DIR" -o '(' "$WTIME" = '0' -a "$CTIME" = '0'\
 -a "$WSIZE" = '0' -a "$CSIZE" = '0' ')' ] ; then
  PRINT_USAGE
fi
 
LASTFILE=$(ls -lt --time-style=+%s "$DIR" | grep -v "^total " | grep "$PATTERN"\
 | head -n 1 | sed 's/\s\+/ /g')
if [ -z "$LASTFILE" ] ; then
  echo "CRITICAL - no backup found in $DIR" 
  exit 2
fi
 
TIMESTAMP=$(cut -d ' ' -f 6 <<< "$LASTFILE")
BYTES=$(cut -d ' ' -f 5 <<< "$LASTFILE")
let "SIZE = $BYTES / 1024"
FILENAME=$(cut -d ' ' -f 7 <<< "$LASTFILE")
let "AGE = ( $(date +%s) - $TIMESTAMP ) / 3600"
 
if [ "$CTIME" -gt 0 -a "$AGE" -gt "$CTIME" ] ; then
  echo "CRITICAL - $FILENAME is out of date ($AGE hours old)" 
  exit 2
fi
 
if [ "$WTIME" -gt 0 -a "$AGE" -gt "$WTIME" ] ; then
  echo "WARNING - $FILENAME is out of date ($AGE hours old)"  
  exit 1
fi
 
if [ "$CSIZE" -gt 0 -a "$SIZE" -lt "$CSIZE" ] ; then
  echo "CRITICAL - $FILENAME is too small ($SIZE kb)" 
  exit 2
fi
 
if [ "$WSIZE" -gt 0 -a "$SIZE" -lt "$WSIZE" ] ; then
  echo "WARNING - $FILENAME is too small ($SIZE kb)"  
  exit 1
fi
 
echo "OK - $FILENAME ($AGE hours old, $SIZE kb)"
exit 0

Here is a sample configuration for Nagios to use my script. check_backup checks a regular folder with compressed backups and check_sync checks a directory that is ought to be synchronized, therefore only age is checked and not the size.

commands.cfg :

1
2
3
4
5
6
7
8
9
10
define command {
  command_name check_backup
  command_line /root/bin/nagios-check-backup.sh -d $ARG1$ -p $ARG2$ -t $ARG3$ \
   -T $ARG4$ -s $ARG5$ -S $ARG6$
}
 
define command {
  command_name check_sync
  command_line /root/bin/nagios-check-backup.sh -d $ARG1$ -t $ARG3$ -T $ARG4$
}

foo.cfg :

1
2
3
4
5
6
7
8
9
10
11
12
13
define service{
        use                             generic-service
        host_name                       bar
        service_description             BAK-FOO-CONF
        check_command                   check_backup!/media/backup/foo/conf/!conf!30!60!2000!1000
}
 
define service{
        use                             generic-service
        host_name                       bar
        service_description             BAK-FOO-WWW
        check_command                   check_sync!/media/backup/foo/www/!30!60
}

- page 1 of 2