Osakidetza y cómo cagarla con un Bash-script (2/2)

Bueno pues.

Después de toda la espera toca ponerse a destripar el script del infierno que ya presenté aquí.

Aviso de que se me va a ir de longitud este post. Nada raro en mí.

Antes de nada deciros muy seriamente un par de cosas:

No está bien meterse con el trabajo de los demás. Este script lo ha hecho una persona. Por lo que veremos, seguramente no tendrá mucho conocimiento de GNU/Linux pero programa bastante limpio así que seguramente sea programador, posiblemente de Java (el programa está hecho en Java), cosa que hará bastante mejor que yo.

El objetivo de esto no es la burla, es el aprendizaje. Tanto el mío como el vuestro. No voy a clavar perfecto todo lo que comente aquí, igual que ocurre con todo lo que escribo en el blog, pero este es un lugar para la reflexión y el análisis positivo.

Escribo en clave de humor y vacilo a Osakidetza y a otros medios gubernamentales porque su labor es hacer las cosas bien (como la de todos) pero tiran de subcontrataciones chapuceras y no tienen un control real de lo que reciben a cambio.

Ni media broma al currela que se parte el lomo por una miseria en una consultora carnicera para hacer un script en un lenguaje que no forma parte de su curro.

Todo mi desprecio a las consultoras carniceras que sólo quieren que parezca que han hecho algo y ponen a cualquiera a hacer el trabajo y le presionan para que funcione y a todos los que contratan a este tipo de esclavistas y estafadores modernos.

Dicho esto, vamos a analizar el código. No sólo lo que esté mal (según mi criterio, ojo) si no que vamos a intentar fijarnos en el estilo y otras cosas. Veremos si lo consigo.

Aquí os dejo el script, pulsad para expandirlo. Lo dejo recogidito porque iré sacando pedazos más abajo, sólo está como referencia.

#!/bin/bash
# This script attempts to find an existing installation of Java that meets a minimum version
# requirement on a Linux machine.  If it is successful, it will export a JAVA_HOME environment
# variable that can be used by another calling script.
#
# To specify the required version, set the REQUIRED_VERSION to the major version required, 
# e.g. 1.3, but not 1.3.1.
REQUIRED_TEXT_VERSION=1.6

# Transform the required version string into a number that can be used in comparisons
REQUIRED_VERSION=`echo $REQUIRED_TEXT_VERSION | sed -e 's;\.;0;g'`
# Check JAVA_HOME directory to see if Java version is adequate
if [ $JAVA_HOME ]
then
    JAVA_EXE=$JAVA_HOME/bin/java
    VERSION=`$JAVA_EXE -version 2>&1 | head -1`
    VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
    VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
    if [ $VERSION ]
    then
        if [ $VERSION -ge $REQUIRED_VERSION ]
        then
            JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
        else
            JAVA_HOME=
        fi
    else
        JAVA_HOME=
    fi
fi

# If the existing JAVA_HOME directory is adequate, then leave it alone
# otherwise, use 'locate' to search for other possible java candidates and
# check their versions.
if [ $JAVA_HOME ]
then
    :
