Weblogs Código

Coding Potions

Sortilegios

enero 15, 2022 12:00

» Leer más, comentarios, etc...

Coding Potions

Cómo crear un sistema de autocompletado en inputs - Sortilegios 03

enero 15, 2022 12:00

Qué vamos a hacer

En este artículo voy a explicar cómo construir un input HTML que tenga sistema de autocompletado. Además, se me ha ocurrido que sería buena idea dar alguna pincelada sobre accesibilidad, ya que yo mismo desconozco mucho sobre este tema.

Para el autocompletado voy a llamar a una API que me devuelva la lista de países para poder ofrecer una lista con resultados que sean parecidos a lo que escribe el usuario, vamos al lío.

Por cierto, te dejo el enlace a la demo de lo que vamos a hacer por si quieres ver ya el código sin leer el artículo:

https://codepen.io/Frostq/pen/oNxygGY

Este artículo pertenece a la serie de artículos con contenido práctico llamada Sortilegios

🗺️ Hoja de ruta

  • Crear el HTML con el input y un sistema para poder autompletar que sea accesible
  • LLamar a la API para recibir la lista de países
  • Según escribe el usuario mostrar el listado de países

Creando la vista y los estilos para el autocompletado

Bien, empecemos, lo primero es crear el HTML. Mi idea es crear un formulario HTML con un input y un botón porque lo quiero es hacer un ejemplo de un buscador de ciudades.

<div class="container">
  <form>
    <input 
      placeholder="Search for a country"
      aria-label='Search for a country'
      aria-autocomplete='both'
      aria-controls='autocomplete-results'
    >
    <button 
      type='submit'
      aria-label='Search'
    >
      <svg aria-hidden='true' viewBox='0 0 24 24'>
        <path d='M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z' />
      </svg>
    </button>
    <ul
      id='autocomplete-results'
      role='listbox'
      aria-label='Search for a country'
     >
    </ul>
  </form>
</div>

Dentro del formulario he puesto 3 cosas: un input, un botón submit, que dentro tiene un svg para poder poner el icono de una lupa, y una lista que por el momento no tiene nada dentro.

El input sirve para que el usuario pueda escribir, el botón en este ejemplo en específico no tendrá uso porque no se puede hacer nada una vez escrito el nombre del país, y la lista vacía que la rellenaremos con el autocompletado.

Fíjate que he ido añadiendo varios atributos aria. Estos atributos están relacionados con la accesibilidad.

El input tiene un aria-label para indicar al lector de pantalla (para personas con problemas de visión) la acción que va a realizar ese elemento. El atributo aria-autocomplete como su nombre indica, sirve para decir que ese input tiene implementado un sistema de autompletado. Este atributo puede recibir varios valores.

  • inline. El texto se autompleta sobre el propio texto que escribe el usuario.
  • list. Al escribir aparece un popup o lista con los elementos que puedes elegir para autocompletar.
  • both. El que he elegido para este ejemplo, indica que va a suceder ambas cosas, que haya una lista con las posibles opciones y que el texto del propio input pueda ser cambiado con la opción elegida.

Por último he añadido el parámetro aria-hidden al svg para indicar al lector de pantalla que ese elemento puede ignorarlo ya que solo tiene función estética.

Para el CSS no he hecho nada del otro mundo. Simplemente he puesto estilos básicos para que se vea decente y he añadido a la clase hidden un display:none para ocultar el autocompletado. Por Javascript quitaremos esas clase dinámicamente para mostrar los resultados.

* {
  border-sizing: border-box;
}
html, body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}
.container {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #e8f0fb;
}
form {
  display: flex;
  box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
  position: relative;
}
input, button {
  border: none;
  background: white;
  margin: 0;
}
input {
  font-size: 16px;
  padding: 10px 12px;
}
button {
  padding: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
}
svg {
  width: 18px;
  height: 18;
  fill: black;
}
ul {
  position: absolute;
  top: 100%;
  margin-top: -1px;
  width: 100%;
  z-index: 1;
  background: #fff;
  margin: 0;
  list-style: none;
  transition: none;
  padding: 0;
  border: 1px solid #bfc8c9;
}
ul li {
  padding: 6px 12px;
  transition: 0.2s background;
  cursor: pointer;
}
ul li:hover {
  background: #e6f0f2;
}
ul li + li {
  border-top: 1px solid #bfc8c9;
}
.hidden {
  display: none;
}

He añadido también estilos para los resultados del autocompletado, los usaremos más adelante. Aunque he usado directamente selectores HTML para el CSS, no te lo recomiendo (yo lo he hecho por comodidad y rapidez), siempre que puedas usa clases en el CSS.

El resultado por el momento es este:

Se observa un input para escribir y a su lado un icon de una lupa

Llamando a la API para recuperar la lista de países

Empecemos con la lógica del formulario. Para empezar, usando el método fetch de javascript, voy a llamar a una API para recuperar la lista de países del mundo y lo voy a guardar en una variable global para poder usar más tarde.

Ojo las variables globales con var las estoy creando por comodidad, pero en una web real no se recomienda hacer uso de estas variables.

var countries = [];

function init() {
  fetch('https://restcountries.com/v3.1/all')
  .then(response => response.json())
  .then(data => countries = data);
}

init();

No tiene mucho misterio, se llama a la API y los resultados se convierten a objetos de javascript para mayor conveniencia.

Para este ejemplo uso una lista de países, aquí es donde tú tienes que usar la lista que quieres usar para el autocompletado, lo único que necesitas es una lista de strings o de objetos que tengan strings.

Autompletando resultados cuando el usuario escribe

Vamos ahora con lo importante de este proyecto, poder ofrecer autocompletado al usuario. Lo primero es escuchar el evento del input de presionar y soltar teclas al escribir para poder lanzar la función que autocomplete.

var countries = [];
var inputElem = null;

function init() {
  fetch('https://restcountries.eu/rest/v2/all')
  .then(response => response.json())
  .then(data => countries = data);
  
  inputElem = document.querySelector("input");
  inputElem.addEventListener("keydown", (event) => {
    autocomplete(event);
  });
}

function autocomplete(event) {
  console.log("Evento keydown");
}

init();

Por el momento simplemente lo que hago es seleccionar el nodo del input mediante querySelector para poder escuchar del evento keydown. Dentro del keydown recojo el evento y se lo paso a una función que me he creado que servirá para autocompletar los resultados.

var countries = [];
var inputElem = null;
var resultsElem = null;

function init() {
  fetch('https://restcountries.com/v3.1/all')
  .then(response => response.json())
  .then(data => countries = data);
  
  resultsElem = document.querySelector("ul");
  inputElem = document.querySelector("input");
  inputElem.addEventListener("keydown", (event) => {
    autocomplete(event);
  });
}

function autocomplete(event) {
  const value = inputElem.value;
  const results = countries.filter(country => {
    return country.name.common.toLowerCase().startsWith(value.toLowerCase());
  })
  
  resultsElem.innerHTML = results.map((result, index) => {
      const isSelected = index === 0;
      return `
        <li
          id='autocomplete-result-${index}'
          class='autocomplete-result${isSelected ? ' selected' : ''}'
          role='option'
          ${isSelected ? "aria-selected='true'" : ''}
        >
          ${result.name.common}
        </li>
      `
    }).join('');
   resultsElem.classList.remove('hidden');
}

init();

Con el código de arriba el formulario ya se autocompleta al empezar a escribir nombres de países. Simplemente lo que hago es, dentro de la fucnión de autocompletado, hacer un filtrado de los países cogiendo el valor que ha introducido el usuario.

Una vez filtrados los países se recorren con un map para poder crear cada uno de los elementos a insertar en los resultados del autocompletado.

También he aprovechado a poner una clase especial si el elemento está seleccionado (por defecto se selecciona el primero) y para poner el aria de elemento seleccionado para mejorar la accesibilidad. Por último se elimina la clase hidden para que se muestre la lista de resultados.

Con eso ya se muestran los resultados del autocompletados pero todavía no es funcinal porque al hacer click no se rellena el input con el autocompletado seleccionado. Vamos con ello:

var countries = [];
var inputElem = null;
var resultsElem = null;

function init() {
  fetch('https://restcountries.com/v3.1/all')
  .then(response => response.json())
  .then(data => countries = data);
  
  resultsElem = document.querySelector("ul");
  inputElem = document.querySelector("input");
  
  resultsElem.addEventListener("click", (event) => {
    handleResultClick(event);
  });
  inputElem.addEventListener("keydown", (event) => {
    autocomplete(event);
  });
}

function autocomplete(event) {
  const value = inputElem.value;
  const results = countries.filter(country => {
    return country.name.common.toLowerCase().startsWith(value.toLowerCase());
  })
  
  resultsElem.innerHTML = results.map((result, index) => {
      const isSelected = index === 0;
      return `
        <li
          id='autocomplete-result-${index}'
          class='autocomplete-result${isSelected ? ' selected' : ''}'
          role='option'
          ${isSelected ? "aria-selected='true'" : ''}
        >
          ${result.name.common}
        </li>
      `
    }).join('');
   resultsElem.classList.remove('hidden');
}

function handleResultClick() {
  if (event.target && event.target.nodeName === 'LI') {
    selectItem(event.target)
  }
}

function selectItem(node) {
  if (node) {
    inputElem.value = node.innerText;
    hideResults();
  }
}

function hideResults() {
  this.resultsElem.innerHTML = '';
  this.resultsElem.classList.add('hidden');
}

init();

He creado tres funciones nuevas. La primera función se ejecuta en el evento click en la lista de resultados, la segunda función sirve para poder seleccionar items, es decir, para sustituir el resultado del autocompletado en el input y la tercera sirve para hacer desaparecer la lista de resultados.

Con esto al clickar ya se coloca en el input la opción escogida.

Como primera versión sería pasable, pero faltan muchas cosas por hacer.

Lo siguiente que quiero añadir sería que al escribir, por defecto se rellene el input con la primera opción del formulario, para mayor comodidad. Vamos con ello.

var countries = [];
var inputElem = null;
var resultsElem = null;

function init() {
  fetch("https://restcountries.com/v3.1/all")
    .then((response) => response.json())
    .then((data) => (countries = data));

  resultsElem = document.querySelector("ul");
  inputElem = document.querySelector("input");

  resultsElem.addEventListener("click", (event) => {
    handleResultClick(event);
  });
  inputElem.addEventListener("input", (event) => {
    autocomplete(event);
  });
  inputElem.addEventListener("keyup", (event) => {
    handleResultKeyDown(event);
  });
}

function autocomplete(event) {
  const value = inputElem.value;
  if (!value) {
    hideResults();
    inputElem.value = "";
    return;
  }
  const results = countries.filter((country) => {
    return country.name.common.toLowerCase().startsWith(value.toLowerCase());
  });

  resultsElem.innerHTML = results
    .map((result, index) => {
      const isSelected = index === 0;
      return `
        <li
          id='autocomplete-result-${index}'
          class='autocomplete-result${isSelected ? " selected" : ""}'
          role='option'
          ${isSelected ? "aria-selected='true'" : ""}
        >
          ${result.name.common}
        </li>
      `;
    })
    .join("");
  resultsElem.classList.remove("hidden");
}

function handleResultClick() {
  if (event.target && event.target.nodeName === "LI") {
    selectItem(event.target);
  }
}
function handleResultKeyDown(event) {
  const { key } = event;
  switch (key) {
    case "Backspace":
      return;
    default:
      selectFirstResult();
  }
}

function selectFirstResult() {
  const value = inputElem.value;
  const autocompleteValue = resultsElem.querySelector(".selected");
  if (!value || !autocompleteValue) {
    return;
  }
  if (value !== autocompleteValue.innerText) {
    inputElem.value = autocompleteValue.innerText;
    inputElem.setSelectionRange(
      value.length,
      autocompleteValue.innerText.length
    );
  }
}
function selectItem(node) {
  if (node) {
    inputElem.value = node.innerText;
    hideResults();
  }
}

function hideResults() {
  this.resultsElem.innerHTML = "";
  this.resultsElem.classList.add("hidden");
}

init();

Lo primero que he hecho es modificar el evento que había añadido antes de keydown para usar el evento input, ya que con el evento keydown no tienes el valor escrito por el usuario actualizado, tienes el anterior valor.

Luego he creado un nuevo evento de keyup para controlar cuando el usuario termina de apretar una tecla. En ese evento se mira si la tecla es el backspace, es decir, la tecla de borrar, para en ese caso no hacer nada. Si es cualquier otra tecla se llama a la función de selectFirstResult para seleccionar por defecto el primer valor de la lista de resultados.

Dentro de ese método simplemente se selecciona el primer elemento seleccionado de la lista de resultados y se hace que el valor del input corresponda a ese valor. Además se llama a la función de setSelectionRange para que aparezca resaltado el texto del input para indicar al usuario que puede seguir escribiendo o puede pulsar la tecla hacia la derecha en el teclado para aceptar el autocompletado sin usar el ratón.

Mejorando la accesibilidad

Para el tema de la accesibilidad me voy a usar la guía de buenas prácticas de la organización W3, en concreto las propuestas para un combo box:

https://www.w3.org/TR/wai-aria-practices-1.1/#combobox

Siguiendo loa guía vamos ahora a programar los controles por teclado del autocompletado. Es decir, necesitamos hacer que_

  • Al pusar la flecha hacia abajo ↓ del teclado, se debería poder navegar entre las sugerencias de autocompletado.
  • Al pusar sobre la flecha hacia arriba ↑ también se debería poder navegar entre las sugerencias.
  • Con la tecla de Escape se esconden las sugerencias y en el input se queda seleccionado el resultado marcado.
  • Con la tecla Intro se debería poder aceptar la sugerencia marcada.

Vamos primero a hacer lo de la tecla Escape que es lo más sencillo. Simplemente voy a modificar el handleResultKeyDown que usa el evento de keyup que creamos antes:

function handleResultKeyDown(event) {
  const { key } = event;
  switch (key) {
    case "Backspace":
      return;
     case "Escape":
       hideResults();
       inputElem.value = "";
      return;
     default:
      selectFirstResult();
  }
}

Para crear la lógica de las flechas voy a usar esa misma función. Voy a crear una variable global llamada activeIndex que sirva para saber el índice de la lista de resultados seleccionado. También voy a crear otra variable global llamada filteredResults que sirva para guardar los resultados filtrados tras cuando el usuario escribe, de esa forma sabemos los items que se muestran para poder crear la lógica de recorrerlos con el teclado.

Todo el javascript quedaría así:

var countries = [];
var inputElem = null;
var resultsElem = null;
var activeIndex = 0;
var filteredResults = [];

function init() {
  fetch("https://restcountries.com/v3.1/all")
    .then((response) => response.json())
    .then((data) => (countries = data));

  resultsElem = document.querySelector("ul");
  inputElem = document.querySelector("input");

  resultsElem.addEventListener("click", (event) => {
    handleResultClick(event);
  });
  inputElem.addEventListener("input", (event) => {
    autocomplete(event);
  });
  inputElem.addEventListener("keyup", (event) => {
    handleResultKeyDown(event);
  });
}

function autocomplete(event) {
  const value = inputElem.value;
  if (!value) {
    hideResults();
    inputElem.value = "";
    return;
  }
  filteredResults = countries.filter((country) => {
    return country.name.common.toLowerCase().startsWith(value.toLowerCase());
  });

  resultsElem.innerHTML = filteredResults
    .map((result, index) => {
      const isSelected = index === 0;
      return `
        <li
          id='autocomplete-result-${index}'
          class='autocomplete-result${isSelected ? " selected" : ""}'
          role='option'
          ${isSelected ? "aria-selected='true'" : ""}
        >
          ${result.name.common}
        </li>
      `;
    })
    .join("");
  resultsElem.classList.remove("hidden");
}

function handleResultClick() {
  if (event.target && event.target.nodeName === "LI") {
    selectItem(event.target);
  }
}
function handleResultKeyDown(event) {
  const { key } = event;
  const activeItem = this.getItemAt(activeIndex);
  if (activeItem) {
   activeItem.classList.remove('selected');
   activeItem.setAttribute('aria-selected', 'false');
  }
  switch (key) {
    case "Backspace":
      return;
    case "Escape":
      hideResults();
      inputElem.value = "";
      return;
    case "ArrowUp": {
      if (activeIndex === 0) {
        activeIndex = filteredResults.length - 1;
      }
      activeIndex--;
      break;
    }
    case "ArrowDown": {
      if (activeIndex === filteredResults.length - 1) {
        activeIndex = 0;
      }
      activeIndex++;
      break;
    }
    default:
      selectFirstResult();
  }
  console.log(activeIndex);
  selectResult();
}
function selectFirstResult() {
  activeIndex = 0;
}

function selectResult() {
  const value = inputElem.value;
  const autocompleteValue = filteredResults[activeIndex].name.common;
  const activeItem = this.getItemAt(activeIndex);
  if (activeItem) {
   activeItem.classList.add('selected');
   activeItem.setAttribute('aria-selected', 'true');
  }
  if (!value || !autocompleteValue) {
    return;
  }
  if (value !== autocompleteValue) {
    inputElem.value = autocompleteValue;
    inputElem.setSelectionRange(value.length, autocompleteValue.length);
  }
}
function selectItem(node) {
  if (node) {
    console.log(node);
    inputElem.value = node.innerText;
    hideResults();
  }
}

function hideResults() {
  this.resultsElem.innerHTML = "";
  this.resultsElem.classList.add("hidden");
}

function getItemAt(index) {
  return this.resultsElem.querySelector(`#autocomplete-result-${index}`)
}

init();

He modificado el método de selectResult para que sirva para seleccionar el item que marque la variable de activeIndex. Además ese método aprovecha y mete la clase de selected y el aria-selected a true para ayudar a los lectores de pantalla.

Dentro de la función de handleResultKeyDown hago la lógica de las flechas del teclado. Lo primero que hago es limpiar la clase selected y el aria-selected para que se deseleccione lo que había anteriormente.

Dentro del switch detecto la flecha pulsada y incremento o decremento la variable activeIndex. Miro si la variable es 0 al decrementar para poner activeIndex al final de la lista y al incrementar miro si ya estamos al final para volver al principio. Por último en ese método se llama a selectResult para que se seleccione el elemento marcado por activeIndex.

Faltaría terminar la lógica de seleccionar con el Enter pero eso te lo voy a dejar como deberes ahora que has visto el código para que pruebes si eres capaz de programarlo.

El resultado final es este:

En la imagen se puede ver un input y debajo una lista de resultados para autocmpletar

Conclusiones

Hoy hemos visto una parte muy superficial de los principios de accesibilidad pero espero que al menos te haya servido para aprender algún concepto nuevo sobre este tema.

Del código que hemos creado, se que no es perfecto. Hay mucho lío en el código, nonmbres de varables que no están bien puesto, funciones que hacen más de una cosa o lógica que podría simplificarse.

Si crees que podrías mejorar el código, crea un pen en codepen con tu propuesta y envíame el link a mi cuenta de Twitter: https://twitter.com/codingpotions

Me hace mucha ilusión que la gente me envíe sus propuestas porque siempre se aprende de otros programadores y me sirve para darme cuenta de que se puede llegar al mismo resultado por varios caminos.

Te dejo el link del proyecto que he hecho yo con el código que has visto en este artículo:

https://codepen.io/Frostq/pen/oNxygGY

» Leer más, comentarios, etc...

Blog Bitix

Artículo #9 de Yo apoyo al software libre

enero 13, 2022 10:00

Con los ingresos por publicidad de AdSense, afiliado de Amazon y artículos patrocinados desde hace unos años hago una donación a los programas que utilizo habitualmente o me resultan interesantes. El gestor de contraseñas KeePassXC es uno de los programas que utilizo y me es muy útil para guardar de forma segura las credenciales de inicio de sesión en varios servicios.

Mucho del software libre además de libre que proporciona a los usuarios varios derechos sobre el programa generalmente es gratuito. Y mucho del software libre es desarrollado sin el objetivo principal de monetizarlo en el tiempo de ocio de sus desarrolladores. Al contrario del software comercial que suelen tener una licencia privativa que entre otras cosas no permite acceso al código fuente para estudiarlo, modificarlo no compartir los cambios con otros usuarios también el software comercial suele tener el objetivo de monetizarlo, requieren pagar una licencia anual y habitualmente cuenta con el soporte de una empresa.

Los desarrolladores de software libre recurren a un modelo de donaciones en el que instan a los usuarios a realizar una pequeña donación según lo que consideren. En algunos casos estas donaciones sirven para cubrir los gastos que tienen como la compra del dominio o el hospedaje para la página web del proyecto.

El problema de los desarrolladores surge cuando hay empresas que se basan y lucran con el trabajo de los desarrolladores de software libre y código abierto sin proporcionar ayuda económica a los desarrolladores ni devuelven nada a cambio, los desarrolladores pueden llegar a cansarse de trabajar gratuitamente.

Desde hace un tiempo y con una pequeña parte de los ingresos del blog tengo la costumbre de hacer una donación al menos cada año a alguno de los programas que uso y que me resultan muy útiles, la donación que hago es sin lugar a dudas mucho menor que si usase una alternativa equivalente comercial pero al menos espero contribuir un poco a que estos programas sigan teniendo nuevas versiones con nuevas características y corrigiendo errores.

KeePassXC

KeePassXC es una aplicación que proporciona la funcionalidad de generar y almacenar las credenciales de inicio de sesión de cualquier servicio en una base de datos cifrada protegida por una contraseña o archivo de clave maestro. Esta aplicación de gestor de contraseñas permite mantener varias buenas prácticas de seguridad como generar contraseñas fuertes del número de caracteres que se desee utilizando una combinación de letras, números y símbolos, utilizar una contraseña única para cada servicio, soporta generar códigos del segundo factor de autenticación que hace mucho más difícil ser víctima de un ataque de phishing y permite asociar a cada apunte notas y archivos adjuntos que también se guardan cifrados entre otras funcionalidades.

En vez de utilizar una única contraseña para todos los servicios o guardar las contraseñas en un archivo de texto sin cifrar es recomendable utilizar una aplicación de gestor de contraseñas como KeePassXC. Es una aplicación de software libre y dispone de versiones para GNU/Linux, Windows y macOS.

Ya colaboro realizando la traducción de la aplicación al español, ya he escrito un artículo sobre esa aplicación para describirla con las ventajas de seguridad que proporciona y es el gestor de contraseñas que utilizo para guardar las credenciales, generar contraseñas y generar los códigos de segundo factor de autenticación de varias cuentas.

Como es una aplicación muy útil para mi y uso de forma habitual hago una pequeña donación que no es muy grande pero seguro que los desarrolladores lo agradecen y espero que sirva como motivación para que sigan manteniendo este programa que se suma a las donaciones que he realizado con anterioridad a otros programas que utilizo o proyectos que me resultan interesantes.

KeePassXC

Comprobante de la donación

Donación KeePassXC

Donación KeePassXC
Donaciones que he realizado hasta la última fecha
# Fecha Proyecto Donación
1 2015/12 Free Software Foundation Europe (FSFE) 40 €
2 2016/09 Wikipedia, Mozilla/Firefox 10 €, 10 €
3 2017/01 elementaryOS, Libre Office, Arch Linux ARM 10 €, 10 €, 10 €
4 2017/05 GNOME, VideoLAN (VLC), Arch Linux 15,31 €, 10 €, 0,31 €
5 2018/01 LineageOS, Replicant 15 €, 15 €
6 2018/12 Wine $20
7 2019/12 Calibre $10
8 2020/12 Phoronix, Mozilla/Firefox, GnuPG $12, 10 €, 10 €
9 2022/01 KeePassXC 15 €
Total 180 €, $42

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 469

enero 11, 2022 09:43

Enlaces interesantes

Vamos con la primera colección del año :) Aquí van los enlaces recopilados durante estas fiestas que, como de costumbre, espero que os resulten interesantes.

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET / Blazor

    Conceptos / Patrones / Buenas prácticas

    Data

    Machine learning / IA / Bots

    Web / HTML / CSS / Javascript

    Visual Studio / Complementos / Herramientas

    Xamarin / .NET MAUI

    Otros

    Publicado en Variable not found.

    » Leer más, comentarios, etc...

    Header Files

    Patrón Factory

    enero 11, 2022 12:10

    Introducción

    El patrón factory nos permite la creación de objetos de un subtipo concreto. Entre las diversas ventajas de este patrón, resaltaré dos:

    • Evita exponer la implementación de los subtipos.
    • Permite diferir la elección del subtipo al tiempo de ejecución, por ejemplo, basándose en un identificador de tipo.

    Implementación

    Para ilustrar este patrón de diseño expondré cómo podemos crear una factoría de formas geométricas. A saber, el cliente tiene expuestos los ficheros shape.h (donde se declara una interfaz IShape que cumpliran todos los objetos) y factory.h (donde se define la función factoría que creará dichos objetos). Los subtipos específicos no están expuestos a los clientes; para construirlos hay que invocar a la función factoría make_shape con el identificador del tipo deseado: triangle, square, circle.

    std::unique_ptr<IShape> make_shape(std::string const& id);
    
    // ...
    auto triangle = make_shape("triangle");
    

    Propuesta inicial

    Una primera aproximación sería definir el método make_shape a pelo, listando todas los subtipos soportados:

    #include "factory.h"
    #include "Triangle.h"
    #include "Square.h"
    #include "Circle.h"
    
    std::unique_ptr<IShape> make_shape(std::string const& id)
    {
        if (id == "triangle") { return std::make_unique<Triangle>(); }
        if (id == "square") { return std::make_unique<Square>(); }
        if (id == "circle") { return std::make_unique<Circle>(); }
        return nullptr;
    }
    

    Primera mejora: extracción del ID

    Esta implementación básica es suficiente para cubrir las necesidades más inmediatas. Vamos a buscarle el primer pero: el identificador del tipo no debería estar en la factoría, sino con el tipo. De este modo si el identificador se necesita en otro lugar (construcción de un JSON, datos de depuración o log, etc.), no habría que duplicarlo.

    Supongamos que se establece que todas las sub-clases de IShape deben definir un método estático id() que nos devuelva el ID del objeto. De esta forma podríamos tener un código más independiente:

    std::unique_ptr<IShape> make_shape(std::string const& id)
    {
        if (id == Triangle::id()) { return std::make_unique<Triangle>(); }
        if (id == Square::id()) { return std::make_unique<Square>(); }
        if (id == Circle::id()) { return std::make_unique<Circle>(); }
        return nullptr;
    }
    

    Esto mejora mucho la parte del ID, pero nos deja ahora el segundo pero: la duplicación de código.

    Segunda mejora: automatizar la factoría

    La duplicación de código antes mencionada puede ser evitada mediante un proceso que se llama registro de clases. En éste se asocian los ID de las clases con un método creador, de forma que dicho método puede ser usado a posteriori. Para ello crearemos una clase privada ShapeFactory que llevará un registro de todas las clases soportadas:

    class ShapeFactory
    {
    public:
        ShapeFactory()
        {
            register_class<Triangle>();
            register_class<Square>();
            register_class<Circle>();
        }
    
        std::unique_ptr<IShape> make(std::string const& id) const
        {
            auto it = m_creators.find(id);
            if (it == m_creators.end())
            {
                // ID not found
                return nullptr;
            }
    
            return it->second();
        }
    
    private:
        template<class T>
        void register_class()
        {
            m_creators.emplace(T::id(), []() { return std::make_unique<T>(); });
        }
    
        std::map<std::string, std::function<std::unique_ptr<IShape()>>> m_creators;
    };
    
    std::unique_ptr<IShape> make_shape(std::string const& id)
    {
        static ShapeFactory factory{};
        return factory.make(id);
    }
    

    Se puede probar un ejemplo en Coliru.

    Tercera mejora: factoría genérica

    Imaginemos que en nuestro código tenemos un par de docenas de interfaces a las que queremos asociar un método factory. La factoría antes presentada funciona muy bien para crear objetos cuyo subtipo implementa la interfaz IShape, pero no para otros tipos, por lo que tendríamos que duplicar dicho código para cada nueva interfaz y los subtipos asociados.

    Si examinamos detenidamente la implementación de la ShapeFactory, podremos ver rápidamente que es fácilmente generalizable si convertimos la clase factoría en una clase templatizada. Usando variadic templates para especializar la factoría con múltiples tipos asociados tenemos algo como:

    template<class... Ts>
    class Factory
    {
        // ...
    };
    
    std::unique_ptr<IShape> make_shape(std::string const& id)
    {
        using factory_t = Factory<Triangle, Square, Circle>;
        static factory_t factory{};
        return factory.make(id);
    }
    

    Ahora, el problema radica en registrar las clases de la factoría. Para ello, necesitamos poder iterar sobre todos los tipos indicados. Esto se consigue mediante una función tonta (vacía) que nos haga las veces de expansor, llamándola con una función que se ejecute una vez para cada tipo de dato en el template. El truco acá consiste en que dicha función debe devolver un valor (cualquier cosa menos void), para poder expandirse como lista de argumentos.

    template<class... Ts>
    class Factory
    {
    public:
        Factory()
        {
            register_all(register_class<Ts>()...);
        }
    
        // ...
    
    private:
        template<class... Ts>
        void register_all(Ts...)
        {
        }
    
        template<class T>
        auto register_class()
        {
            return m_creators.emplace(T::id(), []() { return std::make_unique<T>(); }).second;
        }
    
        // ...
    };
    

    En este caso el método register_class devuelve un booleano indicado si la clase aún no había sido registrada, aunque realmente se usa únicamente para el truco de la expansión de parámetros, no para una verificación real.

    Dicho lo anterior, ahora se pueden crear tantos métodos factory como se deseen con una mínima inversión: únicamente hay que especializar la clase Factory en el método factoría de cada interfaz.

    Como nota final, podéis consultar un ejemplo completo operativo de patrón factory en Coliru.

    » Leer más, comentarios, etc...

    Coding Potions

    ¿Qué es Javascript? ¿Para qué sirve?

    enero 11, 2022 12:00

    Javascript (o JS para abreviar) es un lenguaje de programación, concretamente un lenguaje pensado para las páginas webs. Si hoy en día quieres crear una web y quieres cargar datos o que tenga algún tipo de interacción con el usuario no te queda más remedio que aprender Javascript.

    Logotipo de Javascript, aparece fondo en amarillo y las letras JS

    Si te interesa el mundo del desarrollo web te recomiendo que te pases por el artículo de Frontend.

    Cuando abres una web, es muy normal que se hagan peticiones a archivos .js que directamente se ejecutan por el navegador, es por eso que Javascript es un lenguaje interpretado. El navegador ejecuta directamente las instrucciones en Javascript sin tener que esperar a compilar el código.

    En Javascript, como en otros lenguajes, existe una serie de librerías que puedes instalar en un proyecto. El gestor de librerías más usados es NPM, que tiene su propio repositorio del que se instalan todas las librerías.

    Existe también una forma de ejecutar Javascript en servidor sin tener que depender un navegador, se llama NodeJS . Aunque hay algunas cosas que hay que cambiar en el código la realidad es que Javascript y Node son bastante parecidos en cuanto a sintaxis.

    De dónde viene el nombre de Javascript

    Al principio Javascript se llamaba Mocha (en 1995). Meses después se paso a llamar LiveScript, y cuando Netscape y Sun se juntaron decidieron cambiarle el nombre por Javascript.

    Sus creadores dicen que por aquél entonces Java estaba muy de moda, y que la idea de Javascript era sacar un lenguaje complementario a Java pero para la web.

    Si te interesa más el tema estoy preparando el artículo con la Historia de Javascript

    Para qué sirve Javascript

    Como he explicado antes, Javascript se usa para crear webs, en concreto la parte de la lógica de la web (modificar y crear datos, hacer llamadas a servidores, interacción con el usuario, etc). La parte visual de la web se suele hacer con HTML y CSS .

    Con la llegada de Node y de Electron (mecanismo para crear aplicaciones de escritorio con lenguajes web) Javascript se ha popularizado y ya es usado en todo tipo de entornos y para variedad de cosas.

    Por ejemplo, existen incluso formas (con librerías), de implementar aplicaciones para Android e IOS usando Javascript.

    Se puede usar Javascript para implementar juegos también, que van a tener la ventaja de que se pueden jugar directamente desde una página web, sin instalar nada.

    Si te quieres dedicar a crear webs yo diría que Javascript es casi obligatorio, por mucho que uses frameworks o librerías al final vas a tener que saber hacer cosas con Javascript.

    Si te quieres dedicar a hacer Backend , es decir, cosas en servidor, puedes aprender Javascript y Node perfectamente porque hay bastante trabajo. Python o Java son también buenas opciones para aprender.

    » Leer más, comentarios, etc...

    Variable not found

    Top ten 2021 en Variable Not Found

    enero 10, 2022 07:51

    Top ten 2020 en Variable not found

    Pues parece que comenzamos 2022 con deseos bastante parecidos a los que ya teníamos un año atrás: que la situación actual mejore y que la salud os permita ser felices y disfrutar de todo lo que os rodea. Algo hemos avanzado respecto al año pasado, pero aún nos queda mucho camino por recorrer para volver a la ansiada normalidad.

    Pero bueno, independientemente de esto, seguro que lo que más estáis esperando en este momento es el tradicional post donde comentaremos las entradas más visitadas durante el pasado año, así que ahí va 😁

    Top ten 2021 en Variable not found

    Comenzamos el ranking por el décimo puesto, donde encontramos ¿Por qué llamamos "uppercase" y "lowercase" a mayúsculas y minúsculas?, una curiosidad histórica que explica por qué solemos usar estos términos. Spoiler: aunque pueda parecer increíble, tiene que ver con las imprentas ;)

    A continuación, en novena posición está el post ¿Cuánto pesa realmente una aplicación Blazor WebAssembly recién creada?, un recorrido por distintos escenarios en el que mostrábamos el peso de las aplicaciones Blazor WebAssembly y cómo éste puede variar en función de determinadas configuraciones. Si trabajáis con este modelo de hosting de Blazor, creo que no deberíais perdéroslo.

    En ¿Por qué no compila este código C#?, en el puesto número ocho del ranking, usábamos (con bastante maldad, he de decir) un caso límite del parseador de C# para someteros a un pequeño examen de atención. ¿Fuisteis capaces de descubrir por qué fallaba la compilación en un par de líneas de código? Aún estáis a tiempo de superar la prueba 😉

    La séptima posición la ocupa el post Cómo evitar que entren argumentos nulos en métodos de C#: un recorrido histórico (y lo que nos trae el futuro), donde revisábamos distintas técnicas que nos ayudan a evitar la entrada de valores nulos a nuestros métodos. Porque aunque parezca algo trivial, no está de más ver las novedades que C# y .NET han indo incluyendo a ese respecto, y lo que aún está por venir.

    En programación moderna, la instanciación de objetos es una tarea que solemos delegar al proveedor de servicios de .NET. Y aunque normalmente utilizaremos inyección de dependencias, hay veces que nos interesa hacerlo de forma manual, aunque aprovechando las ventajas de tener por detrás toda la potencia del proveedor de servicios. En el post Crear manualmente instancias de clases usando el proveedor de servicios de .NET descubríamos una pequeña y poco conocida clase de utilidad de .NET que permitía hacerlo de forma bastante sencilla.

    Justo en la mitad de la lista encontramos Syncfusion Blazor UI components library: un primer vistazo, un post patrocinado por Syncfusion donde echamos un rápido vistazo a su magnífica suite de componentes profesionales para Blazor: Grids, gráficas, botones, _dropdowns, calendarios... una maravilla tanto por el número de componentes como por su calidad.

    La cuarta posición sigue demuestra el tirón que tienen las novedades de C# que acompañan a las nuevas versiones del lenguaje. En este caso, el post Un vistazo a los patrones relacionales y combinadores, lo nuevo de C# 9 para exprimir el pattern matching encontramos que escribir algo como var x = i is 3 or 4 or >=5; es posible en nuestro lenguaje favorito.

    Ya entrando en los primeros puestos, en tercer lugar aparece el artículo C# source generators: un ejemplo sencillo, paso a paso. Los generadores de código o source generators son una fórmula superpotente de llevar a compilación tareas de metaprogramación que tradicionalmente se realizaban en tiempo de ejecución, y no debéis perdéroslo porque han llegado para quedarse.

    Por cierto, si os interesa el tema, podéis encontrar aquí un buen número de source generators que probablemente os vengan bien curiosear.

    La segunda posición del ranking la ocupa la interesante capacidad de C# 9 para implementar iteradores sobre prácticamente cualquier tipo de objeto. El post Iterar con foreach sobre cualquier tipo de objeto con C#9 mostraba que era posible implementar algo espectacularmente limpio como esto:

    foreach (var i in 1..10) Console.WriteLine(i);

    Por último, en primer lugar del top ten encontramos algo que demuestra que, como hacíamos en el siglo XX, seguimos dedicando gran parte de nuestro tiempo a implementar altas, bajas y modificaciones ;): el post CRUD en Blazor usando el componente DataGrid de Syncfusion, patrocinado por Syncfusion, mostraba lo sencillo que resulta implementar funcionalidades de este tipo usando la rejilla de esta suite.

    ¡Vamos a por 2022, y mucho ánimo, que la cosa ya sólo puede ir a mejor!

    Publicado en Variable not found.

    » Leer más, comentarios, etc...

    Blog Bitix

    Analizar y detectar fallos de seguridad en las dependencias de Java

    enero 06, 2022 10:00

    Dada la complejidad de muchas aplicaciones de software hace que estas usen gran cantidad de dependencias, muchas de proyectos de software libre o código abierto. Es muy probable que con el paso del tiempo en alguna de las dependencias de las aplicaciones se descubra un error de seguridad importante y requiera una actualización lo más pronto posible. No es posible estar completamente a salvo de estar afectado por un fallo de seguridad en el software por mucho que se intente, por este motivo la mejor estrategia es detectar proactivamente y temprana los errores de seguridad y actualizar las dependencias a nuevas versiones con el fallo corregido. Varias organizaciones identifican, definen, describen y catalogan los fallos de seguridad de forma pública en una base de datos y hay herramientas automatizadas que con las bases de datos de fallos de seguridad permiten analizar las dependencias de un proyecto. Un ejemplo ha sido el caso de la librería Log4j 2 que por su gravedad y y popularidad muchas organizaciones han estado afectadas.

    Java

    Una vez desarrollada una aplicación esta entra en un modo de mantenimiento en el que se añaden nuevas funcionalidades a las existentes y se corrigen errores. La mayor parte de la vida de una aplicación es empleada en su mantenimiento con generalmente pequeños cambios incrementales. Algunas aplicaciones son empleadas durante periodos de tiempo muy largos, de lustros o décadas, que quizá ya se consideren como heredadas y en las que ya únicamente se hacen cambios en caso de errores graves.

    Aunque en una aplicación heredada ya no se hagan mejoras, ni se actualicen versiones mayores de librerías otro de los motivos por los que una aplicación requiere mantenimiento es por fallos de seguridad. Con el paso del tiempo es muy posible que en una aplicación que tenga dependencias de versiones antiguas de librerías se descubran fallos de seguridad. Si es posible y dependiendo de la gravedad del fallo de seguridad descubierto y la forma de explotarlo es conveniente actualizar a la última versión de la librería o al menos a la última versión compatible con el fallo de seguridad corregido. En una aplicación heredada quizá no sea posible actualizar a la última versión ya que posiblemente por un lado requiere cambios importantes en el código y tiempo para hacerlos y por otro lado se trate de evitar hacer cambios para no introducir errores en el código que está funcionando.

    Una de las formas de analizar el código fuente de una aplicación es analizar sus dependencias para conocer si en alguna de ellas se descubre alguna vulnerabilidad. Hay herramientas automatizadas que realizan las dependencias y generan un informe con las vulnerabilidades que tienen. En este caso el análisis estático de código se hace sobre las dependencias sobre el código fuente al igual que las comprobaciones que también se pueden hacer sobre el código para comprobar que cumple las convenciones, algunas restricciones y algunos fallos detectables sobre el código fuente que se pueden hacer con PMD.

    Contenido del artículo

    Base de datos de fallos de seguridad

    Los fallos de seguridad descubiertos se identifican, definen y catalogan con un nombre y se añaden a una base de datos pública de vulnerabilidades de seguridad. Al definir los fallos de seguridad se les asigna un nivel orientativo de gravedad, dos propiedades importantes que sirven para asignar la gravedad son como es la forma de explotar el fallo de seguridad, si requiere acceso físico al sistema o es posible explotarlo de forma remota, y que permite el fallo de seguridad, como ejecución de código remoto o obtención de información confidencial.

    Aunque algunos fallos de seguridad permiten la ejecución remota de código no se consideran tan importantes si requieren acceso físico al sistema. Los más graves son aquellos que concurren ambas circunstancias, permiten explotar los fallos de seguridad de forma remota y permite realizar acciones graves como ejecución de código remoto, escalar privilegios u obtener información confidencial.

    Por otro lado, la organización OWASP tiene documentados fallos de seguridad comunes en las aplicaciones y que conviene evitar, por ejemplo, el error de sql injection o cross site scripting que no por ser ya muy conocidos y no complicados de evitar dejan de ser graves si la aplicación no se implementa adecuadamente.

    El problema de seguridad de Log4j 2

    Un caso de error grave de seguridad denominado identificado con el nivel máximo en la escala de gravedad es el de la librería Log4j 2 en las versiones menores a 2.3.2 (para Java 6), 2.12.4 (para Java 7) y 2.17.1 (para Java 8 y posteriores) que es posible explotarlo de forma remota y permite ejecución remota de código denominado Log4Shell. Log4j 2 es una librería de Java muy utilizada en los proyectos por ser una funcionalidad fundamental para cualquier aplicación que sirve para emitir trazas o logging.

    Dada la gravedad del error descubierto y el amplio uso de la librería en los proyectos Java muchas organizaciones se han visto afectadas por el error de seguridad. La corrección del error requiere actualizar la versión de la dependencia de Log4j a una que no sea vulnerable al error. El problema es que muchas aplicaciones heredadas actualizar a la última versión no es posible e incluso actualizar a una versión compatible no vulnerable supone gran esfuerzo que requiere actualizar la dependencia en el código fuente, generar el nuevo artefacto, validarlo y hacer su despliegue en el entorno de producción.

    Mientras se realiza la corrección conviene observar los registros de trazas, el uso de la CPU, red, memoria, almacenamiento y registros de log ante cualquier comportamiento anómalo para ver si la aplicación está siendo objeto de ataque.

    Aún siendo Log4j una librería mantenida por tres personas de forma voluntaria su licencia de código abierto y alta calidad que muchas veces es mayor incluso que las opciones equivalentes comerciales es utilizada por muchas empresas incluso con facturaciones mil millonarias debido a que no necesitan pagar licencias de software para usarla. Sin embargo, no todas las empresas mil millonarias que usan un software que es vital para su negocio apoyan económicamente a esos proyectos de software que usan. Aún así, esos tres voluntarios pocas horas después de hacerse público el error con la ayuda de los interesados han publicado varias versiones de la librería con el fallo original y posteriores descubiertos corregidos.

    Esta misma historia ya se repitió en el 2014 con OpenSSL con el denominado Heartbleed y se volverá a repetir con otro ejemplo en el futuro. Proyectos en los que no solo se fundamenta ya una empresa sino en los que se fundamenta internet cuyos desarrolladores trabajan de forma voluntaria sin apoyo económico.

    Logotipo de Log4Shell

    Logotipo de Log4Shell
    Fuente: https://www.lunasec.io/

    Analizar y detectar fallos de seguridad en las dependencias de Java con Gradle y Maven

    Dado que se volverá a repetir un fallo de seguridad como Log4 2 o Heartbleed y dado que es imposible estar seguro de que una dependencia no se vea afectada en algún momento por un fallo grave de seguridad conviene estar suscrito a los boletines de seguridad y analizar las dependencias, automatizar el análisis de las dependencias es la mejor opción para que la mayor parte del trabajo lo hagan las computadoras en vez de personas y detectar los fallos de seguridad en cuanto sean publicados.

    La misma organización OWASP proporciona una herramienta automatizada para comprobar la seguridad de las dependencias de un proyecto. La herramienta se usa como un complemento en las herramientas de construcción Gradle o Maven y al ejecutar las tareas que añaden analizan las dependencias y versiones del proyecto y las compara con las bases de datos de errores conocidos. El resultado es un informe con una lista de las vulnerabilidades de cada librería del proyecto si es que tienen alguna. El plugin de OWASP también detecta los fallos de seguridad en las dependencias de forma transitiva a las que se declaren en el archivo de construcción de forma explícita.

    En el siguiente ejemplo de proyecto con Gradle se incluye como dependencia una versión de Log4j 2 vulnerable, con el plugin de OWASP para detectar vulnerabilidades y la tarea dependencyCheckAnalyze se identifican los CVE a los que es vulnerable cada una de las dependencias en este caso la de Log4j 2. En cada uno de los CVE y en las referencias asociadas se detalla el fallo de seguridad.

    Estos son los CVE que detecta para la versión 2.14.1.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    plugins {
        id 'application'
        id "org.owasp.dependencycheck" version "6.5.2.1"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation("org.apache.logging.log4j:log4j-core:2.14.1")
        //implementation("org.apache.logging.log4j:log4j-core:2.17.1")
    }
    
    application {
        mainClass = 'io.github.picodotdev.blogbitix.dependencycheck.App'
    }
    
    build-1.gradle
    1
    2
    
    $ ./gradlew dependencyCheckAnalyze
    
    
    gradle-dependencycheck.sh
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    > Task :app:dependencyCheckAnalyze
    Verifying dependencies for project app
    Checking for updates and analyzing dependencies for vulnerabilities
    Generating report for project app
    Found 4 vulnerabilities in project app
    
    One or more dependencies were identified with known vulnerabilities in app:
    
    log4j-core-2.14.1.jar (pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1, cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*) : CVE-2021-44228, CVE-2021-44832, CVE-2021-45046, CVE-2021-45105
    
    See the dependency-check report for more details.
    
    BUILD SUCCESSFUL in 2m 47s
    1 actionable task: 1 executed
    gradle-dependencycheck-1.out

    Detectado el fallo de seguridad basta con cambiar la versión de Log4j 2 a la última no vulnerable y el error desaparece del informe.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    plugins {
        id 'application'
        id "org.owasp.dependencycheck" version "6.5.2.1"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        //implementation("org.apache.logging.log4j:log4j-core:2.14.1")
        implementation("org.apache.logging.log4j:log4j-core:2.17.1")
    }
    
    application {
        mainClass = 'io.github.picodotdev.blogbitix.dependencycheck.App'
    }
    
    build-2.gradle
    1
    2
    3
    4
    5
    
    > Task :app:dependencyCheckAnalyze
    Verifying dependencies for project app
    Checking for updates and analyzing dependencies for vulnerabilities
    Generating report for project app
    Found 0 vulnerabilities in project app
    gradle-dependencycheck-2.out

    En un proyecto con Maven el análisis se realiza con el siguiente comando:

    1
    2
    
    $ mvn org.owasp:dependency-check-maven:6.5.2.1:check
    
    
    mvn.sh

    Analizar repositorios de Git

    En una organización con gran cantidad de repositorios de Git un error como este supone analizar cada uno de los proyectos, para automatizar la tarea el siguiente script clona los repositorios a analizar, detecta si es un repositorio Gradle o Maven y ejecuta la tarea de análisis de las dependencias.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    initscript {
        repositories {
            mavenCentral()
        }
    
        dependencies {
            classpath 'org.owasp:dependency-check-gradle:6.5.2.1'
        }
    }
    
    allprojects {
        apply plugin: org.owasp.dependencycheck.gradle.DependencyCheckPlugin
    }
    
    
    init.gradle
    1
    2
    3
    
    <?xml version="1.0" encoding="UTF-8"?>
    <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
    </settings>
    settings.xml
     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
    
    
    #!/usr/bin/env bash
    
    # Checks Maven and Gradle dependencies vulnerabilities using OWASP plugins.
    # https://owasp.org/www-project-dependency-check/
    
    WORKDIR="repositories"
    USER=picodotdev
    REPOSITORIES=()
    REGEXP="log4j-core"
    
    function main() {
        mkdir -p $WORKDIR
        cd $WORKDIR
    
        for I in ${!REPOSITORIES[@]}; do
            REPOSITORY=${REPOSITORIES[${I}]}
    
            echo "Checking $REPOSITORY (https://github.com/$USER/$REPOSITORY)"
    
            if [ ! -d "$REPOSITORY" ]; then
                git clone "git@github.com:$USER/$REPOSITORY.git"
            else
                (cd $REPOSITORY && git pull origin)
            fi
    
            if [ -f "$REPOSITORY/pom.xml" ]; then
                checkMaven $REPOSITORY
            elif [ -f "$REPOSITORY/build.gradle" -o -f "$REPOSITORY/settings.gradle" ]; then
                checkGradle $REPOSITORY
            else
                echo "Not checkeable project $REPOSITORY (not detected as Maven or Gradle project)"
            fi
        done
    }
    
    function checkMaven() {
        REPOSITORY=$1
        (cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && mvn --settings ../../settings.xml org.owasp:dependency-check-maven:6.5.2.1:check); EXIT_CODE=$?
        #(cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && mvn --settings ../../settings.xml org.owasp:dependency-check-maven:6.5.2.1:check) | grep -E "$REGEXP" | sort | uniq; EXIT_CODE=${PIPESTATUS[0]}
        if [ "$EXIT_CODE" != "0" ]; then
            echo "Check not completed with success ($EXIT_CODE)"
        fi
    }
    
    function checkGradle() {
        REPOSITORY=$1
        (cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && gradle --init-script ../../init.gradle dependencyCheckAnalyze); EXIT_CODE=$?
        #(cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && gradle --init-script ../../init.gradle dependencyCheckAnalyze) | grep -E "$REGEXP" | sort | uniq; EXIT_CODE=${PIPESTATUS[0]}
        if [ "$EXIT_CODE" != "0" ]; then
            echo "Check not completed with success ($EXIT_CODE)"
        fi
    }
    
    main
    
    owasp-vulnerabilities-check.sh
    Terminal

    El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
    ./gradlew dependencyCheckAnalyze

    » Leer más, comentarios, etc...

    Blog Bitix

    Analizar y detectar fallos de seguridad en las dependencias de Java

    enero 06, 2022 10:00

    Dada la complejidad de muchas aplicaciones de software hace que estas usen gran cantidad de dependencias, muchas de proyectos de software libre o código abierto. Es muy probable que con el paso del tiempo en alguna de las dependencias de las aplicaciones se descubra un error de seguridad importante y requiera una actualización lo más pronto posible. No es posible estar completamente a salvo de estar afectado por un fallo de seguridad en el software por mucho que se intente, por este motivo la mejor estrategia es detectar proactivamente y temprana los errores de seguridad y actualizar las dependencias a nuevas versiones con el fallo corregido. Varias organizaciones identifican, definen, describen y catalogan los fallos de seguridad de forma pública en una base de datos y hay herramientas automatizadas que con las bases de datos de fallos de seguridad permiten analizar las dependencias de un proyecto. Un ejemplo ha sido el caso de la librería Log4j 2 que por su gravedad y y popularidad muchas organizaciones han estado afectadas.

    Java

    Una vez desarrollada una aplicación esta entra en un modo de mantenimiento en el que se añaden nuevas funcionalidades a las existentes y se corrigen errores. La mayor parte de la vida de una aplicación es empleada en su mantenimiento con generalmente pequeños cambios incrementales. Algunas aplicaciones son empleadas durante periodos de tiempo muy largos, de lustros o décadas, que quizá ya se consideren como heredadas y en las que ya únicamente se hacen cambios en caso de errores graves.

    Aunque en una aplicación heredada ya no se hagan mejoras, ni se actualicen versiones mayores de librerías otro de los motivos por los que una aplicación requiere mantenimiento es por fallos de seguridad. Con el paso del tiempo es muy posible que en una aplicación que tenga dependencias de versiones antiguas de librerías se descubran fallos de seguridad. Si es posible y dependiendo de la gravedad del fallo de seguridad descubierto y la forma de explotarlo es conveniente actualizar a la última versión de la librería o al menos a la última versión compatible con el fallo de seguridad corregido. En una aplicación heredada quizá no sea posible actualizar a la última versión ya que posiblemente por un lado requiere cambios importantes en el código y tiempo para hacerlos y por otro lado se trate de evitar hacer cambios para no introducir errores en el código que está funcionando.

    Una de las formas de analizar el código fuente de una aplicación es analizar sus dependencias para conocer si en alguna de ellas se descubre alguna vulnerabilidad. Hay herramientas automatizadas que realizan las dependencias y generan un informe con las vulnerabilidades que tienen. En este caso el análisis estático de código se hace sobre las dependencias sobre el código fuente al igual que las comprobaciones que también se pueden hacer sobre el código para comprobar que cumple las convenciones, algunas restricciones y algunos fallos detectables sobre el código fuente que se pueden hacer con PMD.

    Contenido del artículo

    Base de datos de fallos de seguridad

    Los fallos de seguridad descubiertos se identifican, definen y catalogan con un nombre y se añaden a una base de datos pública de vulnerabilidades de seguridad. Al definir los fallos de seguridad se les asigna un nivel orientativo de gravedad, dos propiedades importantes que sirven para asignar la gravedad son como es la forma de explotar el fallo de seguridad, si requiere acceso físico al sistema o es posible explotarlo de forma remota, y que permite el fallo de seguridad, como ejecución de código remoto o obtención de información confidencial.

    Aunque algunos fallos de seguridad permiten la ejecución remota de código no se consideran tan importantes si requieren acceso físico al sistema. Los más graves son aquellos que concurren ambas circunstancias, permiten explotar los fallos de seguridad de forma remota y permite realizar acciones graves como ejecución de código remoto, escalar privilegios u obtener información confidencial.

    Por otro lado, la organización OWASP tiene documentados fallos de seguridad comunes en las aplicaciones y que conviene evitar, por ejemplo, el error de sql injection o cross site scripting que no por ser ya muy conocidos y no complicados de evitar dejan de ser graves si la aplicación no se implementa adecuadamente.

    El problema de seguridad de Log4j 2

    Un caso de error grave de seguridad denominado identificado con el nivel máximo en la escala de gravedad es el de la librería Log4j 2 en las versiones menores a 2.3.2 (para Java 6), 2.12.4 (para Java 7) y 2.17.1 (para Java 8 y posteriores) que es posible explotarlo de forma remota y permite ejecución remota de código denominado Log4Shell. Log4j 2 es una librería de Java muy utilizada en los proyectos por ser una funcionalidad fundamental para cualquier aplicación que sirve para emitir trazas o logging.

    Dada la gravedad del error descubierto y el amplio uso de la librería en los proyectos Java muchas organizaciones se han visto afectadas por el error de seguridad. La corrección del error requiere actualizar la versión de la dependencia de Log4j a una que no sea vulnerable al error. El problema es que muchas aplicaciones heredadas actualizar a la última versión no es posible e incluso actualizar a una versión compatible no vulnerable supone gran esfuerzo que requiere actualizar la dependencia en el código fuente, generar el nuevo artefacto, validarlo y hacer su despliegue en el entorno de producción.

    Mientras se realiza la corrección conviene observar los registros de trazas, el uso de la CPU, red, memoria, almacenamiento y registros de log ante cualquier comportamiento anómalo para ver si la aplicación está siendo objeto de ataque.

    Aún siendo Log4j una librería mantenida por tres personas de forma voluntaria su licencia de código abierto y alta calidad que muchas veces es mayor incluso que las opciones equivalentes comerciales es utilizada por muchas empresas incluso con facturaciones mil millonarias debido a que no necesitan pagar licencias de software para usarla. Sin embargo, no todas las empresas mil millonarias que usan un software que es vital para su negocio apoyan económicamente a esos proyectos de software que usan. Aún así, esos tres voluntarios pocas horas después de hacerse público el error con la ayuda de los interesados han publicado varias versiones de la librería con el fallo original y posteriores descubiertos corregidos.

    Esta misma historia ya se repitió en el 2014 con OpenSSL con el denominado Heartbleed y se volverá a repetir con otro ejemplo en el futuro. Proyectos en los que no solo se fundamenta ya una empresa sino en los que se fundamenta internet cuyos desarrolladores trabajan de forma voluntaria sin apoyo económico.

    Logotipo de Log4Shell

    Logotipo de Log4Shell
    Fuente: https://www.lunasec.io/

    Analizar y detectar fallos de seguridad en las dependencias de Java con Gradle y Maven

    Dado que se volverá a repetir un fallo de seguridad como Log4 2 o Heartbleed y dado que es imposible estar seguro de que una dependencia no se vea afectada en algún momento por un fallo grave de seguridad conviene estar suscrito a los boletines de seguridad y analizar las dependencias, automatizar el análisis de las dependencias es la mejor opción para que la mayor parte del trabajo lo hagan las computadoras en vez de personas y detectar los fallos de seguridad en cuanto sean publicados.

    La misma organización OWASP proporciona una herramienta automatizada para comprobar la seguridad de las dependencias de un proyecto. La herramienta se usa como un complemento en las herramientas de construcción Gradle o Maven y al ejecutar las tareas que añaden analizan las dependencias y versiones del proyecto y las compara con las bases de datos de errores conocidos. El resultado es un informe con una lista de las vulnerabilidades de cada librería del proyecto si es que tienen alguna. El plugin de OWASP también detecta los fallos de seguridad en las dependencias de forma transitiva a las que se declaren en el archivo de construcción de forma explícita.

    En el siguiente ejemplo de proyecto con Gradle se incluye como dependencia una versión de Log4j 2 vulnerable, con el plugin de OWASP para detectar vulnerabilidades y la tarea dependencyCheckAnalyze se identifican los CVE a los que es vulnerable cada una de las dependencias en este caso la de Log4j 2. En cada uno de los CVE y en las referencias asociadas se detalla el fallo de seguridad.

    Estos son los CVE que detecta para la versión 2.14.1.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    plugins {
        id 'application'
        id "org.owasp.dependencycheck" version "6.5.2.1"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation("org.apache.logging.log4j:log4j-core:2.14.1")
        //implementation("org.apache.logging.log4j:log4j-core:2.17.1")
    }
    
    application {
        mainClass = 'io.github.picodotdev.blogbitix.dependencycheck.App'
    }
    
    build-1.gradle
    1
    2
    
    $ ./gradlew dependencyCheckAnalyze
    
    
    gradle-dependencycheck.sh
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    > Task :app:dependencyCheckAnalyze
    Verifying dependencies for project app
    Checking for updates and analyzing dependencies for vulnerabilities
    Generating report for project app
    Found 4 vulnerabilities in project app
    
    One or more dependencies were identified with known vulnerabilities in app:
    
    log4j-core-2.14.1.jar (pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1, cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*) : CVE-2021-44228, CVE-2021-44832, CVE-2021-45046, CVE-2021-45105
    
    See the dependency-check report for more details.
    
    BUILD SUCCESSFUL in 2m 47s
    1 actionable task: 1 executed
    gradle-dependencycheck-1.out

    Detectado el fallo de seguridad basta con cambiar la versión de Log4j 2 a la última no vulnerable y el error desaparece del informe.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    plugins {
        id 'application'
        id "org.owasp.dependencycheck" version "6.5.2.1"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        //implementation("org.apache.logging.log4j:log4j-core:2.14.1")
        implementation("org.apache.logging.log4j:log4j-core:2.17.1")
    }
    
    application {
        mainClass = 'io.github.picodotdev.blogbitix.dependencycheck.App'
    }
    
    build-2.gradle
    1
    2
    3
    4
    5
    
    > Task :app:dependencyCheckAnalyze
    Verifying dependencies for project app
    Checking for updates and analyzing dependencies for vulnerabilities
    Generating report for project app
    Found 0 vulnerabilities in project app
    gradle-dependencycheck-2.out

    En un proyecto con Maven el análisis se realiza con el siguiente comando:

    1
    2
    
    $ mvn org.owasp:dependency-check-maven:6.5.2.1:check
    
    
    mvn.sh

    Analizar repositorios de Git

    En una organización con gran cantidad de repositorios de Git un error como este supone analizar cada uno de los proyectos, para automatizar la tarea el siguiente script clona los repositorios a analizar, detecta si es un repositorio Gradle o Maven y ejecuta la tarea de análisis de las dependencias.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    initscript {
        repositories {
            mavenCentral()
        }
    
        dependencies {
            classpath 'org.owasp:dependency-check-gradle:6.5.2.1'
        }
    }
    
    allprojects {
        apply plugin: org.owasp.dependencycheck.gradle.DependencyCheckPlugin
    }
    
    
    init.gradle
    1
    2
    3
    
    <?xml version="1.0" encoding="UTF-8"?>
    <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
    </settings>
    settings.xml
     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
    
    
    #!/usr/bin/env bash
    
    # Checks Maven and Gradle dependencies vulnerabilities using OWASP plugins.
    # https://owasp.org/www-project-dependency-check/
    
    WORKDIR="repositories"
    USER=picodotdev
    REPOSITORIES=()
    REGEXP="log4j-core"
    
    function main() {
        mkdir -p $WORKDIR
        cd $WORKDIR
    
        for I in ${!REPOSITORIES[@]}; do
            REPOSITORY=${REPOSITORIES[${I}]}
    
            echo "Checking $REPOSITORY (https://github.com/$USER/$REPOSITORY)"
    
            if [ ! -d "$REPOSITORY" ]; then
                git clone "git@github.com:$USER/$REPOSITORY.git"
            else
                (cd $REPOSITORY && git pull origin)
            fi
    
            if [ -f "$REPOSITORY/pom.xml" ]; then
                checkMaven $REPOSITORY
            elif [ -f "$REPOSITORY/build.gradle" -o -f "$REPOSITORY/settings.gradle" ]; then
                checkGradle $REPOSITORY
            else
                echo "Not checkeable project $REPOSITORY (not detected as Maven or Gradle project)"
            fi
        done
    }
    
    function checkMaven() {
        REPOSITORY=$1
        (cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && mvn --settings ../../settings.xml org.owasp:dependency-check-maven:6.5.2.1:check); EXIT_CODE=$?
        #(cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && mvn --settings ../../settings.xml org.owasp:dependency-check-maven:6.5.2.1:check) | grep -E "$REGEXP" | sort | uniq; EXIT_CODE=${PIPESTATUS[0]}
        if [ "$EXIT_CODE" != "0" ]; then
            echo "Check not completed with success ($EXIT_CODE)"
        fi
    }
    
    function checkGradle() {
        REPOSITORY=$1
        (cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && gradle --init-script ../../init.gradle dependencyCheckAnalyze); EXIT_CODE=$?
        #(cd $REPOSITORY && echo "Checking $REPOSITORY dependencies..." && gradle --init-script ../../init.gradle dependencyCheckAnalyze) | grep -E "$REGEXP" | sort | uniq; EXIT_CODE=${PIPESTATUS[0]}
        if [ "$EXIT_CODE" != "0" ]; then
            echo "Check not completed with success ($EXIT_CODE)"
        fi
    }
    
    main
    
    owasp-vulnerabilities-check.sh
    Terminal

    El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
    ./gradlew dependencyCheckAnalyze

    » Leer más, comentarios, etc...

    Coding Potions

    ¿Cómo se abre un fichero en Vim? Multiples formas

    enero 06, 2022 12:00

    Antes de empezar con los comandos tienes que saber que todo lo que aparece en este artículo se aplica de la misma manera en Vim y Neovim. Si no sabes la diferencia entre ambos te recomiendo que te pases por el artículo de Vim vs Neovim

    Lo primero que tienes que saber es que puedes abrir Vim con un fichero ya cargado, simplemente desde la terminal, escribes vim y el nombre del fichero que quieres abrir (siempre y cuando estés en la carpeta que tenga el archivo ese):

    vim index.html
    

    O para neovim:

    nvim index.html
    

    Puedes incluso pasar varios ficheros a abrir de una:

    vim ./src/views/index.html package.json
    

    Ver los ficheros abiertos en Vim

    Una cosa que recomiendo mucho para evitarse líos es usar un plugin que pinte la barra de los ficheros abiertos, en mi caso uso la de vim-buftablinepero puedes usar la que quieras.

    Abrir ficheros en Vim y Neovim

    Bien, una vez abierto Vim para abrir un fichero simplemente tienes que apretar la tecla de los dos puntos del teclado y escribir la letra e y el fichero que quieres abrir, por ejemplo:

    :e ./src/components/example.vue
    

    Al apretar enter deberías tener el fichero abierto, y si has instalado la extensión de la barra, arriba podrás ver el nombre del fichero que tienes abierto. Si ejecutas otra vez ese comando pero pasando otro fichero pues se te debería abrir y añadir a la barra de ficheros abiertos.

    Por cierto, si pones el nombre de un fichero que no existe y añades texto y le das a guardar lo que has hecho es crear el fichero en la carpeta en la que estuvieras.

    Listar y abrir los ficheros de una carpeta

    Una cosa que no conce mucha gente respecto al comando e es que puedes pasar un punto o una carpeta, ejemplos:

    :e .
    :e ./src/
    

    Al pulsar enter lo que se te va a abrir es la lista de ficheros de la carpeta que has pasado (o de la carpeta actual si has puesto el punto). En esta lista te puedes mover usando las clásicas letras de h j k l (si no sabes para qué sirven estas teclas pásate por el artículo de Movimientos básicos en Vim).

    Cuando pulses enter en esta lista, si es sobre un fichero se te abre directamente y si es sobre una carpeta te aparece todos las carpetas y ficheros internos de la misma (puedes volver hacia atrás pulsando enter sobre la línea que tiene ../ o pulsando el la tecla del guíon -).

    Cambiar entre ficheros abiertos

    Hay un par de comandos en Vim que te permiten cambiar entre los ficheros que tienes abiertos, concretamente son los siguientes:

    • :bprev Para cambiar al buffer anterior, es decir, al último fichero que has modificado
    • :bnext Para cambiar al buffer siguiente, es decir, al siguiente fichero que has cambiado, en caso de que no haya pues abre el último que has cambiado.

    Para que te hagas una idea es como moverte entre pestañas abiertas. Puedes ir atrás una pestaña, o a la pestaña siguiente, y en caso de que no haya más pues el ciclo vuelve a empezar.

    Si te has descargado la extensión que te he recomendado antes esto se ve mucho más claro porque ves las pestañas que tienes abiertas hacia la izquierda de la actual (las anteriores) y a la derecha las siguientes.

    Como da mucho palo andar escribiendo estos dos comandos todo el rato, la mayoría de la gente lo que hace es asignarlos a una combinación de teclas.

    En mi caso particular, tengo las siguiente config en el fichero ~/.vimrc:

    map <C-d> :bnext<CR>
    map <C-a> :bprev<CR>
    
    imap <C-D> <Esc>:bnext<CR>a
    imap <C-A> <Esc>:bprev<CR>a
    

    Simplemente mapeo los dos comandos a las teclas de Control + a y Control + d.

    Te recomiendo que eches un ojo al artículo de Configuración básica de Vim por si no sabes configurar Vim y quieres echar un ojo a cosas que recomiendo poner.

    Árbol de carpetas completo con creación de ficheros

    Como abrir carpetas y ficheros con el comando e a veces se queda corto, es bastante habitual en gente que trabaja con Vim el descargar un plugin que añada un árbol de carpetas.

    El que uso yo es el de nerdtree, que es muy famoso. Una vez lo tengas instalado, puedes ejecutar el comando :NERDTree para abrir el árbol de carpetas del directorio actual

    Como obviamente da toda la pereza ejecutar este comando todo el rato puedes añadir un mapeo de teclas, el que tengo puesto yo en el .vimrc es el siguiente:

    nmap <Leader>nt :NERDTreeToggle<cr>
    

    En mi caso la tecla Leader la tengo mapeada a la tecla coma, por lo que simplemente tengo que escribir en modo normal ,nt para abrir el árbol.

    Como NERDTree se pueden hacer muchas más cosas, lo tengo escrito en el árticulo de Uso de NERDTree en Vim para tener árbol de carpetas y ficheros

    Control + P en Vim para búsqueda de ficheros

    La combinación de teclas Control + P se ha vuelto un poco universal en todos los editores e IDEs para programar. Es un comando que abre una ventana en la que poder buscar nombres de ficheros para abrirlos, en lugar de andar navegando por el árbol, es lo que se llama un fuzzy finder.

    En Vim existen varios fuzzy finders, antes usaba el de fzf pero hace unos meses di el salto y cambié a telescope, mantenido por uno de los creadores de Neovim.

    Lo único malo de telescope es que solo funciona si usas neovim. Si estás usando vim asecas vas a tener que usar el antes mencionado fzf.

    Lo que me gusta de telescope respecto a fzf es que permite ver en una ventana la previsualización del fichero antes de abrirlo (esto es una cosa que incluso no tiene el vscode).

    Lo que he hecho es, en mi .vimrc, añadir un mapeo para abrirlo con la combinación de teclas de Control + P:

    nmap <C-P> :Telescope git_files hidden=true <CR>
    

    En este caso está puesto con el modo git_files para que solo me liste los ficheros que no estén en el gitignore (ignora node_modules, la carpeta .git y ese tipo de historias).

    Telescope es tan guay que incluso permite hacer búsquedas de palabras en todos los ficheros del proyecto, te lo cuento todo en su artículo de Uso del plugin de Telescope en Neovim

    Conclusiones

    Fijo que exiten más comandos para trabajar con ficheros en Vim, pero ahora mismo yo es lo que uso. De todas formas este artículo lo iré completando según vaya aprendiendo mejores formas de trabajar con ficheros.

    » Leer más, comentarios, etc...

    Blog Bitix

    Hemeroteca #20

    diciembre 30, 2021 09:00

    El número de visitas aunque crece muy ligeramente ya no lo hace al ritmo de los primeros años y los ingresos se mantienen en cifras similares a los de los años anteriores, aunque suficientes para alguna necesidad, juego de consola o donación a algún proyecto de software libre que uso. Este 2021 han sido 73 artículos nuevos escribiendo al menos un artículo a la semana, mi intención es que siga siendo parecido durante el 2022.

    Blog Bitix

    Los blogs quizá han perdido relevancia con la aparición YouTube, Twitch y las redes sociales como nueva forma de compartir contenido, los vídeos tiene sus ventajas pero también algunas desventajas, una de sus ventajas es que es un contenido más fácil de consumir, leer requiere algo más de esfuerzo y comprender el contenido escrito requiere más atención. La desventaja es que en muchos vídeos los youtubers para decir una única idea la repiten varias veces de diferentes formas resultando en un vídeo de un cuarto de hora cuando en un blog la misma idea se lee en un par de minutos.

    A día de hoy no me veo como youtuber porque para escribir un tweet algunos necesito editarlos 3 veces para que no quede ninguna falta de ortografía, y el texto de los artículos también los edito por lo mismo o por retocarlos, con un vídeo no podría hacer eso y hay algunos youtubers que hablan sin cortes durante 30 minutos y el trabajo de edición posterior que les hacen a los vídeos es significativo. También por privacidad ya que un canal de youtuber requiere dar imagen, hay algunos canales que solo ponen la voz pero son la minoría.

    No se que tiene YouTube para generar a las personas el escribir comentarios pero se escriben muchos más que en cualquier artículo de cualquier blog. Alguna vez he comentado que recibo pocos comentarios pero la realidad es que también escribo pocos, tampoco es algo que sea a lo que más importancia le dé para seguir motivado al escribir artículos.

    Aquí hay una recopilación bastante exhaustiva de todos los blogs de habla hispana sobre software libre y GNU/Linux junto a otros medios que utilizan los creadores de contenido tanto en de España como de América. El mío no está a pesar de ser uno de ya con 10 años lleve más tiempo que algunos de esos. No interacciono mucho con la comunidad y como digo en mi blog hablo de GNU/Linux pero es un blog solo de GNU/Linux, hablo de Java pero no es un blog solo de Java y desde que tengo una PlayStation 4 estoy hablando de los juegos que voy jugando, no tengo un tema único con lo que no puedo ponerle una solo etiqueta.

    Contenido del artículo

    Retrospectiva 2021

    Durante este 2021 he ido escribiendo cada semana el artículo que he publicado cuando años anteriores el tema del artículo que escribía lo tenía apuntado en una lista de ideas para que no se me olvidase y escrito desde hace varias semanas. Este es uno de los motivos por los que durante este año he estado escribiendo la mayoría de las semanas un solo artículo cuando en el 2020 muchas de las semanas escribí dos a la semana.

    En el 2022 espero seguir escribiendo un artículo a la semana mientras tenga ideas y no eche de menos el tiempo para hacer otras cosas.

    Evolución visitas

    Hay canales de YouTube que en un par de meses tienen 200 suscriptores, en 6 meses 1000 suscriptores y con únicamente 7 vídeos publicados de 1 hora con de 1K a 10K visualizaciones por vídeo. En mi blog no creo que tenga tantas personas suscritas al feed y en twitter solo tengo 400 seguidores. El truco para conseguir crecer en un blog o cualquier sitio de contenido no depende únicamente de crear buen contenido, con eso solo crecer requiere mucho tiempo, sino conseguir además que el contenido sea compartido por cuentas con muchos seguidores que permite llegar a mucha más gente. La importancia que tienen los suscriptores, al menos la que le doy yo es más por ya que una vez que uno dedica tiempo a escribir contenido este sea leído, ver que es leído por muchas personas y que crece o llega a una cifra respetable es una forma de motivación para seguir creando contenido.

    Hace unos meses puse en un tweet a un youtuber sobre GNU/Linux que estaba iniciando su canal, aún tenía pocos meses, se me ocurrió relacionarlo con otro creador de contenido mucho más conocido y con una trayectoria de más de una década compartiendo ya que justo hace unos días anunció que iba dejar de hacerlo tan habitualmente y cambiaría de temas, se me ocurrió relacionarlos al que lleva mucho tiempo y lo deja y el que empieza y ayudar al nuevo ya que esperaba que fuera compartido, era un contraste que es la ley de vida para los creadores de contenido unos lo dejan otros empiezan, con todos los seguidores que tenía el youtuber conocido al retuitear el mensaje fue difundido a gran cantidad de personas interesadas en el tema, en Twitter consiguió unas decenas de seguidores y en YouTube casi una centena de suscriptores partiendo de unos 200. El mayor mérito lo de la difusión la tuvo el creador de contenido conocido por su gran cantidad de seguidores.

    Me sorprende que hay varios youtubers de los que tengo constancia que en realidad no son informáticos pero lo usan a nivel doméstico además comparten contenido en sus canales con éxito seguidos por muchas personas, dos buenos ejemplos son yoyo en su canal de Salmorejo Geek con 16K suscriptores que se dedica a la agricultura y otro Karla en su canal del Karla’s Project con 57K suscriptores.

    Aunque levemente ascendente las páginas vistas se mantienen entre 45K y 65K dependiendo del mes, estas ya no crecen en la misma progresión que en en los primeros tres años del blog.

    Evolución visitas 2021

    Evolución visitas 2021

    Evolución ingresos

    Al poco tiempo de crear el blog le puse publicidad AdSense, al principio por curiosidad de saber cuántos ingresos pueden obtenerse, posteriormente ya que creaba contenido una forma de monetizarlo que aunque no compensa el tiempo dedicado es una pequeña forma de motivación y suficiente para costear comprar de un teclado mecánico, quizá renovar el lector de libros electrónicos Kindle y algún juego de consola para el que no tengo tanto tiempo para jugar como podría si no se lo dedicase al blog.

    No he tenido muchas ofertas de artículos patrocinados, sí contactos pero que se materialicen no tantos, varios preguntan pero en cuanto se les indica la cantidad a pagar ya no responden más ni siquiera para decir que no les interesa. Algunos han sido incluso intentos de phishing. No he recibido tantos artículos patrocinados que son los que me recargan mi saldo en PayPal, pero el final de año ha acabado con uno para el proveedor de infraestructura cloud de Clouding. Ya había escrito uno para Clouding y en esta ocasión han querido publicar otro, un artículo enteramente escrito por mi y con total libertad editorial en tema y contenido.

    Unos 400 € por ingresos de AdSense, 50 € por afiliado de Amazon y 150 € por patrocinios. No está mal pero que nadie piense que compensa el tiempo dedicado al blog.

    Evolución ingresos AdSense y Amazon Afiliados 2021 Evolución ingresos AdSense y Amazon Afiliados 2021

    Evolución ingresos AdSense y Amazon Afiliados 2021

    Sobre Blog Bitix

    Desde que divido los artículos en secciones me facilita escribirlos con mejor estructura además de facilitar la lectura, quedan artículos más largos pero también me lleva más tiempo escribirlos. Este 2021 casi todas las semanas he ido escribiendo sobre temas planificados la semana anterior o durante esa misma semana. Estos son los motivos por los que durante este año he escrito muchas semanas un solo artículo.

    Al nivel que le dedico al blog es casi un segundo trabajo, un artículo puede parecer poco pero es que casi con la rutina que tengo ahora es casi dedicarlo de lunes a jueves a escribirlo, hacer el ejemplo si lo tiene y publicarlo. Lunes hacer ejemplo, martes hacer tabla de contenidos y escribir primeros apartados, miércoles terminar el resto de apartados y jueves publicarlo. Así que llega el viernes y casi no me apetece ni jugar a la play solo escuchar música medio dormido. La semana pasa rápido y una nueva semana con la misma rutina empieza. Un artículo puede parecer poco para el que ve solo el artículo publicado un día y lo lee en quince minutos pero detrás en lo que no se ve el tiempo de dedicación a ese artículo ha sido varios días anteriores.

    Cuando un blog tiene muchos artículos parte del trabajo también se emplea en retocar y actualizar los antiguos como sustituir bpytop por btop, al menos si no se quiere que los artículos queden desfasados y sigan teniendo cierta vigencia. También tengo una lista para los artículos que me gustaría actualizar, pero también por falta de tiempo no puedo hacerlo todo, al menos si quiero hacer otras cosas que no sea únicamente el blog.

    Otro apartado que requiere tiempo es mejorar la propia infraestructura del blog. Un cambio que he hecho es utilizar GitHub Actions para publicar el contenido del blog, me evita tener que crearlo en local con lo que ahorro casi 2 GiB de espacio y bytes escritos en el SSD local. Otro cambio ha sido la gestión de los enlaces de Amazon, con el paso del tiempo muchos se van rompiendo, el cambio me permite saber que enlaces tengo en los artículos dándoles una etiqueta, por ejemplo, enlaces de afiliado de teclado mecánico y poder insertar los mismos enlaces en varios artículos de forma que no tenga que ir a cada artículo cambiándolos, sigue siendo un trabajo manual pero menos que antes. Aún no he tenido necesidad de migrar el blog de Github Pages a otro hospedaje, hasta que no me echen posiblemente no me vaya y entonces tendré que pensar que hosting elijo, una opción es Clouding.

    Continuando mejorando alis y futuras ideas

    Aunque Arch Linux en el medio de instalación incluye un script para facilitar la instalación aún así hay una buena cantidad de usuarios que siguen usando alis que deduzco porque sigue siendo marcado como favorito y con forks. Y es que de los scripts de instalación de Arch Linux que he visto sigo prefiriendo los conceptos en los que se basa alis, automatizado, desatendido, simple y configurable. Otros scripts no cumplen alguno o varios de estos principios que tengo para alis, aunque alis sea simplemente un script en bash no he visto ninguna implementación mejor en todos los aspectos.

    Ya tengo varias pequeñas ideas para mejorar alis. Más que para el script para seguir promocionándolo. Una de estas mejoras es escribir más contenido para atraer visitas como mejorar su sitio web, una guía de usuario, una descripción más detallada, que opinan los usuarios, opciones alternativas y algo de documentación sobre la configuración. Completar algo más los principios en los que se basa el script y que espera el script de los usuarios que lo usen, alis principalmente no es un script para que un usuario sin ningún conocimiento sea capaz de instalar Arch Linux sino para facilitarlo a quien está dispuesto a aprender o ya conoce Arch Linux, el script espera que si no se ajusta a tus necesidades lo modifiques y envíes un pull request si esa funcionalidad es útil para otros usuarios o que el usuario pida ayuda como último recurso en caso de no resolver el problema por sí mismo con los recursos existentes de la excelente Wiki de Arch y sus foros. En Arch Linux no es recomendable usar una configuración que no sabes como funciona, al menos lo básico, el riesgo de hacerlo es que pierdas los datos en caso de que surja un error o en una actualización y no sepas como repararlo. Para usar GNU/Linux como primer contacto sin complicaciones son mejores distribuciones Ubuntu, Fedora, openSuse, Debian o elementaryOS.

    Ya le he añadido Google Analytics para ver cuantas visitas tiene el sitio web de alis, de momento son unas 30 visitas diarias, una vez le añada alguna página más de contenido y mejore algo en SEO supongo que recibirá alguna más. Con estos pocos datos veo si realmente tiene algún impacto los cambios que haga, esto es útil para tomar alguna decisión conociendo datos en vez de solo intuición.

    Otra idea que ya he implementado es utilizar el espacio de discusiones o foro que ofrece GitHub Discussions para tratar de crear algo de comunidad sobre el script que seguro me permite obtener información sobre cómo mejorarlo y como una forma de darlo a conocer. La primera pregunta que he hecho ha sido Why do you use alis?.

    Otra idea es tratar de monetizarlo. Con publicidad AdSense seguro que no da para mucho, mucho menos que el blog pero aunque sea poco al menos es una forma de mantenerme motivado para seguir mejorándolo y evitar abandonarlo, al escribir algo más de contenido posiblemente tenga alguna visita más. No se si volveré a incluir la opción de donación, y es que PayPal en las cuentas personales al hacer la donación muestra el nombre real del usuario cosa que por privacidad es un problema para mi, no se si volveré a añadir esta opción. Y no se si solicitar el programa de sponsors de Github.

    Pero bueno que tampoco sea muy famoso no está mal porque no quiero que al usarlo muchos usuarios suponga una gran cantidad de solicitudes de features, issues y bugs que crezcan a un ritmo que no pueda asumir o que me requiera mucho tiempo.

    He recibido varios pull request que aunque no he incorporado tal cual me han servido como base para hacer los cambios. Durante este año, he hecho el mismo cambio que en el blog para generar el sitio web usando GitHub Actions. He añadido soporte para PipeWire, mejorado el soporte para systemd-homed y la posibilidad de configurar la disposición de sobvolúmnes con el sistema de archivos btrfs.

    Otras contribuciones

    Sigo realizando las traducciones de VLC, KeePassXC y VirtualBox. La de VLC la estoy dejando algo aparcada, me gustaría que hubiese alguien que pudiese contribuir a traducir VLC sobre todo aquellas partes que yo no uso como la versión para iOS y Android.

    Artículos publicados

    Si en el 2020 publiqué unos 90 artículos, este he publicado algunos menos por tiempo y porque no he tenido tantas ideas para escribir artículos. Este 2021 han sido 73 artículos nuevos y algunos pocos más que he actualizado. Los temas son los mismos de siempre, estos artículos son los del segundo semestre.

    Urte berri on!, Buen año nuevo!

    Artículos sobre Java y programación

    Artículos sobre GNU/Linux y software libre

    Artículos sobre JavaScript y web

    Artículos sobre juegos

    Otros temas

    » Leer más, comentarios, etc...

    Coding Potions

    ¿Cómo salir de Vim? Múltiples comandos para salir

    diciembre 30, 2021 12:00

    Si alguna vez has abierto Vim, aunque sea por error, te habrás visto en la situación de no saber cómo salir de Vim. Lo típico es cerrar toda la terminal, pero claro no es lo óptimo, veamos otras alternativas.

    Para salir de Vim (o Neovim) tienes que seguir esta secuencia de pasos:

    • Pulsar la tecla Esc
    • Pulsar la tecla :
    • Pulsar la tecla q

    La explicación de esto es que al pulsar Escape el Vim se pone en modo comandos, pulsando los dos puntos puedes escribir comandos a ejecutar, y el comando q lo que hace es cerrar Vim.

    Si has abierto varios ficheros puede que tengas que repetir esta secuencia varias veces, una por cada fichero.

    Si Vim no se cierra es posible que ocurra porque has hecho cambios en un fichero y no lo has guardado. Tienes varias opciones, pulsar Esc y escribir el comando :q pero con exclamación cerrada, es decir: Esc :q! para salir sin guardar.

    Otra opción para salir es pulsar Esc y escribir el comando :w para guardar los ficheros y luego :q para salir.

    Otros comandos que puedes usar para salir de Vim son los siguientes (recuerda pulsar Esc para entrar en modo comando):

    • :wq - Guardar fichero actual y salir
    • :x - Guardar y salir
    • :qa - Salir y cerrar todos los ficheros

    » Leer más, comentarios, etc...

    Coding Potions

    Qué cosas se puede hacer en el modo normal de Vim

    diciembre 30, 2021 12:00

    Vim tiene dos modos de funcionamiento, modo Insertar con el que puedes escribir de forma normal y modo comandos con el que las teclas de tu teclado se transforman y dejan de escribir. Básicamente en el modo comandos cada tecla va a servir para una cosa.

    Al modo comandos en Vim se le conoce también como modo normal. Dependiendo de tu config de vim puede que abajo a la izquierda te salga si estás en modo normal o modo insertar.

    Lo primero es saber que para entrar en modo normal tienes que pulsar la tecla escape, y para entrar en modo insertar (para escribir texto) tienes que pulsar la tecla i (i de insertar).

    Por cierto, todos los comandos de Vim funcionan exactamente igual en Neovim.

    Para no hacer un artículo demasiado extenso voy a dividir la lista de comandosen varios artículos.

    Te recomiendo que empieces por el de Movimientos básicos en Vim, ya que es lo primero que recomiendo aprender y dominar.

    Luego puedes seguir con el artículo de Comandos de Vim para movimientos entre ficheros, con el que aprenderás a abrir ficheros, cambiar entre ficheros abiertos y demás.

    También son interesantes el artículo de Comandos de Vim para movimientos horizontales y el de Comandos de Vim para movimientos entre líneas del fichero para sacarle más partido a los comandos básicos y ser capaz de editar ficheros más rápido.

    Como cualquier editor de textos en Vim debes ser capaz de copiar y pegar contenido, echa un ojo al artículo de Comandos de Vim para copiar y pegar contenido .

    » Leer más, comentarios, etc...

    Variable not found

    Implicit dots en C# 11

    diciembre 27, 2021 11:01

    NET

    Hace ya mucho tiempo que C# inició un interesante camino para conseguir reducir la cantidad de código necesario para hacer cosas frecuentes, introduciendo la capacidad de hacer implícitas determinadas construcciones y, por tanto, ahorrándonos tiempo y pulsaciones de teclas innecesarias.

    En esta línea, todos recordaréis el tipado implícito, que tanto debate abrió en el lanzamiento de C# 3, hace ya casi quince años:

    // Antes de C# 3
    List<string> strings = new List<string>();

    // Usando tipado implícito
    var strings = new List<string>();

    Bastante tiempo después, ya con C# 10, el lenguaje nos regaló una nueva característica que iba en la misma dirección, las expresiones new con el tipo destino. Éstas permitían construir objetos usando tipado implícito, aunque esta vez en el lado derecho de las expresiones:

    // Antes de C# 10
    public class MyClass
    {
    private Invoice invoice = new Invoice(123);
    private Dictionary<string, Person> people = new Dictionary<string, Person>();
    ...
    }

    // Ahora
    public class MyClass
    {
    private Invoice invoice = new (123);
    private Dictionary<string, Person> people = new ();
    ...
    }

    También en C# 10 nos hemos encontrado con los using globales y using implícitos, características ambas que nos permiten economizar esfuerzos evitando la introducción repetitiva de directivas para la importación de namespaces en el código.

    Pues bien, aquí viene la siguiente vuelta de tuerca :) Unas semanas atrás, Immo Landwerth (Program manager de .NET en Microsoft) sorprendía a todos con esta afirmación sobre una de las características principales del próximo C# 11:

    Immo Landwerth announcing implicit dots

    En otras palabras, la mayoría de los puntos que usamos al escribir en C# serán implícitos, por lo que no será necesario introducirlos:

    // Antes de C# 11
    Console.WriteLine(person.FirstName);

    // En C# 11
    Console WriteLine(person FirstName);

    Como podéis ver, el resultado es espectacularmente legible. Tras ver código escrito de esta forma, sólo puedo pensar cómo hemos podido vivir hasta el momento salpicando nuestro código con puntos, en tantos y tantos lenguajes. Realmente, es la característica que faltaba a C# para convertirlo definitivamente en el lenguaje de referencia para el resto de la industria.

    Varios son los motivos que han llevado a plantear la introducción de implicit dots en el lenguaje:

    • Analizando datos de GitHub, observaron que entre un 3 y un 5% del tamaño de los archivos se debe a los puntos. Por tanto, eliminándolos de la ecuación se obtiene un interesante ahorro energético en servidores y almacenamiento. Este porcentaje, aunque parezca pequeño, supone un ahorro inmenso cuando lo llevamos a grandes escalas (miles de proyectos con miles de archivos).
    • El mismo porcentaje de tiempo y proceso consumen los parsers al compilar los proyectos, que se evitarían gracias a esta
    • Por su posición en la zona inferior del teclado y su continua utilización, se estima que el punto es responsable de hasta el 15% de lesiones neurálgicas y tendinopatías de los profesionales del software, generando pérdidas mundiales de billones de dólares al año. De hecho, según la Asociación Nacional de Afectados de Tendinitis de EEUU, el número de lesiones tendinosas relacionadas con el dedo corazón en el gremio de desarrolladores de software descendería en un 20%.
    • Sumando las décimas de segundo que se tarda en pulsarlo, se estima que en una jornada de trabajo media se ahorrarían unos 15 minutos. De nuevo, llevado a la escala apropiada, podemos ver que una empresa con 30 trabajadores pierde cada día 8 horas de producción (vaya, una persona). 
    • Y, por qué no decirlo, el código queda más bonito, sin ese efecto de salpicaduras o manchitas que aporta ese signo de puntuación.

    Esta corriente anti-puntos llegaría a entrar de tal forma en el ecosistema que incluso se está planteando cambiar el nombre de la plataforma, pasando de denominarse .NET a simplemente NET (de nuevo, el punto implícito), mucho más pronunciable, fácil de deletrear y escribir.

    Por último, es interesante comentar que esta característica de momento es opcional, aunque estará activa por defecto en los nuevos proyectos C#. Para deshabilitarla, será necesario introducir el siguiente código en el archivo .csproj:

    <PropertyGroup>
    <ImplicitDots>disable</ImplicitDots>
    </propertyGroup>

    Pero eso sí, en cualquiera de los casos, quedarían fuera de esta nueva característica los puntos decimales, que obviamente seguirán siendo obligatorios:

    double d1 = 1 2; // Error CS2166: Expected double value
    double d2 = 1.2; // Ok. Es obligatorio usar el punto decimal

    En el resto de escenarios, como los accesos a propiedades o métodos, definiciones de rangos, namespaces, etc. podrán ser omitidos.

    Personalmente, me encanta esta característica. No veo el momento de poder escribir un código como el siguiente y liberarnos para siempre del punto, ese molesto compañero de viaje:

    namespace MyApp Invoicing;
    ...
    if(invoice Customer Email EndsWith ("@gmail.com"))
    {
    Console WriteLine ($"Customer {invoice Customer Name} " +
    "uses a Gmail mailbox: {invoice Customer Email} !");
    }

    ¿Y quedará aquí la cosa?

    Pues probablemente no. Está previsto que este sea el primer paso para que en C# 12 podamos hacer también implícito el punto y coma de separación de instrucciones, como en el siguiente bloque de código:

    // Compilará en C# 12
    namespace MyApp Test
    var x = 1 var y = 2
    Invoice invoice = new() { Amount = 1000 } var z = 3
    Console WriteLine (x) Console WriteLine (y+z)
    Console WriteLine (invoice Amount)

    Aún queda mucho para C# 12 y al final esto podría cambiar, pero espero que la idea prospere: nuestra productividad, dedos y muñecas lo agradecerán :)

    Publicado implícitamente en Variable Not Found

    » Leer más, comentarios, etc...

    Variable not found

    Enlaces interesantes 468

    diciembre 27, 2021 09:04

    Enlaces interesantes

    Ante todo, espero que estés pasando unas muy felices fiestas y con buena salud, a pesar de las circunstancias complicadas que nos rodean. Espero que pronto podamos salir de todo esto y volver a la normalidad.

    Y mientras tanto, os paso algunos enlaces recopilados durante la semana pasada, que espero que os resulten interesantes. :-)

    Por si te lo perdiste...

    .NET Core / .NET

    ASP.NET Core / ASP.NET / Blazor

    Azure / Cloud

    Conceptos / Patrones / Buenas prácticas

      Machine learning / IA / Bots

      Web / HTML / CSS / Javascript

      Visual Studio / Complementos / Herramientas

      Xamarin / .NET MAUI

        Publicado en Variable not found.

        » Leer más, comentarios, etc...

        Header Files

        ¡Feliz Navidad!

        diciembre 25, 2021 07:21

        Aunque publicado con unos días de retraso, me gustaría desear a todos los lectores de HeaderFiles una muy feliz y santa Navidad. Que el Niño Dios nos haga, en la celebración de Su Nacimiento, un poco más humildes y aumente en nosotros el deseo de ser santos.

        ¡Feliz Navidad!

        » Leer más, comentarios, etc...

        Blog Bitix

        Aprovisionar un servidor en la infraestructura cloud de Clouding con Ubuntu y Nginx usando Ansible y protocolo seguro HTTPS con Let's Encrypt

        diciembre 23, 2021 08:00

        La computación cloud permite crear un servidor con unos pocos clics, en unos pocos minutos, con la posibilidad de ajustar a las necesidades su capacidad en unidades de cómputo, memoria y almacenamiento y facturado solo por los recursos consumidos por unidad de tiempo y con la flexibilidad de ampliar o reducir la capacidad del servidor cloud en cualquier momento. Si con la computación cloud ya es muy sencillo y rápido disponer de un servidor la mayor dificultad está en aprovisionarlo para que ofrezca el servicio que se desea, para facilitar el aprovisionamiento hay herramientas como Ansible diseñadas con este objetivo y tratar la infraestructura como código. En este artículo muestro como crear y aprovisionar un servidor cloud en la infraestructura cloud de Clouding para un servidor web con Ubuntu y Nginx aprovisionado con Ansible y configurado con el protocolo seguro HTTPS con certificados generados por Let’s Encrypt.

        Clouding.io

        Para ofrecer un servicio a través de internet es indispensable un servidor que por motivos de fiabilidad entre otros es recomendable contratar a un proveedor de infraestructura. La tendencia es usar servidores cloud con las ventajas de solicitud de creación de los servidores mediante autoservicio con unos pocos clics, de forma rápida en unos pocos minutos y la posibilidad de ampliarlo, o reducirlo, en potencia cómputo, memoria del sistema o almacenamiento, además de una facturación por uso.

        La parte de tener un servidor accesible en internet para proporcionar un servicio ya se consigue en unos pocos clics, no hace falta presupuestarlo, ni comprar hardware y pagar su precio de forma completa por adelantado, ni recibirlo y montarlo, ni alojarlo en infraestructura propia. La parte más difícil de un servidor es aprovisionarlo y configurarlo para el propósito que se desee y para facilitar esta tarea hay herramientas de software específicas.

        Contenido del artículo

        Infraestructura cloud de Clouding

        Uno de los proveedores de infraestructura cloud más reconocidos con sede en España es Clouding, con un servicio de grado empresarial que ofrece un servicio de IaaS con oficinas y centro de datos en Barcelona. Como proveedor de computación cloud ofrece las varias importantes ventajas de este modelo de alojar servicios entre los que están disponer de un servidor en unos pocos clics y en pocos minutos, gran flexibilidad en la selección para configuración del servidor en capacidad de cómputo, memoria y almacenamiento permitiendo ajustar el precio a las necesidad del servicio, flexibilidad para cambiar ya sea ampliar o reducir la configuración del servidor y facturación según uso por unidad de tiempo en horas.

        Una de las ventajas determinante y diferenciadora de Clouding sobre otros proveedores es que al tener sede en España ofrece soporte de asistencia en caso de necesidad por personas en España. No es habitual requerir de esta asistencia pero en caso de necesitarla por el impacto que tiene si el servicio es de gran importancia para una empresa o persona si de este depende su facturación, clientes o sus proveedores. La asistencia es una característica determinante para la toma de una decisión al elegir un proveedor para en caso de necesidad restaurar el servicio.

        Al crear una cuenta en Clouding ofrecen un cupón de 5 € para probar su servicio gratis, este saldo permite crear una instancia de servidor cloud con un tiempo de funcionamiento ininterrumpido de unos 45 días en la configuración más básica.

        Selección de capacidad de los servidores

        Clouding permite seleccionar los recursos de cómputo para los servidores según las necesidades partiendo de la opción más básica de 1/2 unidades de cómputo, 2 GiB por core y 5 GiB de almacenamiento con la opción de elegir Linux o Windows como sistema operativo y dentro de Linux con varias distribuciones y versiones como Ubuntu, Centos o Debian. Esta opción más básica que ya es apta para algunos servicios parte de los 3 € al mes, la opción más capaz llega a las 48 unidades de cómputo o cores, 192 GiB de memoria y casi 2 TiB de almacenamiento suficiente incluso para las necesidades empresariales más exigentes con un precio de 550 € al mes. Y entre la opción más básica y la más capaz la posibilidad de elegir individualmente cada uno de los tres parámetros principales de configuración como unidades de cómputo, memoria y almacenamiento gracias a la computación cloud y una ventaja de los servidores virtuales privados de la generación anterior que no tenían una configuración tan flexible.

        Selección de los recursos de cómputo y coste

        Selección de los recursos de cómputo y coste

        Infraestructura

        Clouding ofrece una infraestructura de alta disponibilidad, con servicios de copias de seguridad para preservar datos y restauración, toma de instantáneas como medida de seguridad y recuperación, archivado de servidores para ahorrar costes, redimensionado de servidores gracias a la computación cloud y configuración de red privada para mayor seguridad. En el panel de administración de los servidores también se ofrecen detalles para la monitorización y observabilidad con los que comprobar el buen estado de funcionamiento del servidor.

        Características de Clouding Características de Clouding Características de Clouding

        Características de Clouding Características de Clouding Características de Clouding

        Características de Clouding

        Para garantizar un buen servicio utilizan una infraestructura moderna y de alto rendimiento, redundante y tolerante a fallos compuesta por hardware, software, red, imágenes y centros de datos. Almacenamiento con discos SSD NVMe de alta velocidad, RAM con corrección de errores ECC, consola de emergencia y monitorización, red de alto rendimiento baja velocidad con protecciones frente ataques DDOS y con dirección IP pública, imágenes para servidores basados en Linux o Windows, paneles de control y aplicaciones preinstaladas, finalmente sus centros de datos ubicados en España son redundantes y con energia 100% renovable.

        Infraestructura de Clouding Infraestructura de Clouding Infraestructura de Clouding

        Infraestructura de Clouding Infraestructura de Clouding

        Infraestructura de Clouding

        Son muchas las empresas que confían en Clouding como proveedor de servicios para sus necesidades tecnológicas. Dado el tamaño de estas empresas es garantía de que el servicio de Clouding está a la altura para tenerlos como clientes.

        Clientes de Clouding

        Clientes de Clouding

        Crear un servidor cloud en Clouding

        Empezar a usar Clouding es sencillo y rápido, necesitando únicamente crear una cuenta en el servicio y añadir algo de saldo a través de las formas de pago que se ofrecen como pago con tarjeta, domiciliación bancaria o cuenta de PayPal. Dispone de notificaciones de saldo bajo y autorecarga para evitar supervisar el saldo y que un servicio no deje de funcionar por motivos de facturación. Informes con el detalle del coste usado en un periodo de tiempo y en el apartado Cuenta la posibilidad de configurar la muy útil y recomendable medida de seguridad del segundo factor de autenticación.

        El primer paso para crear un servidor es crear una llave SSH con la que posteriormente acceder al servidor por línea de comandos, la llave SSH se puede descargar para configurarla en el cliente SSH local. Una vez creada ya es posible crear el servidor donde entre otras se seleccionan las características del mismo como sistema operativo y versión, cantidad de unidades de cómputo, cantidad de memoria y cantidad de almacenamiento, teniendo en cuenta que los tres últimos se pueden ampliar o reducir con posterioridad.

        Selección de características para crear instancia de servidor cloud Selección de características para crear instancia de servidor cloud Selección de características para crear instancia de servidor cloud

        Selección de características para crear instancia de servidor cloud

        Selección de características para crear instancia de servidor cloud

        Después de confirmar las características e iniciar la creación del servidor se inicializa y está disponible al cabo de unos pocos segundos o pocos minutos proporcionado entre sus detalles la dirección IP pública que le ha sido asignada y necesaria para la conexión y la configuración DNS del nombre de dominio en el proveedor externo de registro de dominio o en el panel de DNS de Clouding si se configura como servidor administrativo autorizado para el dominio.

        Progreso de creación de servidor cloud

        Progreso de creación de la servidor cloud, estadísticas y claves SSH

        Estadisticas de estado para monitorización y observabilidad Claves SSH para los servidores

        Progreso de creación de la servidor cloud, estadísticas y claves SSH

        Conexión desde línea de comandos con SSH

        Para la conexión al servidor por línea de comandos se utiliza el protocolo seguro SSH, para ello en GNU/Linux como cliente se usa OpenSSH y en Windows una posibilidad es PuTTY. La clave privada es una clave privada RSA en formato pem que para usar con OpenSSH si se desea usar una propia primero hay que convertirla a formato ssh-rsa con el siguiente comando y aprovisionar la clave pública en forma ssh-rsa en el servidor. Para usar la llave hay que configurar el archivo .ssh/config con la dirección IP pública del servidor. Como Clouding ya se encarga de tanto generar la llave privada como de aprovisionarla en el servidor la conversión y aprovisionamiento no es imprescindible.

        1
        2
        
        $ chmod 0600 clouding.pem
        $ ssh-keygen -y -f clouding.pem > clouding.pub
        
        ssh-key-conversion.sh
        1
        2
        
        Host 27.0.174.19
            IdentityFile ~/.ssh/clouding.pem
        
        ssh-config
        1
        2
        
        $ ssh ubuntu@27.0.174.19
        
        
        ssh.sh

        Conexión SSH a instancia de servidor cloud

        Conexión SSH a instancia de servidor cloud

        Cómo aprovisionar un servidor cloud

        Una vez la conexión al servidor por línea de comandos funciona ya es posible configurar el servidor, no se diferencia en ningún aspecto a configurar un servidor por linea de comandos según el sistema operativo elegido. El aprovisionamiento del servidor y configuración consiste básicamente en la instalación de paquetes, configuración de servicios editando archivos de configuración y reinicio de servicios para que la configuración modificada tome efecto. Los paquetes a instalar dependen del propósito o propósitos del servicio para el servidor puede ser un servidor web con Nginx o Apache HTTPD, un servidor de base de datos con PostgreSQL o MySQL, un servidor de documentos personales con Nextcloud o servidor de archivos entre otras muchas otras funcionalidades.

        Aunque es posible configurar un servidor introduciendo los comandos uno a uno es tedioso además de una tarea repetitiva en caso de tener que aplicarlos en varios servidores, no queda automatizado ni los comandos están bajo un sistema de control de versiones en el que queden descritos los comandos de configuración. Hay herramientas de software con el objetivo de aprovisionar servidores que permiten realizar el aprovisionamiento de un servidor de forma automatizada con la ventaja de ser más rápido, reproducible y la posibilidad de utilizar un sistema de control de versiones para los scripts de aprovisionamiento con sus propias ventajas como historial de cambios y colaboración entre personas en su edición. Una herramienta de aprovisionamiento muy conocida es Ansible, en esencia esta herramienta ejecuta los comandos en el servidor, permite hacerlo de forma remota con únicamente una conexión SSH sin necesidad de instalar software de servidor y la posibilidad de aplicar los cambios a un grupo completo de servidores. La herramienta de aprovisionamiento y configuración Ansible entre dentro del grupo de herramientas para tratar a la infraestructura como código.

        En un primer momento para desarrollar los scripts de aprovisionamiento es posible crear una máquina virtual en local y tratarla como si de un servidor se tratase. Vagrant permite crear máquinas virtuales de forma automatizada con una de sus posibilidades crear máquinas virtuales en VirtualBox. Una vez que el script de aprovisionamiento funciona ya es posible lanzarlo contra el servidor cloud.

        Ejemplo de aprovisionamiento de un servidor cloud de Clouding con Ansible

        En este ejemplo muestro como aprovisionar un servidor cloud de Clouding de forma automatizada con la herramienta Ansible. El servidor cloud creado en el paso anterior tiene el sistema operativo Ubuntu en la versión LTS 20.04, la funcionalidad del servidor es la de un servidor web con Nginx instalado como paquete de software de Ubuntu por más sencillez para el ejemplo que instalarlo con Docker, para que el servidor utilice el protocolo HTTPS es necesario un certificado que con el servicio de Let’s Encrypt permite obtenerlo de forma rápida y automatizada.

        Let’s Encrypt ofrece la herramienta certbot y siguiendo sus instrucciones para Ubuntu permite obtener el certificado que posteriormente hay que usar en la configuración de Nginx al configurar el protocolo HTTPS y para que los navegadores clientes validen correctamente el dominio del servidor. Por motivos de seguridad usar el protocolo HTTPS es un requerimiento indispensable sin embargo es necesario un certificado que identifique el dominio usado en el servidor que en algunos proveedores tiene un coste significativo de unos 100 € o más y se tarda unos días en obtenerlo. Let’s Encrypt permite obtener un certificado para el dominio en unos pocos segundos de forma automatizada y completamente gratuito ni ningún coste.

        Además, de la configuración básica mostrada en este artículo es posible configurar otras opciones en Nginx para variar su comportamiento, de las que he escrito en otros artículos con la etiqueta web.

        Organización de roles en Ansible

        Ansible define unas convenciones para los nombres de archivos necesarios, estructura de directorios y formato para los archivos. El archivo hosts define el inventario de máquinas a las que Ansible puede conectarse y las credenciales de conexión además de poder definir variables asociadas a las máquinas y definir grupos de máquinas según un rol como servidor web o servidor de base de datos si hay varias instancias con ese rol, tiene un formato INI.

        Otra de las convenciones de Ansible y una forma de organizar las tareas de configuración son lo que se denomina como roles que contienen además de las tareas a aplicar a las máquinas que utilicen ese rol de Ansible, los archivos de configuración, plantillas y disparadores o handlers a ejecutar como reacción a la ejecución de alguna tarea. Finalmente, están los playbooks que son las recetas a ejecutar que indican que roles se aplican a una instancia o grupo de instancias del inventario. Dentro de un rol los archivos se organizan en varias carpetas.

        • tasks: contiene la definición de las tareas que se realizan al aplicar el rol. Permiten asociales etiquetas para filtrar su ejecución, añadirles condiciones de ejecución realizar la acción sobre una lista de elementos. Ansible posee una amplia colección de tareas para cualquier acción desde ejecución de comandos como instalación y actualización de paquetes hasta funcionalidades para trabajar con Docker. Las tareas se definen en archivos en formato YAML con la ventaja a diferencia de scripts de Bash ser independientes de la distribución.
        • handlers: contiene la definición de tareas que se ejecutan como consecuencia de la ejecución de otras tareas en puntos concretos de las tareas normales del rol.
        • files: son archivos estáticos que permiten aprovisionarlos en las instancias de las máquinas, pueden ser cualquier tipo de archivo como archivos de configuración de un servicio o recursos estáticos para el caso de un servidor web.
        • templates: son archivos que al procesarse junto variables en tiempo de ejecución producen como resultado un archivo estático. Aunque el resultado final es un archivo estático este se genera dinámicamente.

        Para empezar a aprender está la documentación oficial con la guía de usuario de Ansible junto con los conceptos de introducción a los playbooks, el inventario, tareas, roles y los módulos además de otros capítulos de su amplia tabla de contenidos.

        Tengo que decir que no soy un experto en Ansible y es muy posible que parte de la configuración de Ansible mostrada en este artículo sea posible mejorarla para hacerla más reutilizable los roles y sus tareas o estructurarlos algo mejor. Esta es la estructura completa de archivos y directorios del ejemplo.

         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
        
        $ tree .
        .
        ├── ansible-clouding-content-update.sh
        ├── ansible-clouding-ping.sh
        ├── ansible-clouding-system-init.sh
        ├── ansible-clouding-system-install.sh
        ├── ansible-clouding-system-update.sh
        ├── ansible-env.conf
        ├── ansible-env-default.conf
        ├── ansible-mkdpasswd.sh
        ├── ansible-raspberrypi-ping.sh
        ├── ansible-raspberrypi-system-init.sh
        ├── ansible-raspberrypi-system-install.sh
        ├── ansible-raspberrypi-system-update.sh
        ├── ansible-raspberrypi-update-content.sh
        ├── ansible-vagrant-content-update.sh
        ├── ansible-vagrant-ping.sh
        ├── ansible-vagrant-system-init.sh
        ├── ansible-vagrant-system-install.sh
        ├── ansible-vagrant-system-update.sh
        ├── hosts
        ├── roles
        │   ├── picodotdev.certbot
        │   │   └── tasks
        │   │       ├── configure.yml
        │   │       └── main.yml
        │   ├── picodotdev.goaccess
        │   │   └── tasks
        │   │       └── main.yml
        │   ├── picodotdev.nginx
        │   │   ├── handlers
        │   │   │   └── main.yml
        │   │   ├── tasks
        │   │   │   ├── main.yml
        │   │   │   └── restart.yml
        │   │   └── templates
        │   │       └── https-domain-template.conf
        │   ├── picodotdev.site
        │   │   ├── files
        │   │   │   └── index.html
        │   │   └── tasks
        │   │       ├── main.yml
        │   │       └── nginx-content.yml
        │   ├── picodotdev.system
        │   │   └── tasks
        │   │       ├── install.yml
        │   │       ├── main.yml
        │   │       ├── ssh.yml
        │   │       ├── update.yml
        │   │       └── users.yml
        │   └── picodotdev.ufw
        │       └── tasks
        │           └── main.yml
        ├── site-content-update.yml
        ├── site-system-init.yml
        ├── site-system-install.yml
        └── site-system-update.yml
        
        16 directories, 39 files
        ansible-role-structure.txt

        El contenido del archivo host para el servidor cloud de Clouding es el siguiente, que incluye la dirección IP pública proporcionada al crearlo y visible en los detalles del servidor y la llave privada que Ansible utiliza en su conexión por SSH al servidor y a través de las cuales ejecuta los comandos de forma remota. En el ejemplo se incluyen los datos para la conexión para pruebas en un entorno con Vagrant en una máquina virtual local y una Raspberry Pi, además de la instancia en la infraestructura de Clouding.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        
        [vagrant]
        192.168.56.10
        
        [raspberrypi]
        192.168.1.101
        
        [clouding]
        27.0.174.19
        
        [vagrant:vars]
        ansible_user=ubuntu
        ansible_ssh_private_key_file=~/.ssh/pico.dev@gmail.com
        
        [raspberrypi:vars]
        ansible_user=pi
        ansible_ssh_private_key_file=~/.ssh/pico.dev@gmail.com
        
        [clouding:vars]
        ansible_user=ubuntu
        ansible_ssh_private_key_file=~/.ssh/clouding.pem
        hosts

        Los roles de Ansible permiten agrupar las tareas por funcionalidad a aplicar a un servidor, unos roles quizá sean comunes para cualquier servidor como como el rol de picodotdev.system que realiza unas configuraciones básica en el sistema como instalar paquetes comunes, crear usuarios y configuración SSH y actualizan los paquetes, Otros roles son los siguientes.

        • picodotdev.system: configuración común del servidor independiente de su servicio.
        • picodotdev.ufw: tareas para configurar el cortafuegos UFW para limitar puertos abiertos.
        • picodotdev.nginx: tareas para configurar una máquina con la función de servidor web Nginx.
        • picodotdev.site: tareas para configurar un sitio web en el servidor de Nginx.
        • picodotdev.certbot: tareas para utilizar certbot y generar los certificados de Let’s Encrypt para configurar en Nginx.
        • picodotdev.goaccess: GoAccess es una herramienta para analizar los logs de Nginx.

        El primer paso es comprobar que Ansible puede establecer correctamente la conexión con el servidor para ello hay un módulo que hace la función. Si el comando funciona Ansible es capaz de lanzar el resto de tareas administrativas definidas en los roles a esa instancia del inventario.

        1
        2
        
        #!/usr/bin/env bash
        ansible -i hosts -m ping clouding
        
        ansible/ansible-clouding-ping.sh
        1
        2
        3
        4
        5
        6
        7
        
        27.0.174.19 | SUCCESS => {
            "ansible_facts": {
                "discovered_interpreter_python": "/usr/bin/python3"
            },
            "changed": false,
            "ping": "pong"
        }
        ansible-ping.out

        Estos son los archivos del rol picodotdev.system y picodotdev.ufw de Ansible.

         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
        
        ---
        - name: Create users
          include_tasks:
            file: users.yml
            apply:
              tags:
                - system-init
                - system-install
          tags:
            - system-init
            - system-install
        - name: Configure SSH
          include_tasks:
            file: ssh.yml
            apply:
              tags:
                - system-init
                - system-install
          tags:
            - system-init
            - system-install
        - name: Update system
          include_tasks:
            file: update.yml
            apply:
              tags:
                - system-install
                - system-update
          tags:
            - system-install
            - system-update
        - name: Install system
          include_tasks:
            file: install.yml
            apply:
              tags:
                - system-install
          tags:
            - system-install
        
        ansible/picodotdev.system/tasks/main.yml
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        
        ---
        - name: Install packages
          package:
            name: "{{ item }}"
            state: present
          become: true
          with_items:
            - python3
          tags:
            - system-install
        ansible/picodotdev.system/tasks/install.yml
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        
        ---
        - name: Set authorized keys
          authorized_key:
            user: ubuntu
            state: present
            key: "{{ lookup('file', item) }}"
          become: true
          with_items:
            - "{{ lookup('env', 'SSH_KEY_PUB') }}"
          tags:
            - system-install
        - name: Disable SSH password authentication
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: '^#PasswordAuthentication yes'
            line: 'PasswordAuthentication no'
          become: true
          tags:
            - system-install
        
        ansible/picodotdev.system/tasks/ssh.yml
        1
        2
        3
        4
        5
        6
        7
        8
        9
        
        ---
        - name: Update system
          apt:
            upgrade: full
            update_cache: yes
          become: true
          tags:
            - system-install
            - system-update
        
        ansible/picodotdev.system/tasks/update.yml
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        
        ---
        - name: Create users
          user:
            name: "{{ item }}"
            groups: "sudo"
            state: present
          become: true
          with_items:
            - ubuntu
          tags: system-install
        ansible/picodotdev.system/tasks/users.yml
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        
        ---
        - name: Install packages
          package:
            name: "{{ item }}"
            state: present
          become: true
          with_items:
            - ufw
          tags: system-install
        - name: Configure ufw
          ufw:
            rule: allow
            port: "{{ item }}"
            proto: tcp
          become: true
          with_items: "{{ ports }}"
          tags: system-install
        - name: Enable ufw
          ufw:
            state: enabled
            default: deny
          become: true
          tags: system-install
        ansible/picodotdev.ufw/tasks/main.yml

        Comandos de ejecución

        Para configurar el servidor en el ejemplo la aplicación de los roles están divididas en cuatro playbooks con sus cuatro archivos bash para su ejecución.

        • system-init: aplica las tareas del rol picodotdev.system.
        • system-install: aplica las tareas del rol picodotdev.site.
        • system-update: aplica las tareas del rol picodotdev.site etiquetadas con system-update que permite filtrar las tareas del rol a ejecutar omitiendo el resto que no tienen esa etiqueta.
        • content-update: aplica las tareas del rol picodotdev.site etiquetadas con content-update.

        Estos son la definición de los playbooks y los comandos de ejecución en los que se especifican las etiquetas de las tareas a ejecutar del playbook y roles.

        1
        2
        3
        
        #!/usr/bin/env bash
        source ansible-env.conf
        ansible-playbook -i hosts -l clouding --tags system-init --extra-vars "ansible_user=root ansible_ssh_private_key_file=~/.ssh/clouding.pem" site-system-init.yml
        
        ansible/ansible-clouding-system-init.sh
        1
        2
        3
        
        #!/usr/bin/env bash
        source ansible-env.conf
        ansible-playbook -i hosts -l clouding --tags system-install site-system-install.yml
        
        ansible/ansible-clouding-system-install.sh
        1
        2
        3
        
        #!/usr/bin/env bash
        source ansible-env.conf
        ansible-playbook -i hosts -l clouding --tags system-update site-system-update.yml
        
        ansible/ansible-clouding-system-update.sh
        1
        2
        3
        
        #!/usr/bin/env bash
        source ansible-env.conf
        ansible-playbook -i hosts -l clouding --tags content-update site-content-update.yml
        
        ansible/ansible-clouding-content-update.sh
        1
        2
        3
        4
        
        ---
        - hosts: all
          roles:
            - picodotdev.system
        ansible/site-system-init.yml
        1
        2
        3
        4
        
        ---
        - hosts: all
          roles:
            - picodotdev.site
        ansible/site-system-install.yml
        1
        2
        3
        4
        
        ---
        - hosts: all
          roles:
            - picodotdev.site
        ansible/site-system-update.yml
        1
        2
        3
        4
        
        ---
        - hosts: all
          roles:
            - picodotdev.site
        
        ansible/site-content-update.yml

        Además en el archivo ansible-env.conf se definen algunos datos como variables de entorno que probablemente podrían definirse también como alternativa en el archivo hosts como variables del inventario.

        1
        2
        3
        4
        5
        
        export UBUNTU_PASSWORD="$6$G7g9dNWz$H4FDdICBYP9oo0ILOAQEQnTbqXXuUFd0iuEbC4PQr3TenRHUaiDqcbu01fPQPk4tPuIbG1LFIk0JPhpey29Xo/"
        export SSH_KEY_PUB="~/Documentos/clouding.pub"
        export DOMAIN_DEFAULT="default"
        export DOMAIN_SITE="www.27.0.174.19.sslip.io"
        
        
        ansible/ansible-env.conf

        Certificado de seguridad con Let’s Encrypt en servidor web Nginx

        Para generar el certificado para el servidor web con Let’s Encrypt está la herramienta certbot disponible para Nginx y usando Ubuntu a través del paquete en formato de aplicación snap. El rol picodotdev.certbot contiene las tareas para realizar la configuración de Nginx una vez este está al menos configurado e iniciado para funcionar con el protocolo HTTP.

        Para validar un dominio y comprobar que el propietario del sitio web es el dueño del dominio Let’s Encrypt con la ayuda de certbot genera unos archivos en la raíz de documentos del sitio en la ubicación /.well-known/acme-challenge/, el proceso de validación de Let’s Encrypt accede al sitio web usando el dominio con el protocolo HTTP y si encuentra los archivos que espera valida el dominio y genera los certificados que certbot instala en el directorio /etc/letsencrypt/live/{{ domain }}/fullchain.pem.

         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
        
        ---
        - name: Install packages
          package:
            name: "{{ item }}"
            state: present
          become: true
          with_items:
            - snapd
          tags:
            - system-install
        - name: Install snap base packages
          community.general.snap:
            name:
              - core
              - hello-world
            state: present
          become: true
          tags:
            - system-install
        - name: Install snap packages
          community.general.snap:
            name:
              - certbot
            state: present
            classic: yes
          become: true
          tags:
            - system-install
        ansible/picodotdev.certbot/tasks/main.yml
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        
        ---
        - name: Configure certbot
          shell: |
            ln -sf /snap/bin/certbot /usr/bin/certbot
            certbot certonly --webroot --non-interactive -m "pico.dev@gmail.com" --agree-tos -w /var/www/{{ domain }} -d {{ domain }}
            certbot renew --dry-run    
          become: true
          tags:
            - system-install
            - system-cerbot
        
        ansible/picodotdev.certbot/tasks/configure.yml

        Estos son unos ejemplos de certificados generados por Let’s Encrypt y certbot. Los certificados tienen un tiempo de validez de unos pocos meses con lo que cada cierto tiempo hay que renovarlos de lo que se encarga certbot de forma automática.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        
        ubuntu@nginx:~$ sudo ls -lh /etc/letsencrypt/live/www.27.0.174.19.sslip.io/
        total 4.0K
        lrwxrwxrwx 1 root root  48 Dec 21 13:50 cert.pem -> ../../archive/www.27.0.174.19.sslip.io/cert1.pem
        lrwxrwxrwx 1 root root  49 Dec 21 13:50 chain.pem -> ../../archive/www.27.0.174.19.sslip.io/chain1.pem
        lrwxrwxrwx 1 root root  53 Dec 21 13:50 fullchain.pem -> ../../archive/www.27.0.174.19.sslip.io/fullchain1.pem
        lrwxrwxrwx 1 root root  51 Dec 21 13:50 privkey.pem -> ../../archive/www.27.0.174.19.sslip.io/privkey1.pem
        -rw-r--r-- 1 root root 692 Dec 21 13:50 README
        ubuntu@nginx:~$ sudo ls -lh /etc/letsencrypt/archive/www.27.0.174.19.sslip.io/
        total 20K
        -rw-r--r-- 1 root root 1.9K Dec 21 13:50 cert1.pem
        -rw-r--r-- 1 root root 3.7K Dec 21 13:50 chain1.pem
        -rw-r--r-- 1 root root 5.5K Dec 21 13:50 fullchain1.pem
        -rw------- 1 root root 1.7K Dec 21 13:50 privkey1.pem
        
        certbot-certificates.sh

        Configuración del servidor web Nginx

        Antes de generar los certificados de Let’s Encrypt es necesario configurar el servidor web Nginx e iniciar el servicio, la configuración incluye modificar los archivos de configuración de Nginx para crear en este caso un servidor web virtual y su contenido. Un servidor web virtual permite devolver un contenido u otro en función del nombre del dominio por el que se acceda al servidor. Las tareas también crean varios directorios y archivos como por ejemplo de directorio raíz que el servidor web utiliza para obtener los recursos estáticos que devuelve.

        Configurado Nginx con el protocolo HTTP y el servidor web virtual ya es posible iniciar la validación y generación de certificados de Let’s Encrypt y certbot. Las tareas de Ansible del rol picodotdev.nginx usan las tareas del rol picodotdev.certbot.

        Entre los archivos del rol de Nginx está un archivo que es la plantilla de configuración del servidor para Nginx que procesándolo junto ciertas variables permite generar el contenido final, el contenido generado se guarda en el directorio /etc/nginx/sites-available/{{ item }} y posteriormente se crea un enlace simbólico a este archivo en el directorio /etc/nginx/sites-enabled/{{ item }} del que Nginx lee la configuración de los servidores web virtuales.

         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
        
        ---
        - name: Install packages
          package:
            name: "{{ item }}"
            state: present
          become: true
          with_items:
            - nginx
          tags:
            - system-install
        - name: Create directories
          file:
            path: "{{ item }}"
            state: directory
          with_items:
            - /etc/nginx/conf.d
            - /etc/nginx/sites-available
            - /etc/nginx/sites-enabled
            - "/var/www/{{ domain }}"
          become: true
          tags:
            - system-install
        - name: "Copy nginx configuration (http) ({{ domain }})"
          template:
            src: "{{ template }}"
            dest: "/etc/nginx/sites-available/{{ item }}"
            owner: root
            group: root
            mode: '0644'
          vars:
            template: "nginx-virtual-server.conf"
            default_server: default_server
            domain: domain
            apex: apex
            ssl: false
          with_items:
            - "{{ domain }}"
          become: true
          tags:
            - system-install
        - name: "Enable nginx configuration (http) ({{ domain }})"
          file:
            src: "/etc/nginx/sites-available/{{ item }}"
            path: "/etc/nginx/sites-enabled/{{ item }}"
            state: link
            owner: root
            group: root
            mode: '0644'
          with_items:
            - "{{ domain }}"
          become: true
          tags:
            - system-install
        - name: Restart nginx
          include_tasks:
            file: restart.yml
            apply:
              tags: system-install
          tags:
            - system-install
        - name: "Configure certbot ({{ domain }})"
          include_tasks:
            file: roles/picodotdev.certbot/tasks/configure.yml
            apply:
              tags:
                - system-install
                - system-cerbot
          when: "'clouding' in group_names and ssl"
          tags:
            - system-install
            - system-cerbot
        - name: "Copy nginx configuration (https) ({{ domain }})"
          template:
            src: "{{ template }}"
            dest: "{{ item }}"
            owner: root
            group: root
            mode: '0644'
          vars:
            template: "nginx-virtual-server.conf"
            default_server: default_server
            domain: domain
            apex: apex
            ssl: ssl
          with_items:
            - "/etc/nginx/sites-available/{{ domain }}"
          notify:
            - Restart nginx
          become: true
          when: "'clouding' in group_names and ssl"
          tags:
            - system-install
        ansible/picodotdev.nginx/tasks/main.yml
        1
        2
        3
        4
        5
        6
        
        ---
        - name: Restart nginx
          systemd:
            state: restarted
            name: nginx
          become: true
        ansible/picodotdev.nginx/tasks/restart.yml
        1
        2
        3
        4
        
        ---
        - name: Restart nginx
          include_tasks:
            file: restart.yml
        ansible/picodotdev.nginx/handlers/main.yml
         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
        
        {% if default_server %}
            {% set _default_server = " default_server" %}
        {% else %}
            {% set _default_server = "" %}
        {% endif %}
        
        log_format nginx_vcombined_{{ domain | replace('.', '_') }} '$host:$server_port ' '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent"';
        
        server {
            listen 80{{ _default_server }};
            listen [::]:80{{ _default_server }};
            server_name {{ domain }};
        
            location ~ /.well-known/acme-challenge/ {
                root   /var/www/{{ domain }};
                index  index.html index.htm;
        
                add_header Cache-Control "no-store";
            }
        
            location / {
                return 301 https://$host$request_uri;
            }
        }
        
        {% if ssl %}
        server {
            listen 443 http2{{ _default_server }} ssl;
            listen [::]:443 http2{{ _default_server }} ssl;
            server_name {{ domain }};
        
            gzip on;
            gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
        
            ssl_certificate      /etc/letsencrypt/live/{{ domain }}/fullchain.pem;
            ssl_certificate_key  /etc/letsencrypt/live/{{ domain }}/privkey.pem;
        
            ssl_protocols TLSv1.2 TLSv1.3;
        
            access_log /var/log/nginx/access.log nginx_vcombined_{{ domain | replace('.', '_') }};
            error_log /var/log/nginx/error.log;
        
            {% if apex %}
                return 301 https://www.$host$request_uri;
            {% else %}
                location /404.html {
                    root   /var/www/{{ domain }};
                    index  index.html index.htm;
        
                    expires 10m;
                    add_header Cache-Control "public";
                }
        
                location ~ /\. {
                    deny all;
                    access_log off;
                    log_not_found off;
                    return 404;
                }
        
                location / {
                    root   /var/www/{{ domain }};
                    index  index.html index.htm;
        
                    #rewrite ^/(.*)index-amp.html$ http://{{ domain }}/$1 permanent;
        
                    expires 1d;
                    add_header Cache-Control "public";
        
                    error_page 404 /404.html;
                }
            {% endif %}
        }
        {% endif %}
        
        ansible/picodotdev.nginx/templates/nginx-virtual-server.conf

        En este ejemplo el contenido del servidor web es simplemente un archivo html con un mensaje de bienvenida inicial que permite comprobar que el servidor funciona correctamente. Este se copia al servidor con Ansible, otra forma de aprovisionamiento del contenido del servidor web virtual podría haber sido utilizar la herramienta rsync para sincronizar una carpeta remota en la raíz de documentos del servidor web y otra opción podría ser hacer un clonado de un repositorio de Git que es por ejemple como funciona GitHub Pages que es simplemente un rama en un repositorio donde se añade el contenido del sitio web.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        
        <!DOCTYPE html>
        <html>
        <head>
        <title>Welcome to Clouding.io!</title>
        <style>
            body {
                width: 35em;
                margin: 0 auto;
                font-family: Tahoma, Verdana, Arial, sans-serif;
            }
        </style>
        </head>
        <body>
        <h1>Welcome to Clouding.io!</h1>
        <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p>
        <p>
          For online documentation and support please refer to
          <a href="http://nginx.org/">nginx.org</a>.<br/>
          Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.
        </p>
        <p><em>Thank you for using nginx.</em></p>
        </body>
        </html>
        
        ansible/picodotdev.site/files/index.html

        Servidor web público

        Para configurar el servidor he utilizado como nombre de dominio uno proporcionado por sslip de forma que sea posible configurar el servidor web virtual en Nginx. Con el servidor configurado con los pasos anteriores el y accediendo con el navegador web a la dirección del servidor se obtiene la página con el mensaje de bienvenida, utiliza el protocolo seguro HTTPS con el certificado de Let’s Encrypt y que el navegador Firefox reconocer como autoridad de certificación válida y sin mostrar ningún error en la barra de direcciones.

        Sitio web

        Sitio web
        Terminal

        El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub.

        » Leer más, comentarios, etc...

        Bitácora de Javier Gutiérrez Chamorro (Guti)

        Vintage Computing Christmas Challenge 2021 (VC3 2021)

        diciembre 22, 2021 04:14


        Descubrí por Twitter el concurso que organizaba Logiker, un aficionado al Commodore 64. Se trataba del Vintage Computing Christmas Challenge 2021 (VC³ 2021). Lo que me llamó la atención es que estaba abierto a cualquier plataforma, y también a cualquier lenguaje. Nunca dominé el C64, así que barajé Sinclair BASIC para ZX Spectrum, Javascript/ECMAScript para …

        Vintage Computing Christmas Challenge 2021 (VC3 2021) Leer más »



        Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

        » Leer más, comentarios, etc...

        Variable not found

        Declaración de espacios de nombre en una línea con file-scoped namespaces de C#10

        diciembre 21, 2021 07:05

        .NET

        Parece que uno de los objetivos de C#10 es eliminar código de los archivos de código fuente de C#, simplificando su codificación y lectura. Lo hemos visto con la introducción de características como directivas using globales o los using implícitos: en ambos casos se ahorraba espacio en vertical sustituyendo los using declarados individualmente en cada archivo por directivas aplicadas de forma global al proyecto, bien de forma explícita o implícita.

        En cambio, los espacios de nombre con ámbito de archivo o namespace declarations permiten ahorrar espacio en horizontal, evitando un nivel de indentación en el código que la mayoría de las veces es innecesario.

        Declaración de namespaces en C#10

        Según comentan en la documentación oficial, examinando archivos de código en el ecosistema de C#, se ha demostrado que más del 99% de ellos presentan una estructura similar a la siguiente, que seguro podéis reconocer muy rápidamente:

        // Directivas using
        ...
        namespace XYZ
        {
        public class Something
        {
        ...
        }
        ... // Otros tipos definidos en el namespace
        }

        Esta estructura hace que casi siempre vuestro código (clases, estructuras, enums, etc.) aparezca desplazado hacia la derecha, a la altura de la primera tabulación.

        En C#10 podemos eliminar este espacio extra utilizando una declaración de espacio de nombre, que tiene la siguiente pinta:

        // Usings
        ...
        namespace XYZ; // Yeah!

        public class Something
        {
        ...
        }
        ... // Otros tipos definidos en el namespace

        Como podéis ver, a diferencia del enfoque tradicional, ya no se trata de un bloque namespace que envuelve los elementos definidos en éste, sino en una declaración que afecta a todo el archivo C#.

        Obviamente, el hecho esto tiene sus limitaciones. Por ejemplo, ya no podremos definir elementos de distintos namespaces en el mismo archivo, pero, bah, ¿quién lo hacía? 😄

        Es decir, esto es válido en un único archivo .cs: dos namespaces distintos, cada uno con elementos definidos en su interior:

        // C# válido en un único archivo .cs
        namespace ABC
        {
        public class MyClassInABC { ... }
        }
        namespace DEF
        {
        public class MyClassInDEF { ... }
        }

        Sin embargo, el siguiente código fallará en compilación si lo intentamos usar en un único archivo C#:

        // Error CS8954: Source file can only contain one 
        // file-scoped namespace declaration

        namespace ABC;
        public class ClassInABC { }

        namespace DEF;
        public class ClassInDEF { }

        Por las mismas razones, tampoco es posible anidar namespaces usando estas declaraciones. En otras palabras, no existe un equivalente para el siguiente código:

        // Válido en C#
        namespace A
        {
        namespace B
        {
        public class MyClass { }
        }
        }

        Lo más parecido que podríamos lograr sería lo siguiente:

        // Válido en C#10
        namespace A.B;
        public class MyClass { }

        En definitiva, un cambio más al lenguaje para ganar pedacitos de pantalla. Cuando empecemos a usar esto de forma intensiva seguro que echaremos de menos ese margen de la izquierda (son muchos años de costumbre) pero seguro que en poco tiempo lo habremos interiorizado y disfrutaremos del espacio ganado :)

        Publicado en Variable not found.

        » Leer más, comentarios, etc...

        Variable not found

        Enlaces interesantes 467

        diciembre 20, 2021 07:29

        Enlaces interesantes

        Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

        Por si te lo perdiste...

        .NET Core / .NET

        ASP.NET Core / ASP.NET / Blazor

        Azure / Cloud

        Conceptos / Patrones / Buenas prácticas

        Data

        Machine learning / IA / Bots

        Web / HTML / CSS / Javascript

        Visual Studio / Complementos / Herramientas

        Xamarin / .NET MAUI / Mobile

        Otros

        Publicado en Variable not found.

        » Leer más, comentarios, etc...

        Blog Bitix

        Configuración de una aplicación con Spring Boot y configuración centralizada con Spring Cloud Config

        diciembre 16, 2021 06:00

        La configuración de una aplicación es indispensable para su funcionamiento, permite no hardcoredar ciertos valores en el código fuente al mismo tiempo que externalizarlos en archivos de más fácil edición. Externalizar la configuración de la aplicación permite utilizar el mismo artefacto binario en todos los entornos, los valores que cambian en cada entorno es posible proporcionarlos de diferentes formas y formatos desde archivos en el classpath hasta variables de entorno o un servidor de configuración. Spring Boot permite obtener los valores de diferentes fuentes e implementa un mecanismo de prioridad para determinar el valor a usar.

        Spring

        Java

        La configuración de Spring Boot proporciona un mecanismo muy flexible para la configuración de diferentes fuentes. Cada fuente tiene un orden de preferencia para establecer los valores de las propiedades, además se integra con el servidor de configuración centralizada de Spring Cloud Config.

        La configuración permite cambiar el comportamiento de la aplicación sin cambiar el código ni generar un nuevo artefacto. No hardcodear los valores en el código y extraer la configuración permite utilizar el mismo artefacto en cualquier entorno, ya sea desarrollo, pruebas o producción. Utilizar el mismo artefacto para todos los entornos tiene la ventaja de no introducir un error en la construcción del artefacto como cabría la posibilidad generando un artefacto binario para cada uno de los entornos, usar el mismo artefacto es necesario para que las pruebas realizadas sobre el artefacto en el entorno de desarrollo o pruebas se consideren válidas para producción.

        Aunque en algunos sitios se recomienda que la configuración de la aplicación esté separada de artefacto de despliegue, en realidad más que la configuración esté separada es necesario poder tener un mecanismo de orden de preferencia de los valores de configuración. La aplicación puede tener unos valores de configuración por defecto pero es necesario poder sobreescribirlos debido a que algunos no se desean incluir en el código fuente o en el propio artefacto, también es necesario para cambiar los valores por defecto incluidos en el artefacto si se desea corregir un error sin necesidad de generar un nuevo artefacto.

        Por otro lado es aconsejable tener bajo el control de versiones los archivos de configuración como cualquier otro archivo de código fuente de la aplicación, para con el historial del control de versiones ver los cambios que se han hecho o volver a versiones anteriores.

        Contenido del artículo

        Necesidades según el rol

        Las diferentes personas cada una con su rol desea tener la capacidad de configurar la aplicación. A los desarrolladores nos interesa para poder externalizar ciertas variables del código de la aplicación para tener la capacidad de cambiar los valores sin modificar el código. Esto es más fácil que encontrar donde están los valores hardcodeados y modificar las diferentes coincidencias, y evitando tener que recompilar.

        Aunque los archivos de configuración no son código ejecutable forman parte del código de la aplicación si la configuración se incluye dentro del artefacto, también por comodidad el desarrollador desea cambiarlos al mismo tiempo que el código para mantener la consistencia entre el código y la configuración, ya que todas la variables de configuración que requiere el código deben tener un valor sino se produce un error en tiempo de ejecución.

        Las personas con el rol de sistemas o SRE y por las tareas de mantenimiento de sistemas y operaciones también requieren tener la capacidad de cambiar las propiedades ajustando los valores a los adecuados según el entorno de ejecución sin modificar el artefacto, quizá no sobrescribir los valores de todas las propiedades pero si las relevantes desde el punto de vista de sistemas.

        Es necesario para ajustar los valores por defecto o hacer una corrección que no requiera generar un nuevo artefacto sino simplemente ajustar un valor de configuración. Por rapidez y porque hacer una corrección generando un nuevo artefacto requiere pasar todo el proceso de pruebas para asegurar que el nuevo artefacto no incluye algún cambio adicional no deseado.

        También por motivos de seguridad es necesario externalizar los valores de algunas variables como contraseñas, claves y certificados, de modo que aunque alguien tenga acceso al artefacto no tenga acceso a las credenciales de los servicios que usa.

        Los valores adicionales se proporcionan habitualmente como variables de entorno o con archivos externalizados del artefacto que se buscan en el sistema de archivos, de esta forma la configuración incluida en el artefacto por los desarrolladores es sobrescrita por la configuración por la proporcionada por las personas con el rol de sistemas.

        La solución para estas diferentes necesidades de los diferentes roles es obtener los valores de las variables de configuración de varias fuentes junto un de orden de preferencia para determinar que valor se toma en caso de que esté definido en varias fuentes.

        En las configuraciones más avanzadas es necesario un mecanismo para que las aplicaciones obtengan la configuración de un servidor donde esté centralizada. Al igual que un servicio de registro y descubrimiento es esencial para los microservicios un servicio de obtención de configuración de donde obtengan su configuración es también útil. Dado el gran número de microservicios de los que puede estar compuesto un sistema, su carácter efímero, los varios entornos de ejecución (desarrollo, pruebas, producción, …) mantener centralizada la configuración en un único sitio hace las cosas mucho más sencillas cuando hay que cambiar el valor de alguna propiedad. En vez de las alternativas con un archivo de configuración, aún externalizado del artefacto, en el sistema de archivos del entorno de ejecución o a través de variables de entorno que deben ser aprovisionadas.

        Configuración en una aplicación de Spring Boot

        Spring Boot integra la funcionalidad de obtener la configuración de varias fuente y define un orden de preferencias en caso de conflicto. Spring Cloud Config Server es un servicio que proporciona un mecanismo adicional para centralizar la configuración de las aplicaciones.

        En una aplicación monolítica, un monolito modular o en un entorno donde no hay muchas aplicaciones el mecanismo de configuración proporcionado Spring Boot es suficiente. Sin embargo, en un entorno de microservicios o donde hay muchas aplicaciones tener una configuración centralizada proporciona varios beneficios. Los beneficios de un servidor de configuración es centralizar en un única fuente lo que facilita su ubicación, modificación y despliegue en las aplicaciones.

        Orden de preferencia de las propiedades

        Spring Boot soporta varias fuentes de las que obtener la configuración desde archivos de configuración en el classpath, archivos externalizados en el sistemas de archivos, argumentos del programa, propiedades del sistema de la máquina virtual, variables de entorno e incluso otros mecanismos extensibles personalizados.

        Cada una de estas fuentes tienen un orden de búsqueda y prioridad donde las fuentes posteriores sobrescriben los valores de las anteriores o se añaden nuevas variables.

        En la documentación de Spring Boot están detalladas estas fuentes y prioridad entre ellas. Por ejemplo, la configuración establecida en los archivos de configuración es sobrescrita por la configuración proporcionada como variables de entorno.

        Con Spring Cloud Config las propiedades del servidor de configuración se cargan con posterioridad de los archivos de datos de configuración incluidos en el classpath dentro del artefacto o de los archivos externalizados en el sistema de archivos. Sin embargo, la configuración establecida como variables de entorno siguen teniendo más preferencia.

        Archivos de datos de configuración

        Los archivos de configuración entre ellos también tienen un orden de búsqueda en varios directorios y prioridad, iguamente detallado en la documentación de Spring Boot. Teniendo más preferencia los archivos externalizados y dentro de estos los más específicos para un entorno de ejecución.

        Las ubicaciones en las que Spring Boot archivos de configuración también tiene una preferencia además de ser a su vez configurable.

        Los archivos de configuración se pueden definir en el formato properties y yaml. La ventaja del formato yaml es que permite agrupar las propiedades de forma jerárquica que es más legible que el formato properties habitualmente utilizado en las aplicaciones Java por defecto. La desventaja de yaml es que es un formato en el que una mala tabulación genera algún tipo de error o mal funcionamiento.

        1
        2
        
        app.properties.classpath=classpath
        app.properties.external=
        
        application-format.properties
        1
        2
        3
        4
        
        app:
          properties:
            classpath: classpath
            external: 
        
        application-format.yml

        Propiedades que afectan a la configuración

        En el sistema de configuración de Spring hay ciertas variables que afectan y permiten adaptar la configuración por defecto a las preferencias o necesidades de la aplicación.

        Algunas de estas propiedades son el nombre del servicio, los perfiles activos o las ubicaciones de búsqueda de archivos de configuración.

        1
        2
        3
        4
        5
        6
        7
        
        spring:
          application:
            name: service
          profiles:
            active: production
          config:
            additional-location: optional:classpath:/custom-config/,optional:file:./custom-config/
        
        spring-boot-config-properties.yml

        Estas otras propiedades se utilizan cuando la aplicación de Spring Boot obtiene la configuración adicionalmente de un servidor de configuración de Spring Cloud Config.

        1
        2
        3
        4
        5
        6
        7
        8
        
        spring:
          config:
            import: optional:configserver:http://localhost:8090
          cloud:
            config:
              name: service
              profile: production
              label: 2.0
        
        spring-boot-cloud-config-properties.yml

        En las rutas de búsqueda con el prefijo optional: en caso de no encontrarse la fuente el inicio de la aplicación en vez de fallar con una excepción se ignora y se continúa a riesgo de utilizar los valores de las fuentes anteriores e ignorando lo que tuviese esa fuente opcional.

        1
        2
        3
        4
        5
        6
        7
        8
        
        spring:
          cloud:
            config:
              location:
                optional: ./config
        spring:
          config:
            import: optional:configserver:http://localhost:8090
        spring-boot-optional.yml

        El servidor de configuración centralizada Spring Cloud Config Server

        Un servidor de configuración permite cambiar o proporcionar una forma adicional de la que la aplicación obtiene propiedades y valores de configuración. La aplicación al iniciar realiza una petición al servidor de configuración y obtiene las propiedades adicionales de configuración. En el caso de Spring Cloud Config Server ofrece una interfaz REST que usa las aplicaciones para realizar la petición.

        Por otro lado, la forma de configurar la aplicación cambia, en vez de proporcionar la configuración a la aplicación a cada una de las instancias de su servicio en variables de entorno o en archivos estáticos es cada instancia de la aplicación la que obtiene la configuración de un servidor. Es muy interesante para las personas con el rol de operaciones o SRE y para la arquitectura del sistema.

        Otra de sus utilidades es una forma de que ciertos servicios obtengan la configuración cuando sus entornos y sistemas de archivos son efímeros como es el caso de las funciones de Google Cloud o lambdas de AWS.

        Dado que este servicio de configuración es esencial para que los microservicios puedan obtener su configuración sin el cual no pueden proporcionar su funcionalidad hay que configurarlo de tal manera que sea tolerante a fallos. Una de las medidas para hacerlo tolerante a fallos es iniciar varias instancias de servidores de configuración, estas instancias se autorregistran en el servicio de descubrimiento para que los microservicios puedan descubrirlos y obtener su configuración al iniciarse.

        Fuentes de configuración

        El servidor de configuración centralizada Spring Cloud Config soporta varios sistemas diferentes en los que almacenar las propiedades de configuración o backends para recuperarlos cuando una instancia del servicio la solicite.

        Una opción es utilizar un repositorio de Git con las ventajas asociadas del control de versiones como historial para mantener un registro de los cambios o volver a una versión anterior. Otros son un sistema de archivos, en una base de datos relacional con JDBC, Redis, Vault y algunos otros específicos más.

        Propiedades que afectan a la configuración de Spring Cloud Config Server

        El servidor de configuración de Spring Cloud Config también tiene variables de configuración, varias según el sistema de almacenamiento o backend donde se persisten las propiedades de configuración de los servicios. Otras propiedades son para proporcionar las credenciales de autenticación de los backends.

        En el caso de Git la propiedad label del servicio en Git puede ser la huella o hash del commit, una rama o una etiqueta.

        1
        2
        3
        4
        5
        6
        
        spring:
          cloud:
            config:
              server:
                git:
                  uri: https://github.com/picodotdev/configuration-repository
        spring-cloud-config-server-git-properties.yml

        En el caso del sistema de archivos como backend esta propiedad permite configurar las rutas en las que buscar los archivos de configuración y la disposición de los archivos de configuración en la estructura de directorios. En este caso la propiedad label es la versión de la aplicación.

        Las propiedades application, profile y label permiten identificar la configuración de un servicio, para un entorno y de una versión específica.

        1
        2
        3
        4
        5
        6
        7
        8
        
        spring:
          profiles:
            active: native
          cloud:
            config:
              server:
                native:
                  searchLocations: file:./misc/config/,file:./misc/config/{application}/,file:./misc/config/{application}/{profile}/,file:./misc/config/{application}/{profile}/{label}
        spring-cloud-config-server-filesystem-properties.yml

        Ejemplo de configuración en aplicación de Spring Boot

        Esta aplicación de Spring Boot tiene varias propiedades de configuración. Para mostrar el mecanismo de preferencia en la resolución de los valores cada una de las propiedades se obtiene de una fuente distinta. En esta lista de menor preferencia a mayor preferencia, desde un archivo de configuración en el classpath, archivo externalizado, servidor de configuración, argumento de programa y variable de entorno.

         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
        
        package io.github.picodotdev.blogbitix.springcloudconfig.service;
        
        import org.springframework.beans.factory.annotation.Value;
        import org.springframework.boot.CommandLineRunner;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        
        @SpringBootApplication
        public class Main implements CommandLineRunner {
        
            @Value("${app.properties.classpath}")
            private String classpath;
        
            @Value("${app.properties.external}")
            private String external;
        
            @Value("${app.properties.argument}")
            private String argument;
        
            @Value("${app.properties.environment}")
            private String environment;
        
            @Value("${app.properties.cloud}")
            private String cloud;
        
            @Override
            public void run(String... args) throws Exception {
                System.out.println("Application classpath property: " + classpath);
                System.out.println("Application external property: " + external);
                System.out.println("Application argument property: " + argument);
                System.out.println("Application environment property: " + environment);
                System.out.println("Application cloud property: " + cloud);
            }
        
            public static void main(String[] args) {
                SpringApplication.run(Main.class, args);
            }
        }
        
        Main-client.java

        Este es el archivo de configuración que se incluye en el classpath y como parte del artefacto, no está externalizado.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        
        spring:
          application:
            name: service
          profiles:
            active: production
          config:
            import: optional:configserver:http://localhost:8090
            failFast: true
          cloud:
            config:
              label: 2.0
        
        app:
          properties:
            classpath: "classpath"
            external: ""
            argument: ""
            environment: ""
            cloud: ""
        application-classpath.yml

        Este archivo de configuración externo al artefacto proporciona el valor de una propiedad.

        1
        2
        3
        
        app:
          properties:
            external: "external"
        application-external.yml

        Sin ninguna configuración adicional y con el servidor de configuración no iniciado estos son lo valores que toman las propiedades en la aplicación.

        1
        2
        
        $ ./gradlew service:run
        
        
        gradle-run-1.sh
        1
        2
        3
        4
        5
        
        Application classpath property: classpath
        Application external property: external
        Application argument property: 
        Application environment property: 
        Application cloud property:
        System.out-1

        Añadiendo al iniciar el programa un argumento o variable de entorno para configurar el valor de una propiedad la aplicación toma el valor proporcionado.

        1
        2
        
        $ ./gradlew service:run --args="--app.properties.argument=argument"
        
        
        gradle-run-2.sh
        1
        2
        3
        4
        5
        
        Application classpath property: classpath
        Application external property: external
        Application argument property: argument
        Application environment property: 
        Application cloud property:
        System.out-2
        1
        2
        
        $ APP_PROPERTIES_ENVIRONMENT="enviroment" ./gradlew service:run
        
        
        gradle-run-3.sh
        1
        2
        3
        4
        5
        
        Application classpath property: classpath
        Application external property: external
        Application argument property: 
        Application environment property: enviroment
        Application cloud property:
        System.out-3

        Con el servidor de configuración iniciado la aplicación en este caso adicionalmente toma el valor de la configuración para la aplicación del servidor. En casos casos anteriores la aplicación en el inicio no falla porque la fuente del servidor de Spring Cloud Config Server se considera opcional.

        1
        2
        
        $ ./gradlew configserver:run
        
        
        gradle-run-4.sh
        1
        2
        3
        4
        5
        
        Application classpath property: classpath
        Application external property: external
        Application argument property: 
        Application environment property: 
        Application cloud property: cloud-default
        System.out-4

        Cambiando la propiedad label o como variable de entorno a través de los argumentos en el inicio del servicio es posible cambiar la versión que el servidor de configuración devuelve para el servicio.

        1
        2
        
        $ SPRING_CLOUD_CONFIG_LABEL="1.0" ./gradlew service:run
        
        
        gradle-run-5.sh
        1
        2
        3
        4
        5
        
        Application classpath property: classpath
        Application external property: external
        Application argument property: 
        Application environment property: 
        Application cloud property: cloud-1.0
        System.out-5
        1
        2
        
        $ SPRING_CLOUD_CONFIG_LABEL="2.0" ./gradlew service:run
        
        
        gradle-run-6.sh
        1
        2
        3
        4
        5
        
        Application classpath property: classpath
        Application external property: external
        Application argument property: 
        Application environment property: 
        Application cloud property: cloud-2.0
        System.out-6

        Las dependencias en el archivo de construcción con Gradle son las siguientes.

         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
        
        plugins {
            id 'application'
        }
        
        repositories {
            mavenCentral()
        }
        
        dependencies {
            implementation(platform('org.springframework.boot:spring-boot-dependencies:2.6.1'))
            implementation(platform('org.springframework.cloud:spring-cloud-dependencies:2021.0.0'))
        
            def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
            implementation('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
            implementation('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
            implementation('org.springframework.cloud:spring-cloud-starter-config', excludeSpringBootStarterLogging)
        
            runtimeOnly('com.google.code.gson:gson:2.8.9')
            runtimeOnly('com.fasterxml.jackson.core:jackson-databind:2.13.0')
            runtimeOnly('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.0')
        }
        
        application {
            sourceCompatibility = JavaVersion.VERSION_17
            targetCompatibility = JavaVersion.VERSION_17
            mainClass = 'io.github.picodotdev.blogbitix.springcloudconfig.service.Main'
        }
        
        
        build-client.gradle

        Ejemplo de configuración centralizada con Spring Cloud Config Server

        El servidor de configuración de Spring Cloud Config es posible implementarlo como una aplicación de Spring Boot. La aplicación de Spring Boot simplemente requiere utilizar la anotación @EnableConfigServer y configurar el almacenamiento del backend para las propiedades de configuración, en el ejemplo utilizando el sistema de archivos.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        
        package io.github.picodotdev.blogbitix.springcloudconfig.configserver;
        
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.cloud.config.server.EnableConfigServer;
        
        @SpringBootApplication
        @EnableConfigServer
        public class Main {
        
            public static void main(String[] args) {
                SpringApplication.run(Main.class, args);
            }
        }
        
        Main-server.java

        Los archivos de configuración para los microservicios en este ejemplo están en el directorio misc/config donde siguiendo algunas convenciones para asignar el nombre a los archivos se pueden personalizar las configuraciones de los microservicios según el entorno y perfil con el que se active. Spring Cloud Config denomina un backend como el sistema de almacenamiento de los datos de configuración en este caso se utiliza el sistema de archivos, sin embargo, hay otras disponibles como un repositorio de git el cual ofrece varias ventajas propias de un repositorio de código como historial, ramas de trabajo y hacer cambios con un commit.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        
        server:
          port: ${port:8090}
        
        spring:
          profiles:
            active: native
          application:
            name: configserver
          cloud:
            config:
              server:
                native:
                  searchLocations: file:./misc/config/,file:./misc/config/{application}/,file:./misc/config/{application}/{profile}/,file:./misc/config/{application}/{profile}/{label}
        
        
        application-server.yml

        Con los siguientes archivos de configuración en el servidor para el servicio, en función de la versión de la aplicación solicitada las propiedades devueltas cambian. Estos comandos solicitan al servidor la configuración de la aplicación a través de una petición de red con la interfaz REST, lo datos se devuelven en formato JSON.

        1
        2
        3
        
        app:
          properties:
            cloud: "cloud-1.0"
        application-1.0.yml
        1
        2
        3
        
        app:
          properties:
            cloud: "cloud-2.0"
        application-2.0.yml
        1
        2
        3
        
        app:
          properties:
            cloud: "cloud-default"
        application-default.yml
        1
        2
        
        $ curl -v "http://localhost:8090/service/production/1.0"
        
        
        curl-spring-cloud-config-server-1.0.sh
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        
        {
          "name": "service",
          "profiles": [
            "production"
          ],
          "label": "1.0",
          "version": null,
          "state": null,
          "propertySources": [
            {
              "name": "file:misc/config/service/production/1.0/application.yml",
              "source": {
                "app.properties.cloud": "cloud-1.0"
              }
            },
            {
              "name": "file:misc/config/service/application.yml",
              "source": {
                "app.properties.cloud": "cloud-default"
              }
            }
          ]
        }
        curl-spring-cloud-config-server-1.0.out
        1
        2
        
        $ curl -v "http://localhost:8090/service/production/2.0"
        
        
        curl-spring-cloud-config-server-2.0.sh
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        
        {
          "name": "service",
          "profiles": [
            "production"
          ],
          "label": "2.0",
          "version": null,
          "state": null,
          "propertySources": [
            {
              "name": "file:misc/config/service/production/2.0/application.yml",
              "source": {
                "app.properties.cloud": "cloud-2.0"
              }
            },
            {
              "name": "file:misc/config/service/application.yml",
              "source": {
                "app.properties.cloud": "cloud-default"
              }
            }
          ]
        }
        curl-spring-cloud-config-server-2.0.out
        1
        2
        
        $ curl -v "http://localhost:8090/service/production/"
        
        
        curl-spring-cloud-config-server-default.sh
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        
        {
          "name": "service",
          "profiles": [
            "production"
          ],
          "label": null,
          "version": null,
          "state": null,
          "propertySources": [
            {
              "name": "file:misc/config/service/application.yml",
              "source": {
                "app.properties.cloud": "cloud-default"
              }
            }
          ]
        }
        curl-spring-cloud-config-server-default.out

        Dado que el servicio de configuración se convierte en crítico para el inicio de las aplicaciones es recomendable tener varias instancias del mismo para proporcionar tolerancia a fallos. Y en una arquitectura de microservicios quizá utilizando registro y descubrimiento de servicios.

        Otra necesidad es cifrar algunas propiedades, para ello el servidor de configuración también proporciona dos endpoints uno para hacer el cifrado y otro para hacer el descifrado.

        Incluso es posible recargar la configuración de una aplicación de Spring Boot sin reiniciarla.

        Terminal

        El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
        ./gradlew service:run

        » Leer más, comentarios, etc...

        Bitácora de Javier Gutiérrez Chamorro (Guti)

        El problema con Cloudflare y nginx

        diciembre 02, 2021 08:55


        Después de escribir WordPress y el problema con wp_options en donde explicaba que gracias al nuevo alojamiento había podido migrar del servidor web Apache a nginx continué haciendo seguimiento y pruebas. Me di cuenta que aunque nginx iba bastante rápido, en una máquina de esa capacidad debería dar bastante más rendimiento. Observé también como ocasionalmente, …

        El problema con Cloudflare y nginx Leer más »



        Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

        » Leer más, comentarios, etc...

        Una sinfonía en C#

        oAuth2 paso a paso “¿Qué es oAuth?”

        diciembre 01, 2021 12:00

        oAuth es prácticamente la opción más utilizada para securizar aplicaciones modernas por diferentes motivos, éste es el primer de una serie de post sobre oAuth2, intentando aclarar cada concepto paso a paso. Si bien es un tema bastante complejo pero también muy importante y en lo personal creo que todo desarrollador debería comprenderlo bien.

        También veremos OpenID Connect como parte de esta serie ya que están muy relacionados.

        A fines prácticos vamos a hablar siempre de oAuth2.

        ¿Qué es oAuth2?

        oAuth es un protocolo standard, de autorización, que provee flujos específicos para aplicación web, desktop, mobile y disponistivos de la vida diaría” Algo así dice su definición, así que vamos a aclarar de a poco cada concepto.

        Autorización vs autenticación

        Lo primero es diferenciar autorización de autenticación:

        Autorizado es que tiene permisos para hacer algo.

        Autenticado es que se puede verificar quién es.

        Estar autorizado

        El ejemplo más claro para mí es un ticket de cine, el mismo no tiene nombre, ni dice nada sobre la persona que lo compró, pero autoriza a quien lo tenga a hacer algo, en este caso a entrar a ver una película, y solo lo autoriza a hacer eso. Es importante tener en cuenta que no importa si yo compré el ticket, quien lo tenga podrá entrar al cine aunque lo haya encontrado en el suelo.

        El simple hecho de poseer el ticket (vamos a llamarlo Token) nos habilita a hacer algo independientemente de nuestra identidad, y de hecho, quien nos autorice no necesita saber nada sobre nosotros, solo que tenemos ese Token (en este caso en forma de un ticket de cine)

        Estar autenticado

        En este caso la autenticación verifica que somos quienes decimos ser, pero no implica que estemos autorizados. Un ejemplo puede ser un documento, permite verificar datos propios pero no implica que tenga permisos de hacer algo, por ejemplo, pasar una frontera, o volviendo al ejemplo anterior ingresar al cine, si no tengo un ticket no puedo entrar al cine por más que tenga un DNI o pasaporte. oAuth no provee mecanismos de autenticación

        Algunas aclaraciones sobre OpenID Connect (OIDC)

        Por simplificar, a veces se llama OIDC cuando tenemos ambas cosas, oAuth y OIDC porque este último es una extensión del primero, pero siendo rigurosos OIDC provee una capa de autenticación a oAuth que es un protocolo de autenticación.

        ¿Qué problema intenta solucionar oAuth?

        Sin entrar en historias largas, el problema que intentams solucionar es:

        Autorizar a un cliente, sin exponer credenciales ni identidad, dando un acceso solo un conjunto de recursos por un tiempo limitado.

        Algo de vocabulario

        Hay mucho vocabolario de oAuth y es bueno comenzar a familiarizarnos con él, comencemos con lo más básico.

        ¿Quién es un cliente?

        Un cliente será cualquier aplicación que quiera tener acceso a un recuso. Nunca será una persona física. Algunos ejemplos:

        • Un sitio web
        • Un frontend que quiere acceder a una API
        • Un backend que accede a otro backend

        ¿Qué son recursos?

        Llamamos recursos a todo aquello a lo que podemos dar un nivel de acceso:

        • Una aplicación
        • Una parte de la aplicación
        • Una API
        • Un sistema
        • Un conjunto de información
        • Una funcionalidad del protocolo
        • Etc.

        ¿Y quién se encarga de dar acceso a los clientes sobre esos recursos?

        Esta entidad se llama Identity provider o IDP (por ejemplo Facebook), es quien conoce a los usuarios y otorga permisos a los clientes (el sitio de Adobe) a acceder a mi información (el recurso).

        Algunos ejemplos de uso de oAuth

        En Instagam podemos tener la opción de que al crear una publicación se publique también en Twitter, esto es un buen ejemplo de oAuth.

        Instagram quiere publicar en Twitter en nuestro nombre, bien, entonces:

        • Instagram es el cliente
        • La API de Twitter es el recurso
        • Twitter es también el IDP (porque somos usuarios de Twitter)

        Entonces, Twitter nos conoce como usuarios y también a Instagram como cliente, y simplemente le da acceso a la API de publicación en nuestro nombre por un tiempo limitado a través de un Token. De este modo solucionamos muchos problemas.

        Lo mismo pasa cuando queremos importar contactos de una red social a otra, al final, concentimos acceso a un recurso que es nuestro (nuestros contactos, nuestro timeline, es así que nuestro rol es de Resource owner) a un cliente (Instagram o quién sea) y ese acceso lo otorga el IDP proveyendo un Token al cliente.

        Identity provider

        El IDP es el responsable de otorgar acceso, es quien conoce a los clientes y los recursos, cuando un cliete solicita acceso a un recurso el IDP le provee un Token, el Resource Provider (quien tiene el recurso, por ejemplo Twitter) verificar que ese Token es válido.

        Quien gestiona los recursos no siempre es también el IDP

        El IDP puede no ser quien gestione los recusos como hemos dicho, solo quien gestiona los accesos, por ejemplo en Google tenemos un único login (IDP) con el cual accedemos a muchos servicios (Recursos) que no están en el mismo dominio (y de hecho, no son la misma aplicación ni nada). Entonces el login central de Google es el IDP y la API de GMaps será el Resource Provider, y la misma estará separada en recursos, por ejemplo, la capacidad de buscar una dirección será un Recurso particular sobre el que podemos perdir acceso, pero tal vez no podamos hacerlo para crear puntos de interés en el mapa.

        Nota: es por eso que en 2021 hubo un fallo global en los servicios de Google porque falló el servicio de autenticación (IDP) que todos los otros servicios usan.

        Repasando el vocabulario

        • Recurso: Un elemento al que se puede otorgar acceso, como una API, un dato, una aplicación, etc.
        • Token: Un elemento que sirve para otorgar el acceso, en forma de un conjunto de caracteres (un GUID, un número, o algo más complejo como un JSON Web Token), suelen tener un acceso limitado a ciertos recursos por un tiempo limitado.
        • IDP: Es quien otorga Tokens, quien conoce a los usuarios y a los clientes.
        • Cliente: Una aplicación que quiere acceder a un recurso.
        • Resource Provider: Quien gestiona los recursos, verificar que las solicitudes de acceso de los clientes son válidas para el IDP configurado
        • Resource Owner: El propietario de los recursos, no siempre son personas, en el caso de que el recurso sea una API el RO será la aplicación, pero en caso de nuestro muro de Facebook seremos nosotros.

        Más o menos los actores se relacionan así (de modo simplificado)

        Bien, en la próxima continuamos hablando sobre Tokens. Nos leemos.

        » Leer más, comentarios, etc...

        Arragonán

        Cambio en paralelo de un Event Bus

        diciembre 01, 2021 12:00

        Una de las cosas en las que estamos trabajando desde hace unas semanas en el equipo de producto de Sigma Rail es en cambiar el bus de eventos, algo con lo que aún no hemos terminado porque está siendo un cambio en paralelo que vamos haciendo progresivamente.

        Originalmente empezamos con una implementación de Event Bus en memoria por simplicidad, lo cual nos sirvió para tener un diseño que nos permitía desacoplar fácilmente lógica de los casos de uso con los side-effects que deben desencadenar.

        Uno de esos side-effects es la generación de thumbnails de las imágenes que suben los usuarios a nuestra plataforma, ahora mismo se generan 4 thumbnails distintos que luego terminan persistiéndose en Amazon S3. Teniendo en cuenta que las imágenes de los usuarios pesan bastante y que lo normal es que se suban varios gigas de imágenes de golpe, resultaba en que se empeoraba la experiencia de usuario al tardar más en finalizar la subida de ficheros y se exigía usar instancias con más recursos, y obviamente más caras. De modo que se hacía patente que había que empezar a resolver ese tema con una solución realmente eventual.

        Qué es un Event Bus

        El bus de eventos es como solemos llamar al intermediario responsable de hacer llegar los eventos que genera un componente publisher para que otros componentes subscribers los reciban, con algún tipo de implementación basada en el patrón Pub-Sub.

        Ese patrón lo utilizamos cuando queremos evitar que un componente que genera eventos esté acoplado a quienes deben consumirlo.Hacemos que ese componente envíe los eventos que genera al bus de eventos de dominio, y este bus informa de esos eventos a los componentes que se hayan suscrito al respectivo tipo de evento.

        Esquema representando un event bus

        Qué es hacer un cambio en paralelo

        Según Danilo Sato en la web de (san) Martin Fowler:

        Parallel change, also known as expand and contract, is a pattern to implement backward-incompatible changes to an interface in a safe manner, by breaking the change into three distinct phases: expand, migrate, and contract.

        Lo que viene siendo introducir cambios retro compatibles haciendo baby steps que no rompan una interface existente hasta que todos los clientes de esa interface no se hayan adaptado.

        En este ejemplo vamos a romper la interface existente porque vamos a pasar a una ejecución en el mismo proceso a en distintos workers de forma distribuida. Además como parte de cada uno de los cambios integramos con la línea principal de desarrollo y desplegamos a producción (obviamente antes en desarrollo, stage, etc).

        Esto es porque queremos evitar hacer grandes reescrituras y despliegues a modo big-bang, ir haciendo cambios razonablemente pequeños y hacer una entrega continua que nos ayude a reducir riesgos y detectar posibles problemas pronto.

        Personalmente me gustó mucho el taller que preparó Eduardo Ferro sobre el tema, muy recomendable para practicar en un entorno controlado.

        La interface EventBus

        Como lenguaje de programación que usamos es Python y tenemos la convención de equipo de usar type hints siempre que sea posible; para definir la interface usamos una clase base EventBus que define métodos para añadir un subscriber (que a su vez utilizan una clase base Subscriber). En otros lenguajes hubiéramos usado interfaces frente a clases abstractas.

        La interface de EventBus es sencilla: Expone la posibilidad de publicar eventos, añadir suscriptores al bus y limpiar todos los suscriptores del bus. Mientras que de los suscriptores se espera una tupla con los nombres de los eventos que le interesan y se espera que sean callable.

        La clase abstracta EventBus queda algo así:

        class EventBus(ABC):
           @abstractmethod
           def publish(self, events: List[DomainEvent]):
               pass
         
           @abstractmethod
           def add_subscriber(self, subscriber: Subscriber):
               pass
         
           @abstractmethod
           def clean_subscribers(self):
               pass
        

        Mientras que la clase abstracta Subscribers es algo como:

        class Subscriber(ABC):
           @abstractmethod
           def __call__(self, event: dict):
               pass
         
           @abstractmethod
           def subscribed_to(self) -> Tuple[str, ...]:
               pass
        

        Punto de partida, implementación en memoria

        Una implementación de Event Bus en memoria es muy naive, pero funciona a pequeña escala o cuando tienes side-effects muy ligeros.

        Lo bueno es que como comentaba al inicio, nos habilita a diseñar nuestro software de forma eventual aunque realmente no lo sea, sin tener que introducir nada a nivel de infraestructura y añadir complejidad operacional. Lo malo es que todo ocurre en la misma máquina y petición, así que nos perdemos el habitual beneficio de este tipo de diseños de tener mayor facilidad de escalabilidad horizontal y mejorar el rendimiento/UX.

        En nuestro caso, esta implementación InMemoryEventBus utiliza un diccionario para guardar todos los suscriptores que se le vayan añadiendo a add_subscriber, mientras que a la hora de publicar con publish lo que ocurre es que se recorre los eventos recibidos y se comprueba si los suscriptores están subscribed_to a cada unos de los type_name de los eventos para llamar a su __call__ si es así. Y tenemos una serie de tests automáticos con escenarios que comprueban cuando sí o no se recibe la llamada a un doble que actúa como suscriptor.

        A tener en cuenta que, aunque todo ocurra en memoria, simulamos el serializar y deserializar los eventos para que el día que se cambie a otro adapter que requiera de infraestructura estar seguros de que los suscriptores no necesiten modificar su código. Por eso en nuestro caso el publish transforma el evento en un diccionario que es lo que los Subscriber esperan. De un modo similar en una aplicación web/http recoge todo el contenido del cuerpo de la request. También os digo que posiblemente podríamos haber envuelto el contenido del diccionario en algún objeto propio para esconder ese detalle de implementación.

        Este sería el punto de partida antes de empezar con el cambio en paralelo.

        Implementando el adapter para SQS

        Al necesitar que ocurran cosas de forma realmente eventual nos decidimos por usar SQS, manteniendo todo en el mismo repositorio y buscando maneras de desplegar workers que ejecuten los distintos subscribers para poder escalar de forma independiente unos de otros.

        El primer paso fue implementar el adapter de SQSEventBus usando los mismos escenarios de tests que los de memoria. Aunque tuvimos que introducir algún pequeño cambio en la implementación de esos tests, ya que había que hacer una llamada explícita al consumo de mensajes de la cola SQS.

        La implementación es más elaborada en este caso. En add_subscriber se crea una cola específica en SQS por subscriber y se mantiene también la referencia de cada suscriptor en un diccionario. En este caso el publish elige a qué cola SQS publica el evento con la misma comprobación sobre el subscribed_to, esto es porque queremos añadir sólo los eventos en cada cola está interesada en consumir.

        Todo esto lo he resumido como un paso, pero podemos haberlo ido integrando en la línea de desarrollo principal y desplegado en varios. De momento con que pase los tests nos vale que de momento no se va a ejecutar nada de este código en producción.

        Implementando un adapter para publicar eventos simultáneament en memoria y SQS

        El siguiente paso que queremos dar, es ver que seamos capaces de que ese adapter que hemos implementado publica los eventos en mensajes SQS. Sólo verlo, sin afectar al resto del comportamiento.

        Para ello decidimos crear un HybridEventBus, que es un nuevo adapter que combina ambas implementaciones recibiéndolas en el constructor, en este paso simplemente llama a las implementaciones que compone. Con esto podría habernos valido para validar la publicación, pero no queremos crear demasiadas colas SQS por entorno de momento y decidimos utilizar sólo el suscriptor ThumbnailCreator que es el primero que queremos migrar.

        Quedando el add_subscriber algo como

        def add_subscriber(self, subscriber: BaseSubscriber):
           self.__in_memory.add_subscriber(subscriber)
           if isinstance(subscriber, ThumbnailCreator):
               self.__sqs.add_subscriber(subscriber)
        

        Para testear automáticamente nos basamos en los tests anteriores comprobando que cuando se recibe un evento esperado por un doble que herede de ThumbnailCreator, éste se llama 2 veces.

        De nuevo podemos integrarlo y desplegarlo, aún no se ejecuta nada de SQS en producción. En el siguiente paso es cuando empieza.

        Publicación de eventos simultánea en memoria y SQS

        En nuestro caso, tenemos centralizada la inyección de dependencias, de modo que cambiando de ahí la implementación todos los que reciben esa dependencia pasan a utilizar la implementación híbrida.

        De:

        def event_bus(self):
           return InMemoryEventBus()
        

        A:

        def event_bus(self):
           return HybridEventBus(self.in_memory_event_bus(), self.sqs_event_bus())
        

        Eso ya sí impacta en el código de producción, así que tras integrarlo y desplegarlo se que comprueba que todo sigue funcionando correctamente y que, aunque aún no consumimos los eventos de SQS, podemos validar que se crean las colas esperadas y que están llegando los tipos de eventos que esperamos en ellas.

        Consumir eventos de memoria y de colas SQS

        Una vez viendo que hay mensajes que representan nuestros eventos en las colas, el siguiente paso es empezar a consumirlos. Así que se abre por fin el melón de los workers, que en nuestro caso desplegamos en Amazon Elastic Beanstalk.

        Resumen rápido de cómo funciona el happy path:

        • A Elastic Beanstalk le configuras la url de la cola que debe escuchar y un endpoint http, ese endpoint recibirá vía POST el contenido de los mensajes de esa cola
        • El propio entorno provee de un SQS Dameon que se encarga de leer mensajes de la cola y mandarlos a ese endpoint
        • Si va todo ok y devolvemos en ese endpoint un status 200, SQS Dameon borra el mensaje de la cola

        Para entender mejor cómo funcionaba me resultó bastante aclarador el post AWS Elastic Beanstalk Worker environment deep dive.

        Así que lo que tenemos es un único endpoint para recibir esos mensajes como puerta de entrada de los mensajes, a partir de ahí buscamos qué suscriptor es el que debe consumir el evento que contiene el mensaje por una variable de entorno SUBSCRIBER_NAME, esa información también la configuramos para que aparezca en los logs. Ese endpoint podemos dejarlo testeado automáticamente y de nuevo integrar y desplegar, aún no se ejecutará en producción.

        Aprovisionamiento de workers como suscriptores

        El siguiente paso es el aprovisionamiento de esos entornos, que lo que hemos terminado haciendo a nivel de AWS Elastic Beanstalk es crear un entorno tipo worker nuevo por cada subscriber.

        Sin entrar en detalles por no desviarme mucho de tema, en nuestro caso optamos por provisionar esos entornos y desplegar programáticamente cuando arranca el entorno principal usando el SDK oficial en python de AWS. Tiene limitaciones pero de momento es la mejor opción que hemos encontrado.

        Así que una vez implementada la parte de aprovisionamiento al arrancar la aplicación ya tenemos workers consumiendo los mensajes de las colas SQS.

        Por la implementación de HybridEventBus se va a llamar a ThumbnailCreator una vez por estar suscrito en memoria y otra por estarlo en SQS, como es una operación idempotente no hay gran problema con que de momento se genere 2 veces. Así que tras integrar y desplegar, podemos validar a través de los logs que se está ejecutando correctamente el subscriber dentro del worker.

        Pasando a consumir sólo de colas SQS

        Ahora ya que tenemos validado que se generan los thumbnails vía los nuevos workers toca empezar a hacer limpieza. Tenemos que quitar ese subscriber de la ejecución en memoria para que sea realmente eventual.

        Modificando ligeramente HybridEventBus (y sus tests), podemos hacemos que no se añada el subscriber para el bus de eventos, sólo en el de SQS:

        def add_subscriber(self, subscriber: BaseSubscriber):
           if isinstance(subscriber, ThumbnailCreator):
               self.__sqs.add_subscriber(subscriber)
           else:
               self.__in_memory.add_subscriber(subscriber)
        

        Una vez más se vuelve integrar y desplegar, comprobando que todo continua correcto.

        En este último paso tenemos que tener en cuenta que hemos cambiado el comportamiento del sistema:

        • Lo más evidente es que las peticiones de subida responden más rápido
        • Se ha ganado en resiliencia, si la generación del thumnbail falla la petición de subida ya no lo hace. El mensaje quedará en la cola de nuevo y se reintentará generar el thumbnail en otro momento
        • Precisamente aunque la acción de subida se haya ejecutado con éxito el thumbnail no estará disponible inmediatamente, cosa que en este caso puede llegar a impactar en la interfaz de usuario, debemos contar con ello

        Este es un trabajo que todavía no hemos finalizado, tenemos algunos suscriptores más que iremos migrando poco a poco hasta que podamos borrar tanto HybridEventBus como InMemoryEventBus como pasos finales.


        Aunque seguir esta práctica pueda dar la sensación de ir más lentos es algo que reduce el riesgo, podemos equivocarnos también pero sin tener que desechar tanto trabajo y puede ayudarnos a darnos cuenta antes si estamos en un callejón sin salida. No tenemos ramas sin integrar durante muchos días o incluso semanas que luego generan conflictos dolorosos, eso también facilita que en caso de aparcarlo por otras prioridades podamos retomarlo con mucha más facilidad.

        La parte negativa es que, hasta que el cambio no está hecho, tenemos más complejidad en el código y deuda técnica. Así que cuidado con que esos cambios se eternicen.

        » Leer más, comentarios, etc...

        Una sinfonía en C#

        noviembre 29, 2021 08:33

        Hola a todos!

        Éste es el primer de una serie de post sobre oAuth2, intentando aclarar cada concepto paso a paso.

        La idea es explicar lo mejor posible todos los conceptos (al menos los más importantes) para que se entienda lo mejor posible, es un tema bastante complejo pero también muy importante y en lo personal creo que todo desarrollador debería comprenderlo bien.

        También veremos OpenID Connect como parte de esta serie ya que están muy relacionados.

        A fines prácticos vamos a hablar siempre de oAuth2.

        ¿Qué es oAuth2?

        oAuth es un protocolo standard, de autorización, que provee flujos específicos para aplicación web, desktop, mobile y disponistivos de la vida diaría” Algo así dice su definición, así que vamos a aclarar de a poco cada concepto.

        Autorización vs autenticación

        Lo primero es diferenciar autorización de autenticación:

        Autorizado es que tiene permisos para hacer algo.

        Autenticado es que se puede verificar quién es.

        Estar autorizado

        El ejemplo más claro para mí es un ticket de cine, el mismo no tiene nombre, ni dice nada sobre la persona que lo compró, pero autoriza a quien lo tenga a hacer algo, en este caso a entrar a ver una película, y solo lo autoriza a hacer eso. Es importante tener en cuenta que no importa si yo compré el ticket, quien lo tenga podrá entrar al cine aunque lo haya encontrado en el suelo.

        El simple hecho de poseer el ticket (vamos a llamarlo Token) nos habilita a hacer algo independientemente de nuestra identidad, y de hecho, quien nos autorice no necesita saber nada sobre nosotros, solo que tenemos ese Token (en este caso en forma de un ticket de cine)

        Estar autenticado

        En este caso la autenticación verifica que somos quienes decimos ser, pero no implica que estemos autorizados. Un ejemplo puede ser un documento, permite verificar datos propios pero no implica que tenga permisos de hacer algo, por ejemplo, pasar una frontera, o volviendo al ejemplo anterior ingresar al cine, si no tengo un ticket no puedo entrar al cine por más que tenga un DNI o pasaporte. oAuth no provee mecanismos de autenticación

        Algunas aclaraciones sobre OpenID Connect (OIDC)

        Por simplificar, a veces se llama OIDC cuando tenemos ambas cosas, oAuth y OIDC porque este último es una extensión del primero, pero siendo rigurosos OIDC provee una capa de autenticación a oAuth que es un protocolo de autenticación.

        ¿Qué problema intenta solucionar oAuth?

        Sin entrar en historias largas, el problema que intentams solucionar es:

        Autorizar a un cliente, sin exponer credenciales ni identidad, dando un acceso solo un conjunto de recursos por un tiempo limitado.

        Algo de vocabulario

        Hay mucho vocabolario de oAuth y es bueno comenzar a familiarizarnos con él, comencemos con lo más básico.

        ¿Quién es un cliente?

        Un cliente será cualquier aplicación que quiera tener acceso a un recuso. Nunca será una persona física. Algunos ejemplos:

        • Un sitio web
        • Un frontend que quiere acceder a una API
        • Un backend que accede a otro backend

        ¿Qué son recursos?

        Llamamos recursos a todo aquello a lo que podemos dar un nivel de acceso:

        • Una aplicación
        • Una parte de la aplicación
        • Una API
        • Un sistema
        • Un conjunto de información
        • Una funcionalidad del protocolo
        • Etc.

        ¿Y quién se encarga de dar acceso a los clientes sobre esos recursos?

        Esta entidad se llama Identity provider o IDP (por ejemplo Facebook), es quien conoce a los usuarios y otorga permisos a los clientes (el sitio de Adobe) a acceder a mi información (el recurso).

        Algunos ejemplos de uso de oAuth

        En Instagam podemos tener la opción de que al crear una publicación se publique también en Twitter, esto es un buen ejemplo de oAuth.

        Instagram quiere publicar en Twitter en nuestro nombre, bien, entonces:

        • Instagram es el cliente
        • La API de Twitter es el recurso
        • Twitter es también el IDP (porque somos usuarios de Twitter)

        Entonces, Twitter nos conoce como usuarios y también a Instagram como cliente, y simplemente le da acceso a la API de publicación en nuestro nombre por un tiempo limitado a través de un Token. De este modo solucionamos muchos problemas.

        Lo mismo pasa cuando queremos importar contactos de una red social a otra, al final, concentimos acceso a un recurso que es nuestro (nuestros contactos, nuestro timeline, es así que nuestro rol es de Resource owner) a un cliente (Instagram o quién sea) y ese acceso lo otorga el IDP proveyendo un Token al cliente.

        Identity provider

        El IDP es el responsable de otorgar acceso, es quien conoce a los clientes y los recursos, cuando un cliete solicita acceso a un recurso el IDP le provee un Token, el Resource Provider (quien tiene el recurso, por ejemplo Twitter) verificar que ese Token es válido.

        Quien gestiona los recursos no siempre es también el IDP

        El IDP puede no ser quien gestione los recusos como hemos dicho, solo quien gestiona los accesos, por ejemplo en Google tenemos un único login (IDP) con el cual accedemos a muchos servicios (Recursos) que no están en el mismo dominio (y de hecho, no son la misma aplicación ni nada). Entonces el login central de Google es el IDP y la API de GMaps será el Resource Provider, y la misma estará separada en recursos, por ejemplo, la capacidad de buscar una dirección será un Recurso particular sobre el que podemos perdir acceso, pero tal vez no podamos hacerlo para crear puntos de interés en el mapa.

        Nota: es por eso que en 2021 hubo un fallo global en los servicios de Google porque falló el servicio de autenticación (IDP) que todos los otros servicios usan.

        Repasando el vocabulario

        • Recurso: Un elemento al que se puede otorgar acceso, como una API, un dato, una aplicación, etc.
        • Token: Un elemento que sirve para otorgar el acceso, en forma de un conjunto de caracteres (un GUID, un número, o algo más complejo como un JSON Web Token), suelen tener un acceso limitado a ciertos recursos por un tiempo limitado.
        • IDP: Es quien otorga Tokens, quien conoce a los usuarios y a los clientes.
        • Cliente: Una aplicación que quiere acceder a un recurso.
        • Resource Provider: Quien gestiona los recursos, verificar que las solicitudes de acceso de los clientes son válidas para el IDP configurado
        • Resource Owner: El propietario de los recursos, no siempre son personas, en el caso de que el recurso sea una API el RO será la aplicación, pero en caso de nuestro muro de Facebook seremos nosotros.

        Más o menos los actores se relacionan así (de modo simplificado)

        Bien, en la próxima continuamos hablando sobre Tokens. Nos leemos.

        » Leer más, comentarios, etc...

        Meta-Info

        ¿Que es?

        Planeta Código es un agregador de weblogs sobre programación y desarrollo en castellano. Si eres lector te permite seguirlos de modo cómodo en esta misma página o mediante el fichero de subscripción.

        rss subscripción

        Puedes utilizar las siguientes imagenes para enlazar PlanetaCodigo:
        planetacodigo

        planetacodigo

        Si tienes un weblog de programación y quieres ser añadido aquí, envíame un email solicitándolo.

        Idea: Juanjo Navarro

        Diseño: Albin