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

5 pensamientos en “Osakidetza y cómo cagarla con un Bash-script (2/2)

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

  2. Lo de buscar JAVA_HOME sí es una buena idea, porque, si no lo encuentra, siempre puede poner un mensaje de error del tipo: «No tienes Java instalado en tu sistema. Descárgatelo de no sé dónde. Si crees que sí lo tienes instalado, comprueba que la variable de entorno JAVA_HOME esté correctamente…, etc.» Y si lo encuentra, pues ya sabes dónde está el ejecutable, pero buscar éste por todo el sistema sí es, como tú has dicho, un fallo de seguridad y además una pérdida de tiempo.

    Por otro lado, JAVA_HOME es una variable de entorno, así que encontrar su valor es tan fácil como hacer:

    if [ -n $JAVA_HOME ]; then
    # Encontramos el ejecutable basándonos en el valor de JAVA_HOME
    # y lo ponemos en la variable JAVA_BIN, por ejemplo
    else
    # Mensaje de error
    exit 1
    fi

    Sobre la versión, la página man de java dice lo siguiente: «For JAR files, the preference is to specify version requirements in the JAR file manifest rather than on the command line.» No obstante, se podría sacar con la línea siguiente (que es una ñapa como un piano y no la recomiendo, pero a mí me funciona):

    JAVA_VERSION=$JAVA_BIN -version 2>&1 | head -n1 | cut -f3 -d' ' | cut -f2 -d'"' | rev | cut -f2- -d. | rev

    if [ $REQUIRED_VERSION > $JAVA_VERSION ]; then
    # La versión es correcta
    else
    # Mensaje de error
    exit 1
    fi

    Por lo demás, totalmente de acuerdo en todo. Muy buenos artículos.

    • Hola,

      Comentario largo e interesante, como a mí me gustan.
      Te respondo corto y al pie:

      Sí, tienes razón pero al mismo tiempo no la tienes. Estoy de acuerdo con el fin, pero no con los medios.

      Explico: En mi caso, yo no la tengo seteada, pero sí que podía correr el programa. OpenJDK no te setea el JAVA_HOME. Una forma fácil de encontrarlo siendo así sería directamente ejecutando java y ya que sí que demuestra al 100% que tienes Java instalado.

      Lo demás 100% de acuerdo.

      Y los supercuts esos que te has clavao me han encantado.

      Un abrazo, gracias por comentar y opinar!

      • Hola, Ekaitz:

        Juraría que en mi caso (yo uso Slackware) las variables de entorno (JAVA_HOME, MANPATH y PATH) las puso el SlackBuild, no el instalador del JDK, así que supongo que a nadie le vienen puestas por defecto.

        Pero la idea de comprobar JAVA_HOME no es sólo saber si está o no java instalado y dónde. El mensaje de error también es importante. Le estamos diciendo al usuario: «Es tu sistema, y si no me dices dónde debo tocar, no pienso tocar nada.» Si ejecutamos java por las bravas, no tenemos forma de saber qué java vamos a ejecutar. Todo depende del orden de los directorios en PATH. Es el mismo fallo de seguridad que comentas en el artículo.

        Me podrás decir, con razón, que el usuario o un atacante pueden modificar el valor de JAVA_HOME igual que el de PATH, pero si no podemos estar seguros ni de eso, lo único que nos queda es decirle al usuario el comando que tiene que ejecutar y que lo haga él como le parezca.

        Sobre los cuts y los revs: muchas gracias. Son muy útiles para sacar la versión de un paquete, porque la mayoría tiene el mismo formato:

        nombre-version.tar.xz.

        En los SlackBuilds te permiten no tener que modificar el script con cada actualización.

        Un abrazo. Y gracias a ti por hacerme pensar y aprender.

      • Tu argumentación es buena pero no puedo estar de acuerdo al 100%.

        El fallo de seguridad, para nada. Se tiene que dar por hecho en algún momento que el usuario tiene las cosas bien. En otra entrada hablé de cómo destrozar el PATH y se puede aplicar lo mismo con el JAVA_HOME, son igual de inseguras, ¿por qué íbamos a confiar más en una que en la otra?

        Una pregunta que yo me hago es: ¿Para lanzar todos los comandos del script se le pide al usuario una variable apuntando a dónde están? En realidad no. Hace cut, locate (XD) y otros sin preguntar. Esos comandos también pueden estar pisados por el usuario o en un PATH extraño y dar problemas, ¿por qué no hemos pensado en esto?

        Java es un comando más. Limitarse a si el usuario tiene seteado JAVA_HOME es como comprobar el PATH en todos los scripts y mirar a ver a donde apunta. Me parece excesivo. El usuario debería tener las cosas bien configuradas y, si no las tiene, que no funcione es lo normal. No hay fallo de seguridad en el script.

        Otra cosa es: comprobar primero si existe el JAVA_HOME para lanzar desde ahí y, si éste no existe, lanzar java normal. Esto permitiría a los usuarios más avanzados tener JAVA_HOME apuntando a un lugar extraño y a los menos avanzados tener Java instalado en algún lugar de su PATH sin volverse locos. En el texto también comento usar whereis por si no te fías de dónde se encuentra el ejecutable. El problema principal es el locate loco que hace por todo el sistema. Lo de analizar la variable sí que es útil en una primera instancia, pero no me parece que no tenerla deba ser determinante para cortar la ejecución.

        Esto también me parece importante porque los usuarios no tienen por qué saber lo que es una variable de entorno y les tiramos un fallo en la puta face diciendo: “¡Eh! ¡Que no tienes el JAVA_HOME!” y es normal que se molesten y no sepan reaccionar. La reacción es: “¡Pero si tengo Java instalado! ¿Qué más tengo que hacer?” y eso, aunque sea muy normal que ocurra, no es nada agradable y me molesta mucho.

        Y esa es mi opinión.

        Muy valiosos tus comentarios, te animo a que sigas haciéndolos, porque me hacen volver a pensar y trabajarme las opiniones.

        Gracias de nuevo por comentar 😀

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s