else
    for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`
    do
        if [ $JAVA_HOME ] 
        then
            :
        else
            VERSION=`$JAVA_EXE -version 2>&1 | head -1`
            VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
            VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
            if [ $VERSION ]
            then
                if [ $VERSION -ge $REQUIRED_VERSION ]
                then
                    JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
                fi
            fi
        fi
    done
fi

# Get additional weasis arguments
userParameters=()
for var in "$@"
do
if  [[ $var == \$* ]]
then
    userParameters+=("$var")
fi
done
echo user arguments: ${userParameters[@]}

# If the correct Java version is detected, then export the JAVA_HOME environment variable
if [ $JAVA_HOME ]
then
    export JAVA_HOME
    echo Java Home: $JAVA_HOME/bin/java
    curPath=$(dirname "`readlink -f "$0"`")
    echo Weasis launcher directory: $curPath
    $JAVA_HOME/bin/java -Xms64m -Xmx512m -Dgosh.args="-sc telnetd -p 17179 start" -Dweasis.portable.dir="$curPath" -classpath "$curPath/weasis/weasis-launcher.jar:$curPath/weasis/felix.jar:$curPath/weasis/substance.jar" org.weasis.launcher.WeasisLauncher \$dicom:get --portable ${userParameters[@]}
else echo 'Weasis requires Java Runtime '$REQUIRED_TEXT_VERSION' or higher, please install it'
fi

Empezamos resumiendo un poco la labor del script. Básicamente, se encarga de arrancar un programa en Java con un conjunto de argumentos de entrada, toda la lógica sirve para buscar lo que veréis que se llama JAVA_HOME y comprobar la versión del Java que el usuario tenga instalado (VERSION dentro del script).

Los dos if principales (lineas 13 y 35) son los que aseguran esto. Ahora entramos en detalle pero antes una cuestión de estilo.

Podéis ver que el código está bien indentado (con tabs, pero bien indentado) y comentado. Con comentarios no muy anchos para que sean fáciles de leer. Por eso comentaba antes que me da la sensación de que el que lo ha escrito es programador, pero, por las cosas que veremos luego, no controla demasiado de la herramienta que está usando (que, por otro lado, es bastante cabrona y difícil).

Vamos a por esos if:

El primero, que comienza en la línea 13 es el if con lógica. Parte de la variable de entorno JAVA_HOME que suele indicar dónde está nuestra instalación de Java para ejecutar java -version (línea 16) y comprobar si la versión es más grande que la indicada en la línea 8.

# Check JAVA_HOME directory to see if Java version is adequate
if [ $JAVA_HOME ]
then
    JAVA_EXE=$JAVA_HOME/bin/java
    VERSION=`$JAVA_EXE -version 2>&1 | head -1`
    VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
    VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
    if [ $VERSION ]
    then
        if [ $VERSION -ge $REQUIRED_VERSION ]
        then
            JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
        else
            JAVA_HOME=
        fi
    else
        JAVA_HOME=
    fi
fi

Hasta aquí todo bastante bien. En detalle vemos como necesita diseccionar el resultado de java -version (lineas 16-18) pero tampoco sabría hacerlo mucho más bonito si fuera necesario.

En el if anidado vemos que para recuperar el JAVA_HOME nuevo corta el JAVA_EXE que él mismo ha creado antes (linea 23). No tiene mucho sentido, creo yo, recuperando el JAVA_HOME de más arriba era suficiente. Además lo recorta eliminándole los últimos 9 caracteres, que es exactamente lo que mide /bin/java, que es la parte que le ha añadido a JAVA_HOME antes para crear JAVA_EXE.

Yo ese if lo dejaría directamente así (líneas 19-30):

        if [ $VERSION -lt $REQUIRED_VERSION ] # -lt es less than
            JAVA_HOME=
        fi

Muchos ya os habéis dado cuenta de un par de detalles pero lo iré aclarando al final, no desesperéis. Si luego me olvido pues lo comentáis y listo.

Ese if no tiene mucha más chicha que eso, vamos al siguiente que es donde está la movida guapa.

# If the existing JAVA_HOME directory is adequate, then leave it alone
# otherwise, use 'locate' to search for other possible java candidates and
# check their versions.
if [ $JAVA_HOME ]
then
    :
else
    for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`
    do
        if [ $JAVA_HOME ] 
        then
            :
        else
            VERSION=`$JAVA_EXE -version 2>&1 | head -1`
            VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
            VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
            if [ $VERSION ]
            then
                if [ $VERSION -ge $REQUIRED_VERSION ]
                then
                    JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
                fi
            fi
        fi
    done
fi

El comentario ya lo dice todo.

Para empezar, usa locate, una herramienta que no viene por defecto normalmente. Cagada. Mejor usar find aunque sea más lento.

O mejor no usar, porque lo que hace es aún más cremas. Mirad la línea 39:

    for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`

Ese for itera por todos ficheros del sistema que cumplen lo siguiente:

  • Contienen bin/java en su nombre
  • Terminan en java

El developer esperaba encontrar /usr/local/bin/java por ejemplo, pero con esa búsqueda también puede encontrar /home/user/projects/example_virus/bin/java_binaries/destroy_all_java, por ejemplo.

Lo divertido es que poco más abajo lo ejecuta, así porque él lo vale. Linea 45:

            VERSION=`$JAVA_EXE -version 2>&1 | head -1`

Repito más claro:

Este programa busca cosas casi aleatorias en tu sistema y las ejecuta para ver si su versión es correcta. Luego, si cumple, lo vuelve a ejecutar con un churro gigante de parámetros de entrada.

Si te encuentra algo que no es, lo va a disparar, sea malicioso o no.

Eso es lo más divertido de ese if, el resto es como el de arriba.

El bloque final es mucho más aburrido. Añade los parámetros de entrada que le metamos en la ejecución a unos que permite que le metas por defecto hardcoded (lineas 59-68) y ejecuta el programa haciendo mil jueguecitos que tiene pinta que sean muy necesarios.

Un par de cosas generales:

No entiendo para qué se necesita volverse loco con la puta variable JAVA_HOME. Si el usuario no es idiota y tiene todo bien instalado con llamar directamente a java es suficiente porque estára en el PATH y listo. Si realmente se necesitase setear la variable para algo interno del programa, con buscar dónde está ese binario sería suficiente, de nuevo, tirando del sistema se puede hacer con un whereis.

Es absurdo porque tras nosecuantísimo procesamiento para buscar la versión y la localización de Java en mi sistema, fue incapaz de localizar el OpenJDK, que sí que es capaz de lanzar el programa con éxito. Esto ocurrió porque en el parseo de las versiones utiliza un formato que es específicamente el que da Java, OpenJDK pone otra cosa diferente aunque parecida:

openjdk version "1.8.0_121"
OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-0ubuntu1.16.04.2-b13)
OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)

A parte de todo esto, cuando decía que el programador no controla mucho de Bash es porque le veo muchos detalles en los que se nota que no tiene mucha experiencia con la herramienta. Por ejemplo, tiene ifs vacíos porque necesita el else cuando con negar la condición sería suficiente. El uso del locate en lugar de otras herramientas también puede dar una pista.

En general, el programa parece que está copy-pasteado de Stack Overflow. Cosa que todos hacemos, pero a algunos se les nota más que a otros.

🙂

Conclusiones:

  • No ejecutéis todos los ficheros del sistema. Si el usuario no tiene las cosas bien configuradas que le den por el culo. Es mejor eso que ponerse a ejecutar cosas aleatorias que a saber lo que hacen.

  • No seáis tan locos con las versiones porque eso os puede llevar al caso anterior.

  • Si sólo tenéis código para meter en el else quizás negar el if sea la solución más elegante. man [ para que os cuenten un poco de qué va el rollo.

  • Intentad evitar usar programas que no estén por defecto en el sistema. locate vs find. Y, a poder ser, no busquéis porque puede que caigáis en el caso 1.

  • Todos hemos trabajado en una herramienta en la que estamos un poco cojos, está bien buscar en internet, pero no está de más investigar un poco más si las fechas de entrega te lo permiten.

  • Bash es más feo que una nevera por detrás, pero es útil a veces.

  • No trabajes en una consultora cárnica si lo puedes evitar. Intenta cambiar el mundo en otro tipo de trabajos más sociales o éticos. Esos cabrones no te merecen. Es difícil pero lo puedes conseguir.

En fin.

Ya comenté en la primera parte la solución rápida que le metí al script, dejo en vuestras manos una más elaborada.

Un abrazo. Como siempre.

Anuncios

Osakidetza y cómo cagarla con un Bash-script (1/2)

Hola,

Son la una y media de la mañana y no puedo dormir así que os voy a contar una linda historia con moraleja en la que trataré de poneros en mi piel.

Resulta que hace varias semanitas, debido a unos temas, acaba llegando a tus manos una radiografía de la espalda de alguien. Esta radiografía fue pedida a Osakidetza, el servicio Vasco de Salud (comunidad autónoma en la que resides).

El sistema es sencillo. Te hacen una radiografía y, si la quieres, te permiten que rellenes un escrito para pedirla. Poco después te llega en un CD.

Tiene todo el sentido del mundo porque así tienes oportunidad de tener una segunda opinión de un conocido, acudir a otro médico o verla por ti mismo.

Ahora esperaréis que la historia se tuerza porque no se puede ver en Linux pero va a molar mucho más.

Resulta que el CD tiene un Autorun (que en Linux, evidentemente, no se dispara) que despierta una aplicación para poder ver las radiografías. Piensas que con una imagen sería suficiente, pero, como descubrirás después, el programa es mucho más potente y es capaz de tomar medidas, navegar y otras cosas que a los médicos les vienen bien.

Como la aplicación no se dispara, piensas que en Linux vas a estar jodido, pero te motivas y abres el contenido del CD a ver qué se cuece. Resulta que es un programa en Java. ¡Bien! ¡Eso es portable!

Tiene un par de carpetas de Windows y Mac así que esperas que haya para Linux. Efectivamente. Te encuentras con un script de Bash que esperas que levante la aplicación de Java.

Lo lanzas y falla, te dice que no tienes Java instalado. Lo compruebas y tienes el OpenJDK. Por un momento piensas en instalar el Java oficial pero acabas rechazando la idea y abriendo ese script para buscar por qué no te localiza lo que tienes. Te encuentras con esto:

#!/bin/bash
# This script attempts to find an existing installation of Java that meets a minimum version
# requirement on a Linux machine.  If it is successful, it will export a JAVA_HOME environment
# variable that can be used by another calling script.
#
# To specify the required version, set the REQUIRED_VERSION to the major version required, 
# e.g. 1.3, but not 1.3.1.
REQUIRED_TEXT_VERSION=1.6

# Transform the required version string into a number that can be used in comparisons
REQUIRED_VERSION=`echo $REQUIRED_TEXT_VERSION | sed -e 's;\.;0;g'`
# Check JAVA_HOME directory to see if Java version is adequate
if [ $JAVA_HOME ]
then
	JAVA_EXE=$JAVA_HOME/bin/java
	VERSION=`$JAVA_EXE -version 2>&1 | head -1`
	VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
	VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
	if [ $VERSION ]
	then
		if [ $VERSION -ge $REQUIRED_VERSION ]
		then
			JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
		else
			JAVA_HOME=
		fi
	else
		JAVA_HOME=
	fi
fi

# If the existing JAVA_HOME directory is adequate, then leave it alone
# otherwise, use 'locate' to search for other possible java candidates and
# check their versions.
if [ $JAVA_HOME ]
then
	:
else
	for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`
	do
		if [ $JAVA_HOME ] 
		then
			:
		else
			VERSION=`$JAVA_EXE -version 2>&1 | head -1`
			VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
			VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
			if [ $VERSION ]
			then
				if [ $VERSION -ge $REQUIRED_VERSION ]
				then
					JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
				fi
			fi
		fi
	done
fi

# Get additional weasis arguments
userParameters=()
for var in "$@"
do
if  [[ $var == \$* ]]
then
    userParameters+=("$var")
fi
done
echo user arguments: ${userParameters[@]}

# If the correct Java version is detected, then export the JAVA_HOME environment variable
if [ $JAVA_HOME ]
then
	export JAVA_HOME
	echo Java Home: $JAVA_HOME/bin/java
	curPath=$(dirname "`readlink -f "$0"`")
	echo Weasis launcher directory: $curPath
	$JAVA_HOME/bin/java -Xms64m -Xmx512m -Dgosh.args="-sc telnetd -p 17179 start" -Dweasis.portable.dir="$curPath" -classpath "$curPath/weasis/weasis-launcher.jar:$curPath/weasis/felix.jar:$curPath/weasis/substance.jar" org.weasis.launcher.WeasisLauncher \$dicom:get --portable ${userParameters[@]}
else echo 'Weasis requires Java Runtime '$REQUIRED_TEXT_VERSION' or higher, please install it'
fi

Es gracioso porque en menos de 10 minutos lo haces funcionar convirtiéndolo en lo siguiente sin pensar mucho:

#!/bin/bash
# This script attempts to find an existing installation of Java that meets a minimum version
# requirement on a Linux machine.  If it is successful, it will export a JAVA_HOME environment
# variable that can be used by another calling script.
#
# To specify the required version, set the REQUIRED_VERSION to the major version required, 
# e.g. 1.3, but not 1.3.1.
REQUIRED_TEXT_VERSION=1.6



# Get additional weasis arguments
userParameters=()
for var in "$@"
do
if  [[ $var == \$* ]]
then
    userParameters+=("$var")
fi
done
echo user arguments: ${userParameters[@]}

# If the correct Java version is detected, then export the JAVA_HOME environment variable
if [ 0 -eq 0 ]
then
	export JAVA_HOME=/usr/
	echo Java Home: $JAVA_HOME/bin/java
	curPath=$(dirname "`readlink -f "$0"`")
	echo Weasis launcher directory: $curPath
	$JAVA_HOME/bin/java -Xms64m -Xmx512m -Dgosh.args="-sc telnetd -p 17179 start" -Dweasis.portable.dir="$curPath" -classpath "$curPath/weasis/weasis-launcher.jar:$curPath/weasis/felix.jar:$curPath/weasis/substance.jar" org.weasis.launcher.WeasisLauncher \$dicom:get --portable ${userParameters[@]}
else echo 'Weasis requires Java Runtime '$REQUIRED_TEXT_VERSION' or higher, please install it'
fi

Por el camino, te encuentras con unos detalles increíbles que, viendo lo graves que son, decides contar en tu blog.

La única diferencia con el script de arriba es que has borrado todo el bloque de comprobaciones y has hardcodeado JAVA_HOME. Una solución muy sucia que no funcionaría en todos los casos, pero te vale por el momento.

En la parte borrada hay unas joyas estelares. La próxima entrada explicará cuales, por qué y por qué este código a parte de no ser muy bueno es peligroso.

Seguimos otro día. Comentad con lo que encontréis y lo vamos discutiendo. Dentro de unos días pongo mi análisis del código.

¡Un abrazo!