<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>bash &amp;mdash; Cyberdyne Systems</title>
    <link>https://noblogo.org/aytin/tag:bash</link>
    <description>&#34;Fare o non fare. Non c&#39;è provare!&#34;</description>
    <pubDate>Thu, 30 Apr 2026 09:55:33 +0000</pubDate>
    <item>
      <title>Script per la gestione della configurazione di una vpn wireguard</title>
      <link>https://noblogo.org/aytin/script-per-la-gestione-della-configurazione-di-una-vpn-wireguard</link>
      <description>&lt;![CDATA[vpn&#xA;&#xA;Cos&#39;è wireguard e come si configura, lo sappiamo ma mi serviva qualcosa che mi permettesse di fare provisioning e deprovisioning dei certificati in maniera più semplice di quanto già si faccia con i tool disponibili nella userland, wg e wg-quick.&#xA;&#xA;E questo è il motivo principale per cui è nato questo script.&#xA;!--more--&#xA;Prende come riferimento una configurazione road warrior, il primo scenario di una topologia point-to-site, quella più comune.&#xA;&#xA;Alcune considerazioni&#xA;Lo script definisce una workdir dove deposita tutte le configurazioni e i certificati.&#xA;Le precondizioni sono legate alla presenza dei tool userland wg-tools (ovviamente) e di ufw. Vengono comunque verificate nello script.&#xA;&#xA;Altra semplificazione è data dal fatto di allocare una /24 come rete wireguard. 254 host per una rete domestica sono più che sufficienti.&#xA;&#xA;L&#39;allocazione degli ip viene fatta con un progressivo memorizzato nel file &#39;ip\renew&#39;. Gli ip dei certificati revocati vengono mantenuti in un file, &#39;ip\release&#39;.&#xA;Quando si crea un nuovo certificato, si controlla prima che ci sia un ip da recuperare in ip\release. Se la lista è vuota si ricorre al progressivo.&#xA;&#xA;La creazione della configurazione lato server passa da due file di configurazione: una configurazione globale ottenuta tracciando l&#39;evoluzione iniziale in termini di aggiunta o rimozione dei client peer e destinata al rilascio in esecuzione. L&#39;altra contenente la sola configurazione del server e usata unicamente per la rigenerazione della configurazione globale a partire dai peer esistenti.&#xA;&#xA;L&#39;operazione di init è propedeutica per addclient, removeclient, rebuild, deploy, share.&#xA;&#xA;Le operazioni a disposizione sono:&#xA;&#xA;init: inizializza la configurazione di un server wireguard&#xA;addclient: crea il certificato lato client e aggiunge il relativo peer sulla configurazione del server&#xA;removeclient: cancella il certificato lato client e rimuove il relativo peer sulla configurazione del server&#xA;rebuild: rigenera la configurazione del server usando i certificati client esistenti&#xA;deploy: finalizza la configurazione riavviando il servizio con le nuove impostazioni&#xA;share: crea i qr-code per i certificati client da utilizzare nelle configurazioni&#xA;&#xA;Per tutto il resto, i commenti nel codice sono abbastanza esplicativi.&#xA;Lo script&#xA;!/bin/bash&#xA;&#xA;Questo è il file di configurazione globale, i dati sono fittizi.&#xA;Può essere mantenuto nello script o nella home dell&#39;utente &#xA;(es. $HOME/.config/wgman.conf) e importato &#xA;con &#39;source&#39; all&#39;inizio.&#xA;WGINTERFACE=wg0&#xA;PHYSICALINTERFACE=eth0&#xA;NETWORK=&#34;192.168.15.0&#34;&#xA;SUBNETMASK=&#34;24&#34;&#xA;SERVERIPADDRESS=&#34;192.168.15.1&#34;&#xA;DNS=&#34;1.1.1.1&#34;&#xA;LISTENPORT=&#34;51820&#34;&#xA;ENDPOINT=wireguard.nodns.net:51820&#34;&#xA;WORKDIR=&#34;$HOME/wireguard&#34;&#xA;&#xA;checkdependency() {&#xA;    if test -e /usr/sbin/ufw; then&#xA;        if test -e /usr/bin/wg; then&#xA;            return 0&#xA;        else&#xA;            return 2&#xA;        fi&#xA;    else&#xA;        return 1&#xA;    fi&#xA;}&#xA;&#xA;isinit() {&#xA;    test -e ${WORKDIR}/conf.d/serverwg.conf &amp;&amp; return 0 || return 1&#xA;}&#xA;&#xA;Genera la chiave privata&#xA;genprivkey() {&#xA;    wg genkey | tee ${WORKDIR}/keys/$1privkey&#xA;}&#xA;&#xA;Genera la pre-shared key&#xA;genclientpsk() {&#xA;    # Evita il warning sui permessi del file che devono essere 600&#xA;    umask 077&#xA;    wg genpsk | tee ${WORKDIR}/psk/$1psk&#xA;}&#xA;&#xA;Rilascio di un nuovo indirizzo ip. Questa funziona viene invocata da&#xA;addclient().&#xA;&#xA;I primi 3 ottetti li ottengo dalla variabile globale NETWORK.&#xA;L&#39;ultimo ottetto, relativo all&#39;host, lo ricavo dalla lista degli ip&#xA;riutilizzabili (&#34;iprelease&#34;).&#xA;&#xA;Se non è vuota, prelevo il primo e lo rimuovo dalla lista. Se è vuota,&#xA;prelevo l&#39;ultimo ottetto da &#34;iprenew&#34; e lo incremento per il successivo&#xA;assegnamento.&#xA;iprenew() {&#xA;    # Selezioni i primi 3 ottetti del network&#xA;    SUBIP=$(echo ${NETWORK}|awk -F &#34;.&#34; &#39;{print $1&#34;.&#34;$2&#34;.&#34;$3}&#39;)&#xA;&#xA;    if [[ ! -e ${WORKDIR}/conf.d/iprelease || ! -s ${WORKDIR}/conf.d/iprelease ]]; then&#xA;        # Genero il nuovo ip (Incremento di 1 l&#39;ultimo ottetto)&#xA;        IP=$(cat ${WORKDIR}/conf.d/iprenew)&#xA;        echo $((++IP))   ${WORKDIR}/conf.d/iprenew&#xA;&#xA;        # Restituisco i 3 ottetti + il quarto&#xA;        #echo -n ${SUBIP};cat ${WORKDIR}/conf.d/iprenew&#xA;    else&#xA;        IP=$(head -n 1 ${WORKDIR}/conf.d/iprelease)&#xA;        sed -i &#34;1d&#34; ${WORKDIR}/conf.d/iprelease&#xA;    fi&#xA;    echo -n ${SUBIP}&#34;.&#34;${IP}&#xA;}&#xA;&#xA;Riallocazione di un indirizzo ip. Questa funzione viene invocata da&#xA;removeclient().&#xA;Aggiungo l&#39;ultimo ottetto alla lista degli ip riallocabili.&#xA;@par 1: ultimo ottetto.&#xA;iprelease() {&#xA;    echo $1|cut -d&#34;.&#34; -f 4     ${WORKDIR}/conf.d/iprelease&#xA;}&#xA;&#xA;Aggiunge il nuovo peer al file di configurazione del server&#xA;@par 1: CLIENTPUBLICKEY&#xA;@par 2: CLIENTPSK&#xA;@par 3: CLIENTIPADDRESS&#xA;@par 4: CLIENTNAME&#xA;addpeertoserver() {&#xA;    cat     ${WORKDIR}/conf.d/serverwg.conf &lt;&lt; EOF&#xA;$4&#xA;[Peer]&#xA;PublicKey = $1&#xA;PresharedKey = $2&#xA;AllowedIPs = $3/32&#xA;PersistentKeepalive = 25&#xA;&#xA;EOF&#xA;}&#xA;&#xA;Inizializza la configurazione del server wireguard.&#xA;&#xA;Questa operazione può essere fatta una sola volta a meno che non si&#xA;cancelli tutta la workdir.&#xA;&#xA;Verranno creati due file  di configurazione: serverwg.conf e &#xA;serverwgraw.conf&#xA;&#xA;Il primo conterrà la configurazione completa del server e dei peer,&#xA;verrà aggiornato ad ogni modifica dei client (aggiunta o rimozione) e&#xA;verrà usato per il deploy della configurazione.&#xA;&#xA;Il secondo conterrà la configurazione del solo server e verrà usato&#xA;solo per rigenerare il primo file di configurazione.&#xA;init() {&#xA;    # Controllo che wireguard sia almeno abilitato&#xA;    echo -n &#34;Inserire la password di Amministratore: &#34;; read -s PASSWORD&#xA;    sudo -k&#xA;    if echo $PASSWORD|sudo -S echo &#34;got a root&#34;   /dev/null; then&#xA;        if ! sudo systemctl is-enabled --quiet wg-quick@${WGINTERFACE}; then&#xA;            echo -en &#34;\nAttivazione servizio wireguard... &#34;&#xA;            #sudo systemctl enable --quiet wg-quick@${WGINTERFACE}&#xA;            echo &#34;fatto.&#34;&#xA;        fi&#xA;        sudo -k &#xA;        &#xA;        #Verifico che queste cartelle esistano. Se lo sono, non faccio nulla, se no le creo.&#xA;        echo -n &#34;Creazione workdir... &#34;&#xA;        [[ ! -e ${WORKDIR}/conf.d || ! -e ${WORKDIR}/psk  || ! -e ${WORKDIR}/keys ]] \&#xA;            &amp;&amp; { mkdir -p ${WORKDIR}/{conf.d,keys,psk}; } \&#xA;            || { echo &#34;inizializzazione già effettuata.&#34;; exit; }&#xA;        echo &#34;fatto.&#34;&#xA;        &#xA;        # Genero la chiave privata per il server&#xA;        echo -n &#34;Creazione chiave private del server...&#34;&#xA;        SERVERPRIVKEY=$(genprivkey &#34;serverwg&#34;)&#xA;        echo &#34;fatto.&#34;&#xA;&#xA;        # Genero il file di configurazione&#xA;        echo -n &#34;Inizializzazione del server... &#34;&#xA;        cat   ${WORKDIR}/conf.d/serverwg.conf &lt;&lt; EOF&#xA;[Interface]&#xA;Address = ${SERVERIPADDRESS}/${SUBNETMASK}&#xA;SaveConfig = true&#xA;&#xA;PreUp = ufw route allow in on ${WGINTERFACE} out on ${PHYSICALINTERFACE} log from ${NETWORK}/${SUBNETMASK} to 0.0.0.0/0&#xA;PostDown = ufw route delete allow in on ${WGINTERFACE} out on ${PHYSICALINTERFACE} log from ${NETWORK}/${SUBNETMASK} to 0.0.0.0/0&#xA;&#xA;IP masquerading&#xA;PreUp = iptables -t nat -A POSTROUTING -o ${PHYSICALINTERFACE} -s ${NETWORK}/${SUBNETMASK} -j MASQUERADE&#xA;PostDown = iptables -t nat -D POSTROUTING -o ${PHYSICALINTERFACE} -s ${NETWORK}/${SUBNETMASK} -j MASQUERADE&#xA;&#xA;IP filtering&#xA;PreUp = ufw allow in on ${PHYSICALINTERFACE} log to 0.0.0.0/0 app Wireguard&#xA;PostDown = ufw delete allow in on ${PHYSICALINTERFACE} log to 0.0.0.0/0 app Wireguard&#xA;&#xA;ListenPort = ${LISTENPORT}&#xA;PrivateKey = ${SERVERPRIVKEY}&#xA;&#xA;EOF&#xA;        echo &#34;fatto.&#34;&#xA;        cp ${WORKDIR}/conf.d/serverwg.conf ${WORKDIR}/conf.d/serverwgraw.conf&#xA;        # Inizializza l&#39;erogatore di ip&#xA;        echo 1   ${WORKDIR}/conf.d/iprenew&#xA;    else&#xA;        echo &#34;Sono necessarie le credenziali di amministrazione per eseguire questo comando.&#34;&#xA;        exit 1&#xA;    fi&#xA;}&#xA;&#xA;Genera un certificato per il client composto da:&#xA;chiave privata del client&#xA;chiave pre-condivisa&#xA;client ip&#xA;chiave pubblica del server&#xA;nome simbolico del client&#xA;&#xA;Dopo la creazione e il salvataggio in WORKDIR/conf.d, verrà aggiunto&#xA;il relativo peer nella configurazione del server (serverwg.conf).&#xA;@par1 @par2 @par3...: lista di nomi relativi ai certificati da creare.&#xA;addclient() {&#xA;    if isinit; then&#xA;        # Se non c&#39;è alcun input, esco.&#xA;        if [[ $# -lt 1 ]]; then&#xA;            echo -e &#34;Devi inserire almeno un nome.&#34;&#xA;            exit 1&#xA;        else&#xA;            # Per ogni nome presente in input, creo una configurazione &#xA;            # client e l&#39;aggiungo alla configurazione del server.&#xA;            for CLIENTNAME in &#34;$@&#34;; do&#xA;                echo -n &#34;Creazione certificato per il client \&#34;${CLIENTNAME}\&#34;... &#34;&#xA;                &#xA;                # Controllo se la configurazione esista già discriminando&#xA;                # in base al nome del file.&#xA;                if [[ -e ${WORKDIR}/conf.d/client${CLIENTNAME}.conf ]]; then&#xA;                    echo &#34;già esistente.&#34;&#xA;                else&#xA;                    # Creo la configurazione del client&#xA;                    CLIENTPRIVKEY=$(genprivkey ${CLIENTNAME})&#xA;                    CLIENTPUBLICKEY=$(echo ${CLIENTPRIVKEY} | wg pubkey)&#xA;                    CLIENTPSK=$(genclientpsk ${CLIENTNAME})&#xA;                    CLIENTIPADDRESS=$(iprenew)&#xA;                    SERVERPUBLICKEY=$(grep PrivateKey ${WORKDIR}/conf.d/serverwg.conf | cut -d &#34; &#34; -f 3 | tr -d &#34; &#34; | wg pubkey)&#xA;                    CLIENTNAMEUPPER=$(echo ${CLIENTNAME} | tr &#39;[:lower:]&#39; &#39;[:upper:]&#39;)&#xA;                    cat   ${WORKDIR}/conf.d/client${CLIENTNAME}.conf &lt;&lt; EOF&#xA;    # ${CLIENTNAMEUPPER}&#xA;    [Interface]&#xA;    Address = ${CLIENTIPADDRESS}/${SUBNETMASK}&#xA;    DNS = ${DNS}&#xA;    PrivateKey = ${CLIENTPRIVKEY}&#xA;&#xA;    [Peer]&#xA;    PublicKey = ${SERVERPUBLICKEY}&#xA;    PresharedKey = ${CLIENTPSK}&#xA;    AllowedIPs = 0.0.0.0/0&#xA;    Endpoint = ${ENDPOINT}&#xA;    PersistentKeepalive = 25&#xA;EOF&#xA;                    echo &#34;fatto.&#34;&#xA;                    echo -n &#34;Aggiunta peer \&#34;${CLIENTNAME}\&#34; al file di configurazione del server... &#34;&#xA;                    &#xA;                    # Aggiungo il peer alla configurazione del server.&#xA;                    addpeertoserver ${CLIENTPUBLICKEY} ${CLIENTPSK} ${CLIENTIPADDRESS} ${CLIENTNAMEUPPER}&#xA;                    echo -e &#34;fatto.\n&#34;                &#xA;                fi&#xA;            done&#xA;        fi&#xA;    else&#xA;        echo &#34;È necessario inizializzare il server con &#39;wginit.sh init&#39;.&#34;&#xA;        echo &#34;L&#39;operazione richiesta non sarà completata&#34;&#xA;    fi&#xA;}&#xA;&#xA;Rimuove file di configurazioni, chiavi e pre-shared key realtive a file&#xA;dati in input.&#xA;Rimuove inoltre il peer dal file di configurazione serverwg.conf.&#xA;@par1 @par2 @par3...: lista di nomi relativi ai certificati da rimuovere.&#xA;removeclient() {&#xA;    if isinit; then&#xA;        if [[ $# -lt 1 ]]; then&#xA;            echo -e &#34;Devi inserire almeno un nome.&#34;&#xA;            exit 1&#xA;        else&#xA;            for CLIENTNAME in &#34;$@&#34;; do&#xA;                echo -n &#34;Rimozione client \&#34;${CLIENTNAME}\&#34;... &#34;&#xA;                if [[ ! -e ${WORKDIR}/conf.d/client${CLIENTNAME}.conf ]]; then&#xA;                    echo &#34;non esistente.&#34;&#xA;                else&#xA;                    # Estraggo l&#39;ultimo ottetto dell&#39;ip del client.&#xA;                    IP=$(grep &#34;Address&#34; ${WORKDIR}/conf.d/client${CLIENTNAME}.conf|cut -d &#34; &#34; -f 3|tr -d &#34; &#34;|cut -d &#34;/&#34; -f 1)&#xA;                    &#xA;                    # Lo inserisco nella lista degli ip riallocabili.&#xA;                    iprelease ${IP}&#xA;                    &#xA;                    # Cancello file di configurazione, chiave e psk del client.&#xA;                    rm ${WORKDIR}/conf.d/client${CLIENTNAME}.conf ${WORKDIR}/keys/${CLIENTNAME}privkey ${WORKDIR}/psk/${CLIENTNAME}psk&#xA;                    echo &#34;terminata con successo.&#34;&#xA;                    echo -n &#34;Rimozione peer \&#34;${CLIENTNAME}\&#34; dal file di configurazione del server... &#34;&#xA;                    &#xA;                    # Rimuovo il peer dalla configurazione del server.&#xA;                    CLIENTNAMEUPPER=$(echo ${CLIENTNAME} | tr &#39;[:lower:]&#39; &#39;[:upper:]&#39;)&#xA;                    sed -i &#34;/# ${CLIENTNAMEUPPER}/,+6d&#34; ${WORKDIR}/conf.d/serverwg.conf&#xA;                    echo -e &#34;terminata con successo.\n&#34;   &#xA;                fi&#xA;            done&#xA;        fi&#xA;    else&#xA;        echo &#34;È necessario inizializzare il server con &#39;wginit.sh init&#39;.&#34;&#xA;        echo &#34;L&#39;operazione richiesta non sarà completata&#34;&#xA;    fi&#xA;}&#xA;&#xA;rebuild() {&#xA;    if isinit; then&#xA;        # Rigenero il file di configurazione del server da serverwgraw.conf.&#xA;        echo -n &#34;Ripristino del file di configurazione del server... &#34;&#xA;        cat ${WORKDIR}/conf.d/serverwgraw.conf   ${WORKDIR}/conf.d/serverwg.conf&#xA;        echo &#34;fatto.&#34;&#xA;        &#xA;        # Per ogni file di configurazione client, aggiungo il relativo peer&#xA;        # nel file di configurazione serverwg.conf.&#xA;        for FILECONF in ${WORKDIR}/conf.d/client.conf; do&#xA;            CLIENTPUBLICKEY=$(grep &#34;PrivateKey&#34; ${FILECONF}|cut -d &#34; &#34; -f 3|tr -d &#34; &#34; | wg pubkey)&#xA;            CLIENTPSK=$(grep &#34;PresharedKey&#34; ${FILECONF}|cut -d &#34; &#34; -f 3|tr -d &#34; &#34;)&#xA;            CLIENTIPADDRESS=$(grep &#34;Address&#34; ${FILECONF}|cut -d &#34; &#34; -f 3|tr -d &#34; &#34;|cut -d &#34;/&#34; -f 1)&#xA;            CLIENTNAMEUPPER=$(grep &#34;#&#34; ${FILECONF} | cut -d &#34; &#34; -f 2)&#xA;            echo -n &#34;Aggiunta peer \&#34;$(echo ${CLIENTNAMEUPPER} | tr &#39;[:upper:]&#39; &#39;[:lower:]&#39;)\&#34;... &#34;&#xA;            addpeertoserver ${CLIENTPUBLICKEY} ${CLIENTPSK} ${CLIENTIPADDRESS} ${CLIENTNAMEUPPER}&#xA;            echo -e &#34;fatto.&#34;&#xA;        done&#xA;    else&#xA;        echo &#34;È necessario inizializzare il server con &#39;wginit.sh init&#39;.&#34;&#xA;        echo &#34;L&#39;operazione richiesta non sarà completata&#34;&#xA;    fi&#xA;}&#xA;&#xA;Avvia il server wireguard con la nuova configurazione&#xA;deploy() {&#xA;    if isinit; then&#xA;        echo -n &#34;Inserire la password di Amministratore: &#34;; read -s PASSWORD&#xA;        sudo -k&#xA;        if echo $PASSWORD|sudo -S echo &#34;got a root&#34;   /dev/null; then&#xA;   &#xA;            # Stoppo il servizio wireguard&#xA;            echo -ne &#34;\nStop wireguard... &#34;&#xA;            sudo systemctl stop wg-quick@${WGINTERFACE}&#xA;            echo &#34;fatto.&#34;&#xA;            &#xA;            # Aggiorno la configurazione&#xA;            echo -n &#34;Copia della nuova configurazione... &#34;&#xA;            sudo cp ${WORKDIR}/conf.d/serverwg.conf /etc/wireguard/${WGINTERFACE}&#xA;            echo &#34;fatto.&#34;&#xA;            &#xA;            # Riavvio il servizio wireguard&#xA;            echo -n &#34;Riavvio wireguard... &#34;&#xA;            sudo systemctl start wg-quick@${WGINTERFACE}&#xA;            echo &#34;fatto.&#34;&#xA;            &#xA;            # Revoco i privilegi di amministrazione&#xA;            sudo -k&#xA;        else&#xA;            echo &#34;Sono necessarie le credenziali di amministrazione per eseguire questo comando.&#34;&#xA;            exit 1&#xA;        fi&#xA;    else&#xA;        echo &#34;È necessario inizializzare il server con &#39;wginit.sh init&#39;.&#34;&#xA;        echo &#34;L&#39;operazione richiesta non sarà completata&#34;&#xA;    fi&#xA;}&#xA;&#xA;Crea il qr-code della configurazione del client nella cartella corrente.&#xA;share() {&#xA;    if isinit; then&#xA;        if [[ $# -lt 1 ]]; then&#xA;            echo -e &#34;Devi inserire almeno un nome.&#34;&#xA;            exit 1&#xA;        else&#xA;            for CLIENTNAME in &#34;$@&#34;; do&#xA;                echo -n &#34;Creazione qr-code per il client \&#34;${CLIENTNAME}\&#34;... &#34;&#xA;                qrencode -r ${WORKDIR}/conf.d/client${CLIENTNAME}.conf -o qrcodeclient${CLIENTNAME}.jpg&#xA;                echo &#34;fatto.&#34;&#xA;            done&#xA;        fi&#xA;    else&#xA;        echo &#34;È necessario inizializzare il server con &#39;wginit.sh init&#39;.&#34;&#xA;        echo &#34;L&#39;operazione richiesta non sarà completata&#34;&#xA;    fi&#xA;}&#xA;&#xA;help() {&#xA;&#xA;[[ $1 != &#34;&#34; &amp;&amp; $1 != &#34;help&#34; ]] &amp;&amp; echo -e &#34;Comando inesistente.&#34;&#xA;&#xA;    cat&lt;&lt;EOF&#xA;Usa come: ./wgman.sh [command] ARG&#xA;dove:&#xA;    [command]&#xA;        init                     : Inizializza la configurazione del server.&#xA;        addclient lista nomi   : Aggiunge i client indicati in lista nomi.&#xA;        removeclient lista nomi: Rimuove i client indicati in lista nomi.&#xA;        rebuild                  : Ricostruisce il file di configurazione&#xA;                                   del server partendo dalle configurazioni&#xA;                                   di tutti i client registrati.&#xA;        deploy                   : Riavvia il server wireguard con l&#39;ultima&#xA;                                   configurazione disponibile.&#xA;        share lista nomi       : Genera i qr-code relativi ai client indicati in lista nomi.&#xA;        help                     : Stampa questa pagina di help.&#xA;&#xA;ESEMPI:&#xA;    INiZIALIZZA IL SERVER WIREGUARD&#xA;    wgman.sh init&#xA;&#xA;    AGGIUNGE I CLIENT &#39;macbookpro&#39;, &#39;mobile&#39;&#xA;    wgman.sh addclient macbookpro mobile&#xA;&#xA;    RIMUOVE I CLIENT &#39;iphoneluca&#39;, &#39;workstation&#39;&#xA;    wgman.sh removeclient iphoneluca workstation&#xA;&#xA;    RICOSTRUISCE LA CONFIGURAZIONE DEL SERVER WIREGUARD&#xA;    wgman.sh rebuild&#xA;    &#xA;    ATTIVA LA NUOVA CONFIGURAZIONE&#xA;    wgman.sh deploy&#xA;&#xA;    GENERA I QRCODE PER &#39;pcufficio&#39;, &#39;laptop&#39;&#xA;    wgman.sh share pcufficio laptop&#xA;EOF&#xA;}&#xA;&#xA;main() {&#xA;    case $1 in&#xA;        init         ) init ;;&#xA;        addclient    ) shift; addclient &#34;$@&#34; ;;&#xA;        removeclient ) shift; removeclient &#34;$@&#34; ;;&#xA;        rebuild      ) rebuild ;;&#xA;        deploy       ) deploy ;;&#xA;        share        ) shift; share &#34;$@&#34; ;;&#xA;        ) help ;;&#xA;    esac&#xA;}&#xA;&#xA;check_dependency&#xA;case $? in&#xA;    &#34;0&#34; ) main $* ;;&#xA;    &#34;1&#34; ) echo -e &#34;UFW non presente. Installare UFW.&#34;; exit 1 ;;&#xA;    &#34;2&#34; ) echo -e &#34;wireguard-tools non presenti. È necessario installarli.&#34;; exit 1 ;;&#xA;esac&#xA;&#xA;#bash #wireguard #psk #chiavi #certificati #crittografia&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/586f75268-5004eb/XBn3L6jB84Ka/b6zYDZWzIcLbax1taLgBnIQu8hoy7ZeZVlXBkGf0.png" alt="vpn"></p>

<p><a href="https://noblogo.org/aytin/vpn-con-wireguard-semplice-e-veloce" rel="nofollow">Cos&#39;è wireguard e come si configura</a>, lo sappiamo ma mi serviva qualcosa che mi permettesse di fare provisioning e deprovisioning dei certificati in maniera più semplice di quanto già si faccia con i tool disponibili nella userland, wg e wg-quick.</p>

<p>E questo è il motivo principale per cui è nato questo script.

Prende come riferimento una configurazione road warrior, <a href="https://noblogo.org/aytin/vpn-con-wireguard-semplice-e-veloce#primo-scenario" rel="nofollow">il primo scenario di una topologia point-to-site</a>, quella più comune.</p>

<h2 id="alcune-considerazioni">Alcune considerazioni</h2>
<ol><li><p>Lo script definisce una workdir dove deposita tutte le configurazioni e i certificati.
Le precondizioni sono legate alla presenza dei tool userland <strong>wg-tools</strong> (ovviamente) e di <strong>ufw</strong>. Vengono comunque verificate nello script.</p></li>

<li><p>Altra semplificazione è data dal fatto di allocare una /24 come rete wireguard. 254 host per una rete domestica sono più che sufficienti.</p></li>

<li><p>L&#39;allocazione degli ip viene fatta con un progressivo memorizzato nel file &#39;ip_renew&#39;. Gli ip dei certificati revocati vengono mantenuti in un file, &#39;ip_release&#39;.
Quando si crea un nuovo certificato, si controlla prima che ci sia un ip da recuperare in ip_release. Se la lista è vuota si ricorre al progressivo.</p></li>

<li><p>La creazione della configurazione lato server passa da due file di configurazione: una configurazione <strong>globale</strong> ottenuta tracciando l&#39;evoluzione iniziale in termini di aggiunta o rimozione dei client peer e destinata al rilascio in esecuzione. L&#39;altra contenente <strong>la sola configurazione del server</strong> e usata unicamente per la rigenerazione della configurazione globale a partire dai peer esistenti.</p></li>

<li><p>L&#39;operazione di <strong>init</strong> è propedeutica per <strong>addclient</strong>, <strong>removeclient</strong>, <strong>rebuild</strong>, <strong>deploy</strong>, <strong>share</strong>.</p></li></ol>

<p>Le operazioni a disposizione sono:</p>
<ul><li><strong>init:</strong> inizializza la configurazione di un server wireguard</li>
<li><strong>addclient:</strong> crea il certificato lato client e aggiunge il relativo peer sulla configurazione del server</li>
<li><strong>removeclient:</strong> cancella il certificato lato client e rimuove il relativo peer sulla configurazione del server</li>
<li><strong>rebuild:</strong> rigenera la configurazione del server usando i certificati client esistenti</li>
<li><strong>deploy:</strong> finalizza la configurazione riavviando il servizio con le nuove impostazioni</li>
<li><strong>share:</strong> crea i qr-code per i certificati client da utilizzare nelle configurazioni</li></ul>

<p>Per tutto il resto, i commenti nel codice sono abbastanza esplicativi.</p>

<h2 id="lo-script">Lo script</h2>

<pre><code class="language-bash">#!/bin/bash

# Questo è il file di configurazione globale, i dati sono fittizi.
# Può essere mantenuto nello script o nella home dell&#39;utente 
# (es. $HOME/.config/wg_man.conf) e importato 
# con &#39;source&#39; all&#39;inizio.
WG_INTERFACE=wg0
PHYSICAL_INTERFACE=eth0
NETWORK=&#34;192.168.15.0&#34;
SUBNET_MASK=&#34;24&#34;
SERVER_IP_ADDRESS=&#34;192.168.15.1&#34;
DNS=&#34;1.1.1.1&#34;
LISTEN_PORT=&#34;51820&#34;
ENDPOINT=wireguard.nodns.net:51820&#34;
WORKDIR=&#34;$HOME/wireguard&#34;



check_dependency() {
    if test -e /usr/sbin/ufw; then
        if test -e /usr/bin/wg; then
            return 0
        else
            return 2
        fi
    else
        return 1
    fi
}



is_init() {
    test -e ${WORKDIR}/conf.d/server_wg.conf &amp;&amp; return 0 || return 1
}



# Genera la chiave privata
gen_priv_key() {
    wg genkey | tee ${WORKDIR}/keys/$1_privkey
}



# Genera la pre-shared key
gen_client_psk() {
    # Evita il warning sui permessi del file che devono essere 600
    umask 077
    wg genpsk | tee ${WORKDIR}/psk/$1_psk
}



# Rilascio di un nuovo indirizzo ip. Questa funziona viene invocata da
# addclient().
#
# I primi 3 ottetti li ottengo dalla variabile globale NETWORK.
# L&#39;ultimo ottetto, relativo all&#39;host, lo ricavo dalla lista degli ip
# riutilizzabili (&#34;ip_release&#34;).
#
# Se non è vuota, prelevo il primo e lo rimuovo dalla lista. Se è vuota,
# prelevo l&#39;ultimo ottetto da &#34;ip_renew&#34; e lo incremento per il successivo
# assegnamento.
ip_renew() {
    # Selezioni i primi 3 ottetti del network
    SUB_IP=$(echo ${NETWORK}|awk -F &#34;.&#34; &#39;{print $1&#34;.&#34;$2&#34;.&#34;$3}&#39;)

    if [[ ! -e ${WORKDIR}/conf.d/ip_release || ! -s ${WORKDIR}/conf.d/ip_release ]]; then
        # Genero il nuovo ip (Incremento di 1 l&#39;ultimo ottetto)
        IP=$(cat ${WORKDIR}/conf.d/ip_renew)
        echo $((++IP)) &gt; ${WORKDIR}/conf.d/ip_renew

        # Restituisco i 3 ottetti + il quarto
        #echo -n ${SUB_IP};cat ${WORKDIR}/conf.d/ip_renew
    else
        IP=$(head -n 1 ${WORKDIR}/conf.d/ip_release)
        sed -i &#34;1d&#34; ${WORKDIR}/conf.d/ip_release
    fi
    echo -n ${SUB_IP}&#34;.&#34;${IP}
}



# Riallocazione di un indirizzo ip. Questa funzione viene invocata da
# removeclient().
# Aggiungo l&#39;ultimo ottetto alla lista degli ip riallocabili.
# @par 1: ultimo ottetto.
ip_release() {
    echo $1|cut -d&#34;.&#34; -f 4 &gt;&gt; ${WORKDIR}/conf.d/ip_release
}



# Aggiunge il nuovo peer al file di configurazione del server
# @par 1: CLIENT_PUBLIC_KEY
# @par 2: CLIENT_PSK
# @par 3: CLIENT_IP_ADDRESS
# @par 4: CLIENT_NAME
add_peer_to_server() {
    cat &gt;&gt; ${WORKDIR}/conf.d/server_wg.conf &lt;&lt; EOF
# $4
[Peer]
PublicKey = $1
PresharedKey = $2
AllowedIPs = $3/32
PersistentKeepalive = 25

EOF
}



# Inizializza la configurazione del server wireguard.
#
# Questa operazione può essere fatta una sola volta a meno che non si
# cancelli tutta la workdir.
#
# Verranno creati due file  di configurazione: server_wg.conf e 
# server_wg_raw.conf
#
# Il primo conterrà la configurazione completa del server e dei peer,
# verrà aggiornato ad ogni modifica dei client (aggiunta o rimozione) e
# verrà usato per il deploy della configurazione.
#
# Il secondo conterrà la configurazione del solo server e verrà usato
# solo per rigenerare il primo file di configurazione.
init() {
    # Controllo che wireguard sia almeno abilitato
    echo -n &#34;Inserire la password di Amministratore: &#34;; read -s PASSWORD
    sudo -k
    if echo $PASSWORD|sudo -S echo &#34;got a root&#34; &gt; /dev/null; then
        if ! sudo systemctl is-enabled --quiet wg-quick@${WG_INTERFACE}; then
            echo -en &#34;\nAttivazione servizio wireguard... &#34;
            #sudo systemctl enable --quiet wg-quick@${WG_INTERFACE}
            echo &#34;fatto.&#34;
        fi
        sudo -k 
        
        #Verifico che queste cartelle esistano. Se lo sono, non faccio nulla, se no le creo.
        echo -n &#34;Creazione workdir... &#34;
        [[ ! -e ${WORKDIR}/conf.d || ! -e ${WORKDIR}/psk  || ! -e ${WORKDIR}/keys ]] \
            &amp;&amp; { mkdir -p ${WORKDIR}/{conf.d,keys,psk}; } \
            || { echo &#34;inizializzazione già effettuata.&#34;; exit; }
        echo &#34;fatto.&#34;
        
        # Genero la chiave privata per il server
        echo -n &#34;Creazione chiave private del server...&#34;
        SERVER_PRIV_KEY=$(gen_priv_key &#34;server_wg&#34;)
        echo &#34;fatto.&#34;

        # Genero il file di configurazione
        echo -n &#34;Inizializzazione del server... &#34;
        cat &gt; ${WORKDIR}/conf.d/server_wg.conf &lt;&lt; EOF
[Interface]
Address = ${SERVER_IP_ADDRESS}/${SUBNET_MASK}
# SaveConfig = true

PreUp = ufw route allow in on ${WG_INTERFACE} out on ${PHYSICAL_INTERFACE} log from ${NETWORK}/${SUBNET_MASK} to 0.0.0.0/0
PostDown = ufw route delete allow in on ${WG_INTERFACE} out on ${PHYSICAL_INTERFACE} log from ${NETWORK}/${SUBNET_MASK} to 0.0.0.0/0

# IP masquerading
PreUp = iptables -t nat -A POSTROUTING -o ${PHYSICAL_INTERFACE} -s ${NETWORK}/${SUBNET_MASK} -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o ${PHYSICAL_INTERFACE} -s ${NETWORK}/${SUBNET_MASK} -j MASQUERADE

# IP filtering
PreUp = ufw allow in on ${PHYSICAL_INTERFACE} log to 0.0.0.0/0 app Wireguard
PostDown = ufw delete allow in on ${PHYSICAL_INTERFACE} log to 0.0.0.0/0 app Wireguard

ListenPort = ${LISTEN_PORT}
PrivateKey = ${SERVER_PRIV_KEY}

EOF
        echo &#34;fatto.&#34;
        cp ${WORKDIR}/conf.d/server_wg.conf ${WORKDIR}/conf.d/server_wg_raw.conf
        # Inizializza l&#39;erogatore di ip
        echo 1 &gt; ${WORKDIR}/conf.d/ip_renew
    else
        echo &#34;Sono necessarie le credenziali di amministrazione per eseguire questo comando.&#34;
        exit 1
    fi
}



# Genera un certificato per il client composto da:
# * chiave privata del client
# * chiave pre-condivisa
# * client ip
# * chiave pubblica del server
# * nome simbolico del client
#
# Dopo la creazione e il salvataggio in &lt;WORKDIR&gt;/conf.d, verrà aggiunto
# il relativo peer nella configurazione del server (server_wg.conf).
# @par_1 @par_2 @par_3...: lista di nomi relativi ai certificati da creare.
add_client() {
    if is_init; then
        # Se non c&#39;è alcun input, esco.
        if [[ $# -lt 1 ]]; then
            echo -e &#34;Devi inserire almeno un nome.&#34;
            exit 1
        else
            # Per ogni nome presente in input, creo una configurazione 
            # client e l&#39;aggiungo alla configurazione del server.
            for CLIENT_NAME in &#34;$@&#34;; do
                echo -n &#34;Creazione certificato per il client \&#34;${CLIENT_NAME}\&#34;... &#34;
                
                # Controllo se la configurazione esista già discriminando
                # in base al nome del file.
                if [[ -e ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf ]]; then
                    echo &#34;già esistente.&#34;
                else
                    # Creo la configurazione del client
                    CLIENT_PRIV_KEY=$(gen_priv_key ${CLIENT_NAME})
                    CLIENT_PUBLIC_KEY=$(echo ${CLIENT_PRIV_KEY} | wg pubkey)
                    CLIENT_PSK=$(gen_client_psk ${CLIENT_NAME})
                    CLIENT_IP_ADDRESS=$(ip_renew)
                    SERVER_PUBLIC_KEY=$(grep PrivateKey ${WORKDIR}/conf.d/server_wg.conf | cut -d &#34; &#34; -f 3 | tr -d &#34; &#34; | wg pubkey)
                    CLIENT_NAME_UPPER=$(echo ${CLIENT_NAME} | tr &#39;[:lower:]&#39; &#39;[:upper:]&#39;)
                    cat &gt; ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf &lt;&lt; EOF
    # ${CLIENT_NAME_UPPER}
    [Interface]
    Address = ${CLIENT_IP_ADDRESS}/${SUBNET_MASK}
    DNS = ${DNS}
    PrivateKey = ${CLIENT_PRIV_KEY}

    [Peer]
    PublicKey = ${SERVER_PUBLIC_KEY}
    PresharedKey = ${CLIENT_PSK}
    AllowedIPs = 0.0.0.0/0
    Endpoint = ${ENDPOINT}
    PersistentKeepalive = 25
EOF
                    echo &#34;fatto.&#34;
                    echo -n &#34;Aggiunta peer \&#34;${CLIENT_NAME}\&#34; al file di configurazione del server... &#34;
                    
                    # Aggiungo il peer alla configurazione del server.
                    add_peer_to_server ${CLIENT_PUBLIC_KEY} ${CLIENT_PSK} ${CLIENT_IP_ADDRESS} ${CLIENT_NAME_UPPER}
                    echo -e &#34;fatto.\n&#34;                
                fi
            done
        fi
    else
        echo &#34;È necessario inizializzare il server con &#39;wg_init.sh init&#39;.&#34;
        echo &#34;L&#39;operazione richiesta non sarà completata&#34;
    fi
}



# Rimuove file di configurazioni, chiavi e pre-shared key realtive a file
# dati in input.
# Rimuove inoltre il peer dal file di configurazione server_wg.conf.
# @par_1 @par_2 @par_3...: lista di nomi relativi ai certificati da rimuovere.
remove_client() {
    if is_init; then
        if [[ $# -lt 1 ]]; then
            echo -e &#34;Devi inserire almeno un nome.&#34;
            exit 1
        else
            for CLIENT_NAME in &#34;$@&#34;; do
                echo -n &#34;Rimozione client \&#34;${CLIENT_NAME}\&#34;... &#34;
                if [[ ! -e ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf ]]; then
                    echo &#34;non esistente.&#34;
                else
                    # Estraggo l&#39;ultimo ottetto dell&#39;ip del client.
                    IP=$(grep &#34;Address&#34; ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf|cut -d &#34; &#34; -f 3|tr -d &#34; &#34;|cut -d &#34;/&#34; -f 1)
                    
                    # Lo inserisco nella lista degli ip riallocabili.
                    ip_release ${IP}
                    
                    # Cancello file di configurazione, chiave e psk del client.
                    rm ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf ${WORKDIR}/keys/${CLIENT_NAME}_privkey ${WORKDIR}/psk/${CLIENT_NAME}_psk
                    echo &#34;terminata con successo.&#34;
                    echo -n &#34;Rimozione peer \&#34;${CLIENT_NAME}\&#34; dal file di configurazione del server... &#34;
                    
                    # Rimuovo il peer dalla configurazione del server.
                    CLIENT_NAME_UPPER=$(echo ${CLIENT_NAME} | tr &#39;[:lower:]&#39; &#39;[:upper:]&#39;)
                    sed -i &#34;/# ${CLIENT_NAME_UPPER}/,+6d&#34; ${WORKDIR}/conf.d/server_wg.conf
                    echo -e &#34;terminata con successo.\n&#34;   
                fi
            done
        fi
    else
        echo &#34;È necessario inizializzare il server con &#39;wg_init.sh init&#39;.&#34;
        echo &#34;L&#39;operazione richiesta non sarà completata&#34;
    fi
}



rebuild() {
    if is_init; then
        # Rigenero il file di configurazione del server da server_wg_raw.conf.
        echo -n &#34;Ripristino del file di configurazione del server... &#34;
        cat ${WORKDIR}/conf.d/server_wg_raw.conf &gt; ${WORKDIR}/conf.d/server_wg.conf
        echo &#34;fatto.&#34;
        
        # Per ogni file di configurazione client, aggiungo il relativo peer
        # nel file di configurazione server_wg.conf.
        for FILE_CONF in ${WORKDIR}/conf.d/client_*.conf; do
            CLIENT_PUBLIC_KEY=$(grep &#34;PrivateKey&#34; ${FILE_CONF}|cut -d &#34; &#34; -f 3|tr -d &#34; &#34; | wg pubkey)
            CLIENT_PSK=$(grep &#34;PresharedKey&#34; ${FILE_CONF}|cut -d &#34; &#34; -f 3|tr -d &#34; &#34;)
            CLIENT_IP_ADDRESS=$(grep &#34;Address&#34; ${FILE_CONF}|cut -d &#34; &#34; -f 3|tr -d &#34; &#34;|cut -d &#34;/&#34; -f 1)
            CLIENT_NAME_UPPER=$(grep &#34;#&#34; ${FILE_CONF} | cut -d &#34; &#34; -f 2)
            echo -n &#34;Aggiunta peer \&#34;$(echo ${CLIENT_NAME_UPPER} | tr &#39;[:upper:]&#39; &#39;[:lower:]&#39;)\&#34;... &#34;
            add_peer_to_server ${CLIENT_PUBLIC_KEY} ${CLIENT_PSK} ${CLIENT_IP_ADDRESS} ${CLIENT_NAME_UPPER}
            echo -e &#34;fatto.&#34;
        done
    else
        echo &#34;È necessario inizializzare il server con &#39;wg_init.sh init&#39;.&#34;
        echo &#34;L&#39;operazione richiesta non sarà completata&#34;
    fi
}



# Avvia il server wireguard con la nuova configurazione
deploy() {
    if is_init; then
        echo -n &#34;Inserire la password di Amministratore: &#34;; read -s PASSWORD
        sudo -k
        if echo $PASSWORD|sudo -S echo &#34;got a root&#34; &gt; /dev/null; then
   
            # Stoppo il servizio wireguard
            echo -ne &#34;\nStop wireguard... &#34;
            sudo systemctl stop wg-quick@${WG_INTERFACE}
            echo &#34;fatto.&#34;
            
            # Aggiorno la configurazione
            echo -n &#34;Copia della nuova configurazione... &#34;
            sudo cp ${WORKDIR}/conf.d/server_wg.conf /etc/wireguard/${WG_INTERFACE}
            echo &#34;fatto.&#34;
            
            # Riavvio il servizio wireguard
            echo -n &#34;Riavvio wireguard... &#34;
            sudo systemctl start wg-quick@${WG_INTERFACE}
            echo &#34;fatto.&#34;
            
            # Revoco i privilegi di amministrazione
            sudo -k
        else
            echo &#34;Sono necessarie le credenziali di amministrazione per eseguire questo comando.&#34;
            exit 1
        fi
    else
        echo &#34;È necessario inizializzare il server con &#39;wg_init.sh init&#39;.&#34;
        echo &#34;L&#39;operazione richiesta non sarà completata&#34;
    fi
}



# Crea il qr-code della configurazione del client nella cartella corrente.
share() {
    if is_init; then
        if [[ $# -lt 1 ]]; then
            echo -e &#34;Devi inserire almeno un nome.&#34;
            exit 1
        else
            for CLIENT_NAME in &#34;$@&#34;; do
                echo -n &#34;Creazione qr-code per il client \&#34;${CLIENT_NAME}\&#34;... &#34;
                qrencode -r ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf -o qrcode_client_${CLIENT_NAME}.jpg
                echo &#34;fatto.&#34;
            done
        fi
    else
        echo &#34;È necessario inizializzare il server con &#39;wg_init.sh init&#39;.&#34;
        echo &#34;L&#39;operazione richiesta non sarà completata&#34;
    fi
}



help() {

[[ $1 != &#34;&#34; &amp;&amp; $1 != &#34;help&#34; ]] &amp;&amp; echo -e &#34;Comando inesistente.&#34;

    cat&lt;&lt;EOF
Usa come: ./wg_man.sh [command] ARG
dove:
    [command]
        init                     : Inizializza la configurazione del server.
        addclient &lt;lista nomi&gt;   : Aggiunge i client indicati in &lt;lista nomi&gt;.
        removeclient &lt;lista nomi&gt;: Rimuove i client indicati in &lt;lista nomi&gt;.
        rebuild                  : Ricostruisce il file di configurazione
                                   del server partendo dalle configurazioni
                                   di tutti i client registrati.
        deploy                   : Riavvia il server wireguard con l&#39;ultima
                                   configurazione disponibile.
        share &lt;lista nomi&gt;       : Genera i qr-code relativi ai client indicati in &lt;lista nomi&gt;.
        help                     : Stampa questa pagina di help.

ESEMPI:
    INiZIALIZZA IL SERVER WIREGUARD
    wg_man.sh init

    AGGIUNGE I CLIENT &#39;macbook_pro&#39;, &#39;mobile&#39;
    wg_man.sh addclient macbook_pro mobile

    RIMUOVE I CLIENT &#39;iphone_luca&#39;, &#39;workstation&#39;
    wg_man.sh removeclient iphone_luca workstation

    RICOSTRUISCE LA CONFIGURAZIONE DEL SERVER WIREGUARD
    wg_man.sh rebuild
    
    ATTIVA LA NUOVA CONFIGURAZIONE
    wg_man.sh deploy

    GENERA I QRCODE PER &#39;pc_ufficio&#39;, &#39;laptop&#39;
    wg_man.sh share pc_ufficio laptop
EOF
}



main() {
    case $1 in
        init         ) init ;;
        addclient    ) shift; add_client &#34;$@&#34; ;;
        removeclient ) shift; remove_client &#34;$@&#34; ;;
        rebuild      ) rebuild ;;
        deploy       ) deploy ;;
        share        ) shift; share &#34;$@&#34; ;;
        *            ) help ;;
    esac
}



check_dependency
case $? in
    &#34;0&#34; ) main $* ;;
    &#34;1&#34; ) echo -e &#34;UFW non presente. Installare UFW.&#34;; exit 1 ;;
    &#34;2&#34; ) echo -e &#34;wireguard-tools non presenti. È necessario installarli.&#34;; exit 1 ;;
esac
</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:wireguard" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">wireguard</span></a> <a href="/aytin/tag:psk" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">psk</span></a> <a href="/aytin/tag:chiavi" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">chiavi</span></a> <a href="/aytin/tag:certificati" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">certificati</span></a> <a href="/aytin/tag:crittografia" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">crittografia</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/script-per-la-gestione-della-configurazione-di-una-vpn-wireguard</guid>
      <pubDate>Sun, 22 Sep 2024 13:27:09 +0000</pubDate>
    </item>
    <item>
      <title>Aggiornare LibreOffice su Fedora</title>
      <link>https://noblogo.org/aytin/aggiornare-libreoffice-su-fedora</link>
      <description>&lt;![CDATA[office&#xA;&#xA;Piccolo script scritto di fretta per avere sotto mano la versione fresh di Libreoffice su Fedora invece che quella pacchettizzata. Un giorno ci ritornerò come ho fatto con &#34;Gestione TOTP in bash&#34;&#xA;!--more--&#xA;&#xA;!/bin/bash&#xA;download.documentfoundation.org&#xA;&#xA;usage() {&#xA;cat&lt;&lt;EOF&#xA;Usa come: ./libreoffice-update.sh [options]&#xA;dove:&#xA;    [options]&#xA;        -u verifica l esistenza di una nuova versione&#xA;        -i numeroversion installa la nuova versione di libreoffice&#xA;        -h stampa questa pagina di help&#xA;&#xA;ESEMPI:&#xA;    VERIFICA CHE UN AGGIORNAMENTO SIA DISPONIBILE&#xA;    libreoffice-update -u&#xA;&#xA;    INSTALLA L ULTIMA VERSIONE DI LIBREOFFICE&#xA;    libreoffice-update -i&#xA;&#xA;    VISUALIZZA l HELP&#xA;    libreoffice-update -h&#xA;EOF&#xA;}&#xA;&#xA;downloadlosuite() {&#xA;    echo -n &#34;Inizio download LibreOffice $VERSION... &#34;&#xA;    [[ $(aria2c https://download.documentfoundation.org/libreoffice/stable/&#34;$VERSION&#34;/rpm/x8664/LibreOffice&#34;$VERSION&#34;Linuxx86-64rpm.tar.gz | grep ERROR) ]] \&#xA;        &amp;&amp; { echo &#34; Impossibile scaricare LibreOffice $VERSION &#34;; ERRORCODE=64; return ${ERRORCODE}; } \&#xA;        || echo &#34;Fatto.&#34;&#xA;&#xA;    echo -n &#34;Inizio download LibreOfficeLangack $VERSION... &#34;&#xA;    [[ $(aria2c https://download.documentfoundation.org/libreoffice/stable/&#34;$VERSION&#34;/rpm/x8664/LibreOffice&#34;$VERSION&#34;Linuxx86-64rpmlangpackit.tar.gz | grep ERROR) ]] \&#xA;        &amp;&amp; { echo &#34; Impossibile scaricare LibreOfficelangPack $VERSION &#34;; ERRORCODE=65; return ${ERRORCODE}; } \&#xA;        || echo &#34;Fatto.&#34;&#xA;&#xA;    echo -n &#34;Inizio download LibreOfficeHelppack $VERSION... &#34;&#xA;    [[ $(aria2c https://download.documentfoundation.org/libreoffice/stable/&#34;$VERSION&#34;/rpm/x8664/LibreOffice&#34;$VERSION&#34;Linuxx86-64rpmhelppackit.tar.gz | grep ERROR) ]] \&#xA;        &amp;&amp; { echo &#34; Impossibile scaricare LibreOfficeHelppack $VERSION &#34;; ERRORCODE=66; return ${ERRORCODE}; } \&#xA;        || echo -e &#34;Fatto.\n&#34;&#xA;}&#xA;&#xA;decomprimilosuite() {&#xA;    echo -n &#34;Decompressione tar LibreOffice $VERSION in corso... &#34;&#xA;    [[ $(tar -xzf LibreOffice&#34;$VERSION&#34;Linuxx86-64rpm.tar.gz 2  /dev/null) ]] \&#xA;        &amp;&amp; { echo &#34; Impossibile decomprimere il tar di LibreOffice $VERSION &#34;; ERRORCODE=67; return ${ERRORCODE}; } \&#xA;        || echo &#34;Fatto.&#34;&#xA;&#xA;    echo -n &#34;Decompressione tar LibreOfficeLangpack $VERSION in corso... &#34;&#xA;    [[ $(tar -xzf LibreOffice&#34;$VERSION&#34;Linuxx86-64rpmlangpackit.tar.gz  2  /dev/null) ]] \&#xA;        &amp;&amp; { echo &#34; Impossibile decomprimere il tar di LibreOfficeLangpack $VERSION &#34;; ERRORCODE=68; return ${ERRORCODE}; } \&#xA;        || echo &#34;Fatto.&#34;&#xA;&#xA;    echo -n &#34;Decompressione tar LibreOfficeHelppack $VERSION in corso... &#34;&#xA;    [[ $(tar -xzf LibreOffice&#34;$VERSION&#34;Linuxx86-64rpmhelppackit.tar.gz 2  /dev/null) ]] \&#xA;        &amp;&amp; { echo &#34; Impossibile decomprimere il tar di LibreOfficeHelppack $VERSION &#34;; ERRORCODE=69; return ${ERRORCODE}; } \&#xA;        || echo -e &#34;Fatto.\n&#34;&#xA;        &#xA;    [[ $? -gt 0 ]] &amp;&amp; return $?&#xA;}&#xA;&#xA;installlosuite() {&#xA;    cd $(tar -tf LibreOffice&#34;$VERSION&#34;Linuxx86-64rpm.tar.gz|head -1) &amp;&amp; sudo dnf -y localinstall RPMS/ &amp;&amp; cd ..&#xA;    cd $(tar -tf LibreOffice&#34;$VERSION&#34;Linuxx86-64rpmlangpackit.tar.gz|head -1) &amp;&amp; sudo dnf -y localinstall RPMS/ &amp;&amp; cd ..&#xA;    cd $(tar -tf LibreOffice&#34;$VERSION&#34;Linuxx86-64rpmhelppackit.tar.gz|head -1) &amp;&amp; sudo dnf -y localinstall RPMS/ &amp;&amp; cd $HOME&#xA;}&#xA;&#xA;isloupgradeable() {&#xA;    VERSIONEINSTALLATA=$(dnf info installed|grep -A 2 libreoffice|grep Version|head -n 1|cut -d &#34;:&#34; -f 2|cut -c 2-7)&#xA;    VERSIONEONLINE=$(curl https://it.libreoffice.org/download/download/ 2  /dev/null|grep &#34;version=&#34;|head -n 1|cut -d &#34;&amp;&#34; -f 2|cut -d &#34;=&#34; -f 2)&#xA;    echo &#34;VERSIONE INSTALLATA: $VERSIONEINSTALLATA&#34;&#xA;    echo &#34;VERSIONE ONLINE:     $VERSIONEONLINE&#34;&#xA;    [[ ${VERSIONEINSTALLATA} == ${VERSIONEONLINE} ]] \&#xA;        &amp;&amp; echo -e &#34;Nessun aggiornamento disponibile.&#34; \&#xA;        || echo -e &#34;Una nuova versione è disponibile.\n&#34;&#xA;}&#xA;&#xA;cleanup() {&#xA;    rm -r /tmp/tmp.&#xA;}&#xA;&#xA;removeoldlosuite() {&#xA;    sudo dnf -y remove libreoffice&#xA;}&#xA;&#xA;install() {&#xA;    VERSION=$1&#xA;    cd $(mktemp -d)&#xA;    ! downloadlosuite &amp;&amp; { cleanup; exit ${ERRORCODE}; }&#xA;    ! decomprimilosuite &amp;&amp; { cleanup; exit ${ERRORCODE}; }&#xA;    ! removeoldlosuite &amp;&amp; { cleanup; exit ${ERRORCODE}; }&#xA;    installlosuite &amp;&amp; cleanup || exit ${ERRORCODE}?&#xA;}&#xA;&#xA;noparam() {&#xA;    clear;&#xA;    usage&#xA;    exit 0&#xA;}&#xA;&#xA;main() {&#xA;    while getopts &#34;ui:h&#34; opt; do&#xA;        case $opt in&#xA;            u ) islo_upgradeable ;;&#xA;            i ) install $OPTARG ;;&#xA;            h ) clear;usage; exit;;&#xA;            ) clear;echo -e &#34;[ERRORE] Opzione non valida.&#34;;usage; exit 1;;&#xA;        esac&#xA;    done&#xA;    shift $(($OPTIND - 1))&#xA;}&#xA;&#xA;[[ $# -eq 0 ]] &amp;&amp; noparam || main $&#xA;&#xA;#bash #libreoffice #scripting]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/7c649620b-69b6c5/KrPmYlEt1gcy/imPYx0fikTBe14R3C5cAH21rMs6qSEoBGeTujUdT.webp" alt="office"></p>

<p>Piccolo script scritto di fretta per avere sotto mano la versione fresh di Libreoffice su Fedora invece che quella pacchettizzata. Un giorno ci ritornerò come ho fatto con “<a href="https://noblogo.org/aytin/gestione-totp-in-bash" rel="nofollow">Gestione TOTP in bash</a>“
</p>

<pre><code class="language-bash">#!/bin/bash
#download.documentfoundation.org

usage() {
cat&lt;&lt;EOF
Usa come: ./libreoffice-update.sh [options]
dove:
    [options]
        -u verifica l esistenza di una nuova versione
        -i &lt;numero_version&gt; installa la nuova versione di libreoffice
        -h stampa questa pagina di help

ESEMPI:
    VERIFICA CHE UN AGGIORNAMENTO SIA DISPONIBILE
    libreoffice-update -u

    INSTALLA L ULTIMA VERSIONE DI LIBREOFFICE
    libreoffice-update -i

    VISUALIZZA l HELP
    libreoffice-update -h
EOF
}

download_lo_suite() {
    echo -n &#34;Inizio download LibreOffice $VERSION... &#34;
    [[ $(aria2c https://download.documentfoundation.org/libreoffice/stable/&#34;$VERSION&#34;/rpm/x86_64/LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm.tar.gz | grep ERROR) ]] \
        &amp;&amp; { echo &#34;*** Impossibile scaricare LibreOffice $VERSION ***&#34;; ERROR_CODE=64; return ${ERROR_CODE}; } \
        || echo &#34;Fatto.&#34;

    echo -n &#34;Inizio download LibreOfficeLangack $VERSION... &#34;
    [[ $(aria2c https://download.documentfoundation.org/libreoffice/stable/&#34;$VERSION&#34;/rpm/x86_64/LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm_langpack_it.tar.gz | grep ERROR) ]] \
        &amp;&amp; { echo &#34;*** Impossibile scaricare LibreOfficelangPack $VERSION ***&#34;; ERROR_CODE=65; return ${ERROR_CODE}; } \
        || echo &#34;Fatto.&#34;

    echo -n &#34;Inizio download LibreOfficeHelppack $VERSION... &#34;
    [[ $(aria2c https://download.documentfoundation.org/libreoffice/stable/&#34;$VERSION&#34;/rpm/x86_64/LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm_helppack_it.tar.gz | grep ERROR) ]] \
        &amp;&amp; { echo &#34;*** Impossibile scaricare LibreOfficeHelppack $VERSION ***&#34;; ERROR_CODE=66; return ${ERROR_CODE}; } \
        || echo -e &#34;Fatto.\n&#34;
}


decomprimi_lo_suite() {
    echo -n &#34;Decompressione tar LibreOffice $VERSION in corso... &#34;
    [[ $(tar -xzf LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm.tar.gz 2&gt;/dev/null) ]] \
        &amp;&amp; { echo &#34;*** Impossibile decomprimere il tar di LibreOffice $VERSION ***&#34;; ERROR_CODE=67; return ${ERROR_CODE}; } \
        || echo &#34;Fatto.&#34;

    echo -n &#34;Decompressione tar LibreOfficeLangpack $VERSION in corso... &#34;
    [[ $(tar -xzf LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm_langpack_it.tar.gz  2&gt;/dev/null) ]] \
        &amp;&amp; { echo &#34;*** Impossibile decomprimere il tar di LibreOfficeLangpack $VERSION ***&#34;; ERROR_CODE=68; return ${ERROR_CODE}; } \
        || echo &#34;Fatto.&#34;

    echo -n &#34;Decompressione tar LibreOfficeHelppack $VERSION in corso... &#34;
    [[ $(tar -xzf LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm_helppack_it.tar.gz 2&gt;/dev/null) ]] \
        &amp;&amp; { echo &#34;*** Impossibile decomprimere il tar di LibreOfficeHelppack $VERSION ***&#34;; ERROR_CODE=69; return ${ERROR_CODE}; } \
        || echo -e &#34;Fatto.\n&#34;
        
    [[ $? -gt 0 ]] &amp;&amp; return $?
}

install_lo_suite() {
    cd $(tar -tf LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm.tar.gz|head -1) &amp;&amp; sudo dnf -y localinstall RPMS/* &amp;&amp; cd ..
    cd $(tar -tf LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm_langpack_it.tar.gz|head -1) &amp;&amp; sudo dnf -y localinstall RPMS/* &amp;&amp; cd ..
    cd $(tar -tf LibreOffice_&#34;$VERSION&#34;_Linux_x86-64_rpm_helppack_it.tar.gz|head -1) &amp;&amp; sudo dnf -y localinstall RPMS/* &amp;&amp; cd $HOME
}

is_lo_upgradeable() {
    VERSIONE_INSTALLATA=$(dnf info installed|grep -A 2 libreoffice|grep Version|head -n 1|cut -d &#34;:&#34; -f 2|cut -c 2-7)
    VERSIONE_ONLINE=$(curl https://it.libreoffice.org/download/download/ 2&gt;/dev/null|grep &#34;version=&#34;|head -n 1|cut -d &#34;&amp;&#34; -f 2|cut -d &#34;=&#34; -f 2)
    echo &#34;VERSIONE INSTALLATA: $VERSIONE_INSTALLATA&#34;
    echo &#34;VERSIONE ONLINE:     $VERSIONE_ONLINE&#34;
    [[ ${VERSIONE_INSTALLATA} == ${VERSIONE_ONLINE} ]] \
        &amp;&amp; echo -e &#34;Nessun aggiornamento disponibile.&#34; \
        || echo -e &#34;Una nuova versione è disponibile.\n&#34;
}

cleanup() {
    rm -r /tmp/tmp.*
}

remove_old_lo_suite() {
    sudo dnf -y remove libreoffice*
}

install() {
    VERSION=$1
    cd $(mktemp -d)
    ! download_lo_suite &amp;&amp; { cleanup; exit ${ERROR_CODE}; }
    ! decomprimi_lo_suite &amp;&amp; { cleanup; exit ${ERROR_CODE}; }
    ! remove_old_lo_suite &amp;&amp; { cleanup; exit ${ERROR_CODE}; }
    install_lo_suite &amp;&amp; cleanup || exit ${ERROR_CODE}?
}

noparam() {
    clear;
    usage
    exit 0
}

main() {
    while getopts &#34;ui:h&#34; opt; do
        case $opt in
            u ) is_lo_upgradeable ;;
            i ) install $OPTARG ;;
            h ) clear;usage; exit;;
            * ) clear;echo -e &#34;[ERRORE] Opzione non valida.&#34;;usage; exit 1;;
        esac
    done
    shift $(($OPTIND - 1))
}

[[ $# -eq 0 ]] &amp;&amp; noparam || main $*
</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:libreoffice" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">libreoffice</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/aggiornare-libreoffice-su-fedora</guid>
      <pubDate>Fri, 23 Feb 2024 14:55:02 +0000</pubDate>
    </item>
    <item>
      <title>Gestione TOTP in bash</title>
      <link>https://noblogo.org/aytin/gestione-totp-in-bash</link>
      <description>&lt;![CDATA[totp&#xA;&#xA;In una giornata in cui avevo un po’ di tempo da investire, ho ripreso la versione 2 dello script &#34;Generazione OTP via bash&#34;, e ho deciso di renderlo un po’ più flessibile introducendo una logica crud. Anzi, il termine corretto dovrebbe essere &#34;dave&#34; (Delete, Add, View, Edit).&#xA;!--more--&#xA;Che altro aggiungere?&#xA;&#xA;Il file è ancora un insieme di coppie chiave-valore (nome account e chiave privata).&#xA;Il separatore è il simbolo &#34;:&#34;. Ma può essere configurato cambiando il valore della variabile &#34;DELIMITER&#34;.&#xA;Lo script è discretamente commentato così, in futuro, riuscirò a ricordare perché ho fatto quello che ho fatto (tipo &#34;Memento&#34;).&#xA;Le opzioni di gpg per la cifratura/decifratura del file sono configurabili nello script modificando il valore delle variabili &#34;GPG\ENC\OPTIONS&#34; e &#34;GPG\DEC\OPTIONS&#34;.&#xA;L’uso di getopts, mi rendo conto, è solo un esercizio di stile. Non è molto utile per come l’ho usato, visto che ogni optarg corrisponde ad un’operazione autoconclusiva.&#xA;Ero partito per rimpiazzare ogni costrutto if con combinazioni di lista and / lista or ma le condizioni risultavano talmente complesse da rendere il codice francamente molto più incomprensibile di adesso. In alternativa avrei potuto delegare l’esecuzione delle condizioni ad un insieme di funzioni con una stratificazione tale da ricadere nel punto precedente: offuscamento permanente del codice che va a vanificare l’intenzione iniziale di semplificare il codice e renderlo più leggibile.&#xA;&#xA;!/usr/local/bin/bash&#xA;&#xA;init() {&#xA;    # CODICI D&#39;ERRORE&#xA;    DECRYPTERR=64&#xA;    INVALIDOPTIONERR=65&#xA;    INVALIDACCOUNTERR=66&#xA;    ACCOUNTNOTFOUNDERR=67&#xA;    ACCOUNTNAMEEMPTYERR=68&#xA;    ACCOUNTEXISTSERR=69&#xA;    INVALIDKEYERR=70&#xA;&#xA;    # Colorscheme&#xA;    LIGHTGREEN=&#39;\033[1;32m&#39;&#xA;    LIGHTRED=&#39;\033[1;31m&#39;&#xA;    NC=&#39;\033[0m&#39;&#xA;&#xA;    # Path del file cifrato contentente i codici 2FA&#xA;    KEYS2FAFILE=&#34;path/fileenc.gpg&#34;&#xA;&#xA;    # Separatore delle coppie chiave-valore&#xA;    DELIMITER=&#34;:&#34;&#xA;&#xA;    # path gpg e opzioni di cifratura/decifratura&#xA;    GPGTTY=$(tty)&#xA;    export GPGTTY&#xA;&#xA;    #GPGCOMMAND=&#34;/usr/local/bin/gpg&#34;&#xA;    GPGENCOPTIONS=&#34;--yes -c --s2k-cipher-algo aes256 --s2k-digest-algo sha512 --s2k-mode 3 --s2k-count 260000 - &#34;&#xA;    GPGDECOPTIONS=&#39;-d&#39;&#xA;&#xA;    # Caratteri consentiti nel nome account&#xA;    PATTERN=&#39;^[a-zA-Z0-9.-]+$&#39;;&#xA;&#xA;    # Su MacOS è disponibile &#34;pbcopy&#34;, su nix &#34;xsel&#34;&#xA;    case &#34;$(sysctl hw.targettype 2  /dev/null)&#34; in&#xA;        &#34;&#34;)&#xA;            CLIPBOARD=&#34;xsel -bi&#34;&#xA;            GPGCOMMAND=&#34;/usr/bin/gpg&#34;;;&#xA;         )&#xA;            CLIPBOARD=&#34;pbcopy&#34;&#xA;            GPGCOMMAND=&#34;/usr/local/bin/gpg&#34;;;&#xA;    esac&#xA;&#xA;    # Decifra e carica in ram il contenuto del file cifrato.&#xA;    echo &#34;Decifratura del file degli account in corso...&#34;&#xA;    KEYS2FAFILEDEC=$(${GPGCOMMAND} ${GPGDECOPTIONS} &#34;${KEYS2FAFILE}&#34; 2  /dev/null)&#xA;&#xA;    # Se la password non è corretta, restituisce un errore.&#xA;    [[ -z &#34;${KEYS2FAFILEDEC}&#34; ]] &amp;&amp; { errmsg &#34;${DECRYPTERR}&#34; 1; }&#xA;    infomsg &#34;Fatto.&#34;&#xA;}&#xA;&#xA;getaccount() {&#xA;    ACCOUNTSTDIO=&#34;$1&#34;&#xA;&#xA;    # Check nome account vuoto&#xA;    [[ -z &#34;${ACCOUNTSTDIO}&#34; ]] &amp;&amp; return &#34;${ACCOUNTNAMEEMPTYERR}&#34;&#xA;&#xA;    # Check caratteri invalidi nel nome account&#xA;    [[ ! &#34;${ACCOUNTSTDIO}&#34; =~ $PATTERN ]] &amp;&amp; return &#34;${INVALIDACCOUNTERR}&#34;&#xA;&#xA;    # Estrae l&#39;elemento contenente l&#39;id dato in input&#xA;    OTPLINE=$(echo &#34;${KEYS2FAFILEDEC}&#34; | grep &#34;${ACCOUNTSTDIO}:&#34;)&#xA;&#xA;    # Se l&#39;account non esiste, restituisce un errore&#xA;    [[ -z &#34;${OTPLINE}&#34; ]] &amp;&amp; return &#34;${ACCOUNTNOTFOUNDERR}&#34;&#xA;&#xA;    ACCOUNT=$(echo $OTPLINE | cut -d&#34;${DELIMITER}&#34; -f 1)&#xA;    KEY=$(echo $OTPLINE | cut -d&#34;${DELIMITER}&#34; -f 2)&#xA;}&#xA;&#xA;cryptaccountfile() {&#xA;    ACCOUNTFILE=&#34;$1&#34;&#xA;    echo -e &#34;\nCifratura del file degli account in corso...&#34;&#xA;    ${GPGCOMMAND} -o &#34;$KEYS2FAFILE&#34; ${GPGENCOPTIONS} &lt;&lt;&lt; $(echo -e &#34;${ACCOUNTFILE}&#34;)&#xA;}&#xA;&#xA;noparam() {&#xA;    clear&#xA;    help&#xA;    exit 0&#xA;}&#xA;&#xA;Visualizza messaggi di errore su stdio&#xA;errmsg() {&#xA;    if [[ $# -ne 2 ]]; then&#xA;        echo -r&#34;${LIGHTRED}[DEBUG] [errmsg] Numero di parametri non corretto.${NC}.&#34;; exit 1&#xA;    else&#xA;        case $1 in&#xA;            &#34;${DECRYPTERR}&#34;            ) echo -e &#34;${LIGHTRED}[ERRORE] Impossibile decriptare il file.${NC}&#34;;;&#xA;            &#34;${INVALIDOPTIONERR}&#34;     ) echo -e &#34;${LIGHTRED}[ERRORE] Opzione \&#34;$OPTARG\&#34; non valida.${NC}&#34;;;&#xA;            &#34;${INVALIDACCOUNTERR}&#34;    ) echo -e &#34;${LIGHTRED}ERRORE] Rilevati caratteri non consentiti.\nI caratteri ammessi sono: [a-z[0-9].-${NC}&#34;;;&#xA;            &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) echo -e &#34;${LIGHTRED}[ERRORE] Account non trovato${NC}&#34;;;&#xA;            &#34;${ACCOUNTNAMEEMPTYERR}&#34; ) echo -e &#34;${LIGHTRED}[ERRORE] Nome account vuoto.${NC}&#34;;;&#xA;            &#34;${ACCOUNTEXISTSERR}&#34;     ) echo -e &#34;${LIGHTRED}[ERRORE] Nome account già esistente${NC}&#34;;;&#xA;            &#34;${INVALIDKEYERR}&#34;        ) echo -e &#34;${LIGHTRED}[ERRORE] Chiave privata nulla o non corretta.\nLa chiave privata deve essere base32.\n${NC}&#34;;;&#xA;            ) echo -e &#34;${LIGHTRED}[ERRORE] Errore generico.${NC}&#34;;;&#xA;        esac&#xA;&#xA;        [[ $2 -eq 1 ]] &amp;&amp; exit $2;&#xA;    fi&#xA;}&#xA;&#xA;Visualizza messaggi info su stdio&#xA;infomsg() {&#xA;    echo -e &#34;${LIGHTGREEN}$1${NC}&#34;&#xA;}&#xA;&#xA;continuemsg() {&#xA;    case $1 in&#xA;        &#34;insert&#34;    ) ACTION=&#34;creato&#34;;;&#xA;        &#34;edit&#34;      ) ACTION=&#34;modificato&#34;;;&#xA;        &#34;delete&#34;    ) ACTION=&#34;cancellato&#34;;;&#xA;    esac&#xA;&#xA;    echo -n &#34;L&#39;account \&#34;${ACCOUNT}\&#34; sta per essere ${ACTION}. Vuoi continuare? [y|N]: &#34;; read ANSWER;&#xA;}&#xA;&#xA;continueyn() {&#xA;        ANSWER=&#34;0&#34;&#xA;        while [[ &#34;${ANSWER}&#34; != &#34;y&#34; ]]; do&#xA;            [[ -z &#34;${ANSWER}&#34; || &#34;${ANSWER}&#34; == &#34;N&#34; ]] &amp;&amp; { echo &#34;Operazione annullata&#34;; exit; }&#xA;            [[ &#34;${ANSWER}&#34; != &#34;y&#34; || &#34;${ANSWER}&#34; != &#34;N&#34; ]] &amp;&amp; continuemsg &#34;$1&#34;&#xA;        done&#xA;}&#xA;&#xA;totp() {&#xA;    # Se la&#39;ccount non esiste o è invalido, esco con errore&#xA;    getaccount &#34;$1&#34;; RETCODE=$?&#xA;    case &#34;${RETCODE}&#34; in&#xA;        &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) errmsg &#34;${ACCOUNTNOTFOUNDERR}&#34; 1;;&#xA;        &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 1;;&#xA;    esac&#xA;&#xA;    echo -e &#34;\nCopia OTP nella clipboard...&#34;&#xA;    # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel&#xA;    oathtool --totp -b &#34;${KEY}&#34; | $CLIPBOARD&#xA;    infomsg &#34;Fatto.&#34;&#xA;    exit&#xA;}&#xA;&#xA;insert() {&#xA;    # Se l&#39;account non esiste o è invalido, esco con errore&#xA;    getaccount &#34;$1&#34;; RETCODE=$?&#xA;    case &#34;${RETCODE}&#34; in&#xA;        0                           ) errmsg &#34;${ACCOUNTEXISTSERR}&#34; 1;;&#xA;        &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 1;;&#xA;    esac&#xA;&#xA;    ACCOUNT=&#34;$1&#34;&#xA;&#xA;    # Inserisci la chiave&#xA;    echo -en &#34;Nuovo account: ${ACCOUNT}\nInserisci la nuova chiave privata: &#34;; read KEY&#xA;    while [[ -z &#34;${KEY}&#34; || ! $(echo ${KEY} | oathtool -b - 2  /dev/null) ]]; do&#xA;        errmsg &#34;${INVALIDKEYERR}&#34; 0&#xA;        echo -n &#34;Inserisci la chiave privata: &#34;; read KEY&#xA;    done&#xA;&#xA;    infomsg &#34;\nNuovo account:        ${ACCOUNT}\nNuova chiave privata: ${KEY}&#34;&#xA;    continueyn &#34;insert&#34;&#xA;&#xA;    # Appendo il nuovo record nel file degli account&#xA;    KEYS2FAFILEDEC+=&#34;\n${ACCOUNT}${DELIMITER}${KEY}&#34;&#xA;    infomsg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato creato con successo.&#34;&#xA;&#xA;    # Cifratura del file degli account&#xA;    cryptaccountfile &#34;${KEYS2FAFILEDEC}&#34;&#xA;    infomsg &#34;Fatto.&#34;&#xA;    exit&#xA;}&#xA;&#xA;edit() {&#xA;    # Se la&#39;ccount non esiste o è invalido, esco con errore&#xA;    getaccount &#34;$1&#34;; RETCODE=$?&#xA;    case &#34;${RETCODE}&#34; in&#xA;        &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) errmsg &#34;${ACCOUNTNOTFOUNDERR}&#34; 1;;&#xA;        &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 1;;&#xA;    esac&#xA;&#xA;    # Faccio una copia dell&#39;account per un eventuale ripristino durante&#xA;    # la fase di input&#xA;    ACCOUNTTEMP=&#34;${ACCOUNT}&#34;&#xA;    ACCOUNTMOD=&#34;${ACCOUNT}&#34;&#xA;&#xA;    # Inserisco il nuovo nome account.&#xA;    # 1. Se è vuoto: è la conferma dell&#39;account inserito all&#39;inizio e proseguo&#xA;    # 2. Se il nome è invalido: sollevo un&#39;eccezione e reitero&#xA;    # 3. Se il nome è valido:&#xA;    #    a. Se è un nome nuovo: acquisisco il nuovo nome, recupero nome e chiave dell&#39;account di partenza e proseguo&#xA;    #    b. Sè è lo stesso nome di partenza: è la conferma dell&#39;account iniziale e proseguo&#xA;    #    c. Se è un altro nome esistente o invalido: come il punto 2.&#xA;    while true; do&#xA;        echo&#xA;        echo -en &#34;Account: ${ACCOUNT}\nInserisci nuovo nome account: &#34;; read ACCOUNTMOD;&#xA;        getaccount &#34;${ACCOUNTMOD}&#34;; RETCODE=$?&#xA;        case &#34;${RETCODE}&#34; in&#xA;            # Punto 1.&#xA;            &#34;${ACCOUNTNAMEEMPTYERR}&#34; ) ACCOUNTMOD=&#34;${ACCOUNT}&#34;; break;;&#xA;            # Punto 2&#xA;            &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 0;;&#xA;            # Punto 3a.&#xA;            &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) getaccount &#34;${ACCOUNTTEMP}&#34;; break;;&#xA;            # Punti 3b e 3c, rispettivamente&#xA;            0                           ) [[ &#34;${ACCOUNTMOD}&#34; == &#34;${ACCOUNTTEMP}&#34; ]] &amp;&amp; break \&#xA;                                                                                 || { getaccount &#34;${ACCOUNTTEMP}&#34;; errmsg &#34;${ACCOUNTEXISTSERR}&#34; 0; };;&#xA;        esac&#xA;    done&#xA;&#xA;    [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNTMOD}&#34; &amp;&amp; ! -z &#34;${ACCOUNTMOD}&#34; ]] &amp;&amp; msg=&#34;\nL&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato in \&#34;${ACCOUNTMOD}\&#34;.&#34; \&#xA;                                                                    || msg=&#34;\nNessuna modifica rilevata.\n&#34;&#xA;    infomsg &#34;${msg}&#34;&#xA;&#xA;    # Modifica la chiave privata&#xA;    KEYMOD=&#34;${KEY}&#34;&#xA;    echo&#xA;&#xA;    while true; do&#xA;        echo -en &#34;Chiave privata: ${KEY}\nInserisci la nuova chiave privata: &#34;; read KEYMOD&#xA;        if [[ -z $KEYMOD ]]; then&#xA;            KEYMOD=&#34;${KEY}&#34;; break&#xA;        elif [[ ! $(echo &#34;${KEYMOD}&#34; | oathtool -b - 2  /dev/null) ]]; then&#xA;            errmsg &#34;${INVALIDKEYERR}&#34; 0; KEYMOD=&#34;${KEY}&#34;&#xA;        else&#xA;            break&#xA;        fi&#xA;    done&#xA;&#xA;    # Se c&#39;è stata almeno una modifica (account/key), sovrascrivo il record. Altrimenti esco senza fare nulla.&#xA;    if [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNTMOD}&#34; || &#34;${KEY}&#34; != &#34;${KEYMOD}&#34; ]]; then&#xA;        # Conferma modifica&#xA;&#xA;        infomsg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\nNuovo account:        ${ACCOUNTMOD}\nNuova chiave privata: ${KEYMOD}\n&#34;&#xA;        continueyn &#34;edit&#34;&#xA;&#xA;        # Modifica del file degli account&#xA;        KEYS2FAFILEDECTEMP=$(sed &#34;s/${ACCOUNT}${DELIMITER}${KEY}/${ACCOUNTMOD}${DELIMITER}${KEYMOD}/&#34; &lt;&lt;&lt; &#34;${KEYS2FAFILEDEC}&#34;)&#xA;        infomsg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato con successo.&#34;&#xA;&#xA;        # Cifratura del file degli account&#xA;        cryptaccountfile &#34;${KEYS2FAFILEDECTEMP}&#34;&#xA;    else&#xA;        infomsg &#34;Nessuna modifica rilevata.&#34;&#xA;    fi&#xA;    infomsg &#34;Fatto.&#34;&#xA;}&#xA;&#xA;delete() {&#xA;    if getaccount &#34;$1&#34;; then&#xA;        # Conferma eliminazione&#xA;        infomsg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\n&#34;&#xA;        continueyn &#34;delete&#34;&#xA;&#xA;        # Cancellazione account&#xA;        KEYS2FAFILEDECTEMP=$(sed &#34;/$ACCOUNT/d&#34; &lt;&lt;&lt; &#34;${KEYS2FAFILEDEC}&#34;)&#xA;        infomsg &#34;L&#39;account \&#34;$ACCOUNT\&#34; è stato eliminato.&#34;&#xA;&#xA;        # Cifratura del nuovo file degli account&#xA;        cryptaccountfile &#34;${KEYS2FAFILEDECTEMP}&#34;&#xA;        infomsg &#34;Fatto.&#34;&#xA;    else&#xA;        errmsg &#34;${ACCOUNTNOTFOUNDERR}&#34; 1&#xA;    fi&#xA;}&#xA;&#xA;list() {&#xA;    echo&#xA;    while read LINE; do&#xA;        cut -d&#34;${DELIMITER}&#34; -f 1 &lt;&lt;&lt; &#34;${LINE}&#34;&#xA;    done &lt;&lt;&lt; &#34;${KEYS2FAFILEDEC}&#34;&#xA;}&#xA;&#xA;help() {&#xA;cat&lt;&lt;EOF&#xA;Usa come: ./totp.sh [options] ARG&#xA;dove:&#xA;    [options]&#xA;        -l restituisce la lista degli account.&#xA;        -t nomeaccount restituisce l&#39;otp per quell&#39;account.&#xA;        -i nomeaccount inserisce un nuovo account.&#xA;        -d nomeaccount cancella nomeaccount.&#xA;        -e nomeaccount modifica nomeaccount.&#xA;        -h stampa questa pagina di help.&#xA;&#xA;ESEMPI:&#xA;    RESTITUISCE LA LISTA DEGLI ACCOUNT&#xA;    totp.sh -l&#xA;&#xA;    INSERISCE UN ACCOUNT&#xA;    totp.sh -i dropbox&#xA;&#xA;    CANCELLA UN ACCOUNT&#xA;    totp.sh -d megaupload&#xA;&#xA;    MODIFICA UN ACCOUNT&#xA;    totp.sh -e paypal&#xA;&#xA;    GENERA OTP&#xA;    totp.sh -t firefoxsync&#xA;EOF&#xA;}&#xA;&#xA;main() {&#xA;    init&#xA;    while getopts &#34;lt:i:e:d:&#34; opt; do&#xA;        case $opt in&#xA;            l ) list ;;&#xA;            t ) totp $OPTARG ;;&#xA;            i ) insert $OPTARG ;;&#xA;            e ) edit $OPTARG ;;&#xA;            d ) delete $OPTARG ;;&#xA;            ) clear;errmsg &#34;${INVALIDOPTIONERR}&#34; 1;;&#xA;        esac&#xA;    done&#xA;    shift $(($OPTIND - 1))&#xA;}&#xA;&#xA;[[ $# -eq 0 || $# -eq 1 &amp;&amp; &#34;$1&#34; == &#34;-h&#34; ]] &amp;&amp; noparam || main $*&#xA;&#xA;#bash #totp #cryptography #gpg #scripting]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/7c649620b-69b6c5/x5Ykru5oywKu/RE9KMTKwgIzhwDrkGnZqsTThRVvDSlNlyB2XwSv4.webp" alt="totp"></p>

<p>In una giornata in cui avevo un po’ di tempo da investire, ho ripreso la versione 2 dello script “<a href="https://noblogo.org/aytin/generazione-otp-via-bash" rel="nofollow">Generazione OTP via bash</a>”, e ho deciso di renderlo un po’ più flessibile introducendo una logica <strong>crud</strong>. Anzi, il termine corretto dovrebbe essere “<strong>dave</strong>” (<strong>D</strong>elete, <strong>A</strong>dd, <strong>V</strong>iew, <strong>E</strong>dit).

Che altro aggiungere?</p>
<ul><li>Il file è ancora un insieme di coppie chiave-valore (nome account e chiave privata).</li>
<li>Il separatore è il simbolo “<strong>:</strong>”. Ma può essere configurato cambiando il valore della variabile “<strong>DELIMITER</strong>”.</li>
<li>Lo script è discretamente commentato così, in futuro, riuscirò a ricordare perché ho fatto quello che ho fatto (tipo “Memento”).</li>
<li>Le opzioni di gpg per la cifratura/decifratura del file sono configurabili nello script modificando il valore delle variabili “<strong>GPG_ENC_OPTIONS</strong>” e “<strong>GPG_DEC_OPTIONS</strong>”.</li>
<li>L’uso di <strong>getopts,</strong> mi rendo conto, è solo un esercizio di stile. Non è molto utile per come l’ho usato, visto che ogni optarg corrisponde ad un’operazione autoconclusiva.</li>
<li>Ero partito per rimpiazzare ogni costrutto <strong>if</strong> con combinazioni di lista and / lista or ma le condizioni risultavano talmente complesse da rendere il codice francamente molto più incomprensibile di adesso. In alternativa avrei potuto delegare l’esecuzione delle condizioni ad un insieme di funzioni con una stratificazione tale da ricadere nel punto precedente: offuscamento permanente del codice che va a vanificare l’intenzione iniziale di semplificare il codice e renderlo più leggibile.</li></ul>

<pre><code class="language-bash">
#!/usr/local/bin/bash


init() {
    # CODICI D&#39;ERRORE
    DECRYPT_ERR=64
    INVALID_OPTION_ERR=65
    INVALID_ACCOUNT_ERR=66
    ACCOUNT_NOT_FOUND_ERR=67
    ACCOUNT_NAME_EMPTY_ERR=68
    ACCOUNT_EXISTS_ERR=69
    INVALID_KEY_ERR=70

    # Colorscheme
    LIGHT_GREEN=&#39;\033[1;32m&#39;
    LIGHT_RED=&#39;\033[1;31m&#39;
    NC=&#39;\033[0m&#39;

    # Path del file cifrato contentente i codici 2FA
    KEYS_2FA_FILE=&#34;&lt;path/file_enc.gpg&gt;&#34;

    # Separatore delle coppie chiave-valore
    DELIMITER=&#34;:&#34;

    # path gpg e opzioni di cifratura/decifratura
    GPG_TTY=$(tty)
    export GPG_TTY

    #GPG_COMMAND=&#34;/usr/local/bin/gpg&#34;
    GPG_ENC_OPTIONS=&#34;--yes -c --s2k-cipher-algo aes256 --s2k-digest-algo sha512 --s2k-mode 3 --s2k-count 260000 - &#34;
    GPG_DEC_OPTIONS=&#39;-d&#39;

    # Caratteri consentiti nel nome account
    PATTERN=&#39;^[a-zA-Z0-9._-]+$&#39;;

    # Su MacOS è disponibile &#34;pbcopy&#34;, su *nix &#34;xsel&#34;
    case &#34;$(sysctl hw.targettype 2&gt;/dev/null)&#34; in
        &#34;&#34;)
            CLIPBOARD=&#34;xsel -bi&#34;
            GPG_COMMAND=&#34;/usr/bin/gpg&#34;;;
         *)
            CLIPBOARD=&#34;pbcopy&#34;
            GPG_COMMAND=&#34;/usr/local/bin/gpg&#34;;;
    esac

    # Decifra e carica in ram il contenuto del file cifrato.
    echo &#34;Decifratura del file degli account in corso...&#34;
    KEYS_2FA_FILE_DEC=$(${GPG_COMMAND} ${GPG_DEC_OPTIONS} &#34;${KEYS_2FA_FILE}&#34; 2&gt;/dev/null)

    # Se la password non è corretta, restituisce un errore.
    [[ -z &#34;${KEYS_2FA_FILE_DEC}&#34; ]] &amp;&amp; { err_msg &#34;${DECRYPT_ERR}&#34; 1; }
    info_msg &#34;Fatto.&#34;
}


get_account() {
    ACCOUNT_STDIO=&#34;$1&#34;

    # Check nome account vuoto
    [[ -z &#34;${ACCOUNT_STDIO}&#34; ]] &amp;&amp; return &#34;${ACCOUNT_NAME_EMPTY_ERR}&#34;

    # Check caratteri invalidi nel nome account
    [[ ! &#34;${ACCOUNT_STDIO}&#34; =~ $PATTERN ]] &amp;&amp; return &#34;${INVALID_ACCOUNT_ERR}&#34;

    # Estrae l&#39;elemento contenente l&#39;id dato in input
    OTP_LINE=$(echo &#34;${KEYS_2FA_FILE_DEC}&#34; | grep &#34;${ACCOUNT_STDIO}:&#34;)

    # Se l&#39;account non esiste, restituisce un errore
    [[ -z &#34;${OTP_LINE}&#34; ]] &amp;&amp; return &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;

    ACCOUNT=$(echo $OTP_LINE | cut -d&#34;${DELIMITER}&#34; -f 1)
    KEY=$(echo $OTP_LINE | cut -d&#34;${DELIMITER}&#34; -f 2)
}


crypt_account_file() {
    ACCOUNT_FILE=&#34;$1&#34;
    echo -e &#34;\nCifratura del file degli account in corso...&#34;
    ${GPG_COMMAND} -o &#34;$KEYS_2FA_FILE&#34; ${GPG_ENC_OPTIONS} &lt;&lt;&lt; $(echo -e &#34;${ACCOUNT_FILE}&#34;)
}


no_param() {
    clear
    help
    exit 0
}


# Visualizza messaggi di errore su stdio
err_msg() {
    if [[ $# -ne 2 ]]; then
        echo -r&#34;${LIGHT_RED}[DEBUG] [err_msg] Numero di parametri non corretto.${NC}.&#34;; exit 1
    else
        case $1 in
            &#34;${DECRYPT_ERR}&#34;            ) echo -e &#34;${LIGHT_RED}[ERRORE] Impossibile decriptare il file.${NC}&#34;;;
            &#34;${INVALID_OPTION_ERR}&#34;     ) echo -e &#34;${LIGHT_RED}[ERRORE] Opzione \&#34;$OPTARG\&#34; non valida.${NC}&#34;;;
            &#34;${INVALID_ACCOUNT_ERR}&#34;    ) echo -e &#34;${LIGHT_RED}[ERRORE] Rilevati caratteri non consentiti.\nI caratteri ammessi sono: [a-z][A-Z][0-9].-_${NC}&#34;;;
            &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) echo -e &#34;${LIGHT_RED}[ERRORE] Account non trovato${NC}&#34;;;
            &#34;${ACCOUNT_NAME_EMPTY_ERR}&#34; ) echo -e &#34;${LIGHT_RED}[ERRORE] Nome account vuoto.${NC}&#34;;;
            &#34;${ACCOUNT_EXISTS_ERR}&#34;     ) echo -e &#34;${LIGHT_RED}[ERRORE] Nome account già esistente${NC}&#34;;;
            &#34;${INVALID_KEY_ERR}&#34;        ) echo -e &#34;${LIGHT_RED}[ERRORE] Chiave privata nulla o non corretta.\nLa chiave privata deve essere base32.\n${NC}&#34;;;
            *                           ) echo -e &#34;${LIGHT_RED}[ERRORE] Errore generico.${NC}&#34;;;
        esac

        [[ $2 -eq 1 ]] &amp;&amp; exit $2;
    fi
}


# Visualizza messaggi info su stdio
info_msg() {
    echo -e &#34;${LIGHT_GREEN}$1${NC}&#34;
}


continue_msg() {
    case $1 in
        &#34;insert&#34;    ) ACTION=&#34;creato&#34;;;
        &#34;edit&#34;      ) ACTION=&#34;modificato&#34;;;
        &#34;delete&#34;    ) ACTION=&#34;cancellato&#34;;;
    esac

    echo -n &#34;L&#39;account \&#34;${ACCOUNT}\&#34; sta per essere ${ACTION}. Vuoi continuare? [y|N]: &#34;; read ANSWER;
}


continue_yn() {
        ANSWER=&#34;0&#34;
        while [[ &#34;${ANSWER}&#34; != &#34;y&#34; ]]; do
            [[ -z &#34;${ANSWER}&#34; || &#34;${ANSWER}&#34; == &#34;N&#34; ]] &amp;&amp; { echo &#34;Operazione annullata&#34;; exit; }
            [[ &#34;${ANSWER}&#34; != &#34;y&#34; || &#34;${ANSWER}&#34; != &#34;N&#34; ]] &amp;&amp; continue_msg &#34;$1&#34;
        done
}


totp() {
    # Se la&#39;ccount non esiste o è invalido, esco con errore
    get_account &#34;$1&#34;; RET_CODE=$?
    case &#34;${RET_CODE}&#34; in
        &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) err_msg &#34;${ACCOUNT_NOT_FOUND_ERR}&#34; 1;;
        &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 1;;
    esac

    echo -e &#34;\nCopia OTP nella clipboard...&#34;
    # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel
    oathtool --totp -b &#34;${KEY}&#34; | $CLIPBOARD
    info_msg &#34;Fatto.&#34;
    exit
}


insert() {
    # Se l&#39;account non esiste o è invalido, esco con errore
    get_account &#34;$1&#34;; RET_CODE=$?
    case &#34;${RET_CODE}&#34; in
        0                           ) err_msg &#34;${ACCOUNT_EXISTS_ERR}&#34; 1;;
        &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 1;;
    esac

    ACCOUNT=&#34;$1&#34;

    # Inserisci la chiave
    echo -en &#34;Nuovo account: ${ACCOUNT}\nInserisci la nuova chiave privata: &#34;; read KEY
    while [[ -z &#34;${KEY}&#34; || ! $(echo ${KEY} | oathtool -b - 2&gt;/dev/null) ]]; do
        err_msg &#34;${INVALID_KEY_ERR}&#34; 0
        echo -n &#34;Inserisci la chiave privata: &#34;; read KEY
    done

    info_msg &#34;\nNuovo account:        ${ACCOUNT}\nNuova chiave privata: ${KEY}&#34;
    continue_yn &#34;insert&#34;

    # Appendo il nuovo record nel file degli account
    KEYS_2FA_FILE_DEC+=&#34;\n${ACCOUNT}${DELIMITER}${KEY}&#34;
    info_msg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato creato con successo.&#34;

    # Cifratura del file degli account
    crypt_account_file &#34;${KEYS_2FA_FILE_DEC}&#34;
    info_msg &#34;Fatto.&#34;
    exit
}


edit() {
    # Se la&#39;ccount non esiste o è invalido, esco con errore
    get_account &#34;$1&#34;; RET_CODE=$?
    case &#34;${RET_CODE}&#34; in
        &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) err_msg &#34;${ACCOUNT_NOT_FOUND_ERR}&#34; 1;;
        &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 1;;
    esac

    # Faccio una copia dell&#39;account per un eventuale ripristino durante
    # la fase di input
    ACCOUNT_TEMP=&#34;${ACCOUNT}&#34;
    ACCOUNT_MOD=&#34;${ACCOUNT}&#34;

    # Inserisco il nuovo nome account.
    # 1. Se è vuoto: è la conferma dell&#39;account inserito all&#39;inizio e proseguo
    # 2. Se il nome è invalido: sollevo un&#39;eccezione e reitero
    # 3. Se il nome è valido:
    #    a. Se è un nome nuovo: acquisisco il nuovo nome, recupero nome e chiave dell&#39;account di partenza e proseguo
    #    b. Sè è lo stesso nome di partenza: è la conferma dell&#39;account iniziale e proseguo
    #    c. Se è un altro nome esistente o invalido: come il punto 2.
    while true; do
        echo
        echo -en &#34;Account: ${ACCOUNT}\nInserisci nuovo nome account: &#34;; read ACCOUNT_MOD;
        get_account &#34;${ACCOUNT_MOD}&#34;; RET_CODE=$?
        case &#34;${RET_CODE}&#34; in
            # Punto 1.
            &#34;${ACCOUNT_NAME_EMPTY_ERR}&#34; ) ACCOUNT_MOD=&#34;${ACCOUNT}&#34;; break;;
            # Punto 2
            &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 0;;
            # Punto 3a.
            &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) get_account &#34;${ACCOUNT_TEMP}&#34;; break;;
            # Punti 3b e 3c, rispettivamente
            0                           ) [[ &#34;${ACCOUNT_MOD}&#34; == &#34;${ACCOUNT_TEMP}&#34; ]] &amp;&amp; break \
                                                                                 || { get_account &#34;${ACCOUNT_TEMP}&#34;; err_msg &#34;${ACCOUNT_EXISTS_ERR}&#34; 0; };;
        esac
    done

    [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNT_MOD}&#34; &amp;&amp; ! -z &#34;${ACCOUNT_MOD}&#34; ]] &amp;&amp; msg=&#34;\nL&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato in \&#34;${ACCOUNT_MOD}\&#34;.&#34; \
                                                                    || msg=&#34;\nNessuna modifica rilevata.\n&#34;
    info_msg &#34;${msg}&#34;

    # Modifica la chiave privata
    KEY_MOD=&#34;${KEY}&#34;
    echo

    while true; do
        echo -en &#34;Chiave privata: ${KEY}\nInserisci la nuova chiave privata: &#34;; read KEY_MOD
        if [[ -z $KEY_MOD ]]; then
            KEY_MOD=&#34;${KEY}&#34;; break
        elif [[ ! $(echo &#34;${KEY_MOD}&#34; | oathtool -b - 2&gt;/dev/null) ]]; then
            err_msg &#34;${INVALID_KEY_ERR}&#34; 0; KEY_MOD=&#34;${KEY}&#34;
        else
            break
        fi
    done

    # Se c&#39;è stata almeno una modifica (account/key), sovrascrivo il record. Altrimenti esco senza fare nulla.
    if [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNT_MOD}&#34; || &#34;${KEY}&#34; != &#34;${KEY_MOD}&#34; ]]; then
        # Conferma modifica

        info_msg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\nNuovo account:        ${ACCOUNT_MOD}\nNuova chiave privata: ${KEY_MOD}\n&#34;
        continue_yn &#34;edit&#34;

        # Modifica del file degli account
        KEYS_2FA_FILE_DEC_TEMP=$(sed &#34;s/${ACCOUNT}${DELIMITER}${KEY}/${ACCOUNT_MOD}${DELIMITER}${KEY_MOD}/&#34; &lt;&lt;&lt; &#34;${KEYS_2FA_FILE_DEC}&#34;)
        info_msg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato con successo.&#34;

        # Cifratura del file degli account
        crypt_account_file &#34;${KEYS_2FA_FILE_DEC_TEMP}&#34;
    else
        info_msg &#34;Nessuna modifica rilevata.&#34;
    fi
    info_msg &#34;Fatto.&#34;
}


delete() {
    if get_account &#34;$1&#34;; then
        # Conferma eliminazione
        info_msg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\n&#34;
        continue_yn &#34;delete&#34;

        # Cancellazione account
        KEYS_2FA_FILE_DEC_TEMP=$(sed &#34;/$ACCOUNT/d&#34; &lt;&lt;&lt; &#34;${KEYS_2FA_FILE_DEC}&#34;)
        info_msg &#34;L&#39;account \&#34;$ACCOUNT\&#34; è stato eliminato.&#34;

        # Cifratura del nuovo file degli account
        crypt_account_file &#34;${KEYS_2FA_FILE_DEC_TEMP}&#34;
        info_msg &#34;Fatto.&#34;
    else
        err_msg &#34;${ACCOUNT_NOT_FOUND_ERR}&#34; 1
    fi
}


list() {
    echo
    while read LINE; do
        cut -d&#34;${DELIMITER}&#34; -f 1 &lt;&lt;&lt; &#34;${LINE}&#34;
    done &lt;&lt;&lt; &#34;${KEYS_2FA_FILE_DEC}&#34;
}


help() {
cat&lt;&lt;EOF
Usa come: ./totp.sh [options] ARG
dove:
    [options]
        -l restituisce la lista degli account.
        -t &lt;nome_account&gt; restituisce l&#39;otp per quell&#39;account.
        -i &lt;nome_account&gt; inserisce un nuovo account.
        -d &lt;nome_account&gt; cancella &lt;nome_account&gt;.
        -e &lt;nome_account&gt; modifica &lt;nome_account&gt;.
        -h stampa questa pagina di help.

ESEMPI:
    RESTITUISCE LA LISTA DEGLI ACCOUNT
    totp.sh -l

    INSERISCE UN ACCOUNT
    totp.sh -i dropbox

    CANCELLA UN ACCOUNT
    totp.sh -d megaupload

    MODIFICA UN ACCOUNT
    totp.sh -e paypal

    GENERA OTP
    totp.sh -t firefoxsync
EOF
}


main() {
    init
    while getopts &#34;lt:i:e:d:&#34; opt; do
        case $opt in
            l ) list ;;
            t ) totp $OPTARG ;;
            i ) insert $OPTARG ;;
            e ) edit $OPTARG ;;
            d ) delete $OPTARG ;;
            * ) clear;err_msg &#34;${INVALID_OPTION_ERR}&#34; 1;;
        esac
    done
    shift $(($OPTIND - 1))
}

[[ $# -eq 0 || $# -eq 1 &amp;&amp; &#34;$1&#34; == &#34;-h&#34; ]] &amp;&amp; no_param || main $*

</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:totp" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">totp</span></a> <a href="/aytin/tag:cryptography" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">cryptography</span></a> <a href="/aytin/tag:gpg" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">gpg</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/gestione-totp-in-bash</guid>
      <pubDate>Wed, 21 Feb 2024 15:37:02 +0000</pubDate>
    </item>
    <item>
      <title>Connettere lo smartphone al pc via adb</title>
      <link>https://noblogo.org/aytin/connettere-lo-smartphone-al-pc-via-adb</link>
      <description>&lt;![CDATA[tools&#xA;smalliFoto di Pixabay: a href=&#34;https://www.pexels.com/it-it/foto/chiave-inglese-stretta-su-chiave-inglese-210881/&#34;Pexels.com/a/i/small&#xA;&#xA;Android Debug Bridge, adb, permette di connettere device ad un pc. Oltre che consentire l&#39;accesso all&#39;intero file system del device invece che della sola parte multimediale, mette a disposizione un set di comandi per la manipolazione di:&#xA;!--more--&#xA;&#xA;rete&#xA;installazione app&#xA;filesystem&#xA;scripting&#xA;debugging&#xA;&#xA;Do per scontata la presenza degli android tools e la conoscenza di come si attivi la modalità sviluppatore per accedere alla parte di debugging.&#xA;&#xA;Modalità di connessione&#xA;&#xA;Posso collegare il device all&#39;host in 3 modi: usb, tcpip, wireless (da android 11 in poi).&#xA;&#xA;Probabilmente la modalità usb è quella più sicura trattandosi di un collegamento fisico, ma il wireless, per me, è una gran comodità.&#xA;USB&#xA;&#xA;Giusto per dovere di cronaca. Chiunque possegga un telefono android, credo l&#39;abbia fatto almeno una volta.&#xA;&#xA;Collegare il telefono alla porta usb del pc.&#xA;Abilitare il Debug Usb dalle opzioni sviluppatore.&#xA;Autorizzare l&#39;host sul device.&#xA;&#xA;Come avviene l&#39;autorizzazione&#xA;L&#39;autorizzazione dell&#39;host consiste in un&#39;autenticazione con chiave pubblica.&#xA;Sull&#39;host, in ~/.android (almeno sui sistemi nix) è presente:&#xA;&#xA;una coppia di chiavi pubblica e privata&#xA;il file adb\known\hosts.pb.&#xA;&#xA;Quando si avvia il server adb sull&#39;host per la prima volta, viene generata una coppia di chiavi.&#xA;La chiave pubblica dell&#39;host viene inviata al device durante la prima connessione, ed è quella che deve essere accettata dal device affinché sia possibile accedervi via adb. Quando si effettua una &#34;revoca delle autorizzazioni&#34; sul device, si cancellano le chiavi pubbliche che devono essere riaccettate se avviene una nuova connessione.&#xA;&#xA;Il file adb\known\hosts.pb contiene gli identificativi dei device connessi all&#39;host via adb.&#xA;&#xA;Se si vuole rigenerare la coppia di chiavi per ruotarle, si può ricorrere al seguente comando:&#xA;adb keygen adbkey&#xA;che produce una chiave privata rsa-2048 e la corrispondente chiave pubblica.&#xA;&#xA;Le chiavi così generate andranno posizionate in ~/.android e il daemon adb andrà riavviato.&#xA;adb kill-server&#xA;adb start-server&#xA;TCPIP&#xA;L&#39;abilitazione della modalità tcpip richiede il preventivo collegamento fisico all&#39;host in modalità usb.&#xA;Una volta abilitata, questa rimarrà la modalità di collegamento predefinita finché il daemon sarà attivo.&#xA;&#xA;Collegare il device in modalità debug usb (vedi USB).&#xA;adb tcpip port per abilitare la modalità tcpip su una porta. Verrà avviato un altro daemon in ascolto sulla porta port.&#xA;recuperare l&#39;ip del device sulla nostra rete locale (normalmente disponibile su Info telefono / Info stato).&#xA;adb connect ip:port.&#xA;Staccare il device dalla porta usb.&#xA;Per le successive operazioni di connessione | disconnessione basta il solito:&#xA;adb connect | disconnect ip:port, connette | disconnette il device.&#xA;&#xA;Wi-Fi&#xA;A differenza della modalità tcpip, non richiede un collegamento fisico all&#39;host perché il pairing viene fatto sia sulla rete a cui ci si assesta, sia sull&#39;host attraverso un PIN. Inoltre ogni connessione avviene su una porta randomica.&#xA;&#xA;La prima volta:&#xA;  abilitare Debug wireless da Opzioni per sviluppatori.&#xA;  Confermare il pairing alla rete.&#xA;  Andare su Debug wireless, accoppiare un nuovo dispositivo e prendere nota dell&#39;ip, della porta e del PIN.&#xA;  Sull&#39;host adb pair ip:port, per associare il device all&#39;host attraverso il PIN segnato in precedenza.&#xA;Ad ogni connessione:&#xA;  adb connect | disconnect ip:port per connettere | disconnettere il device.&#xA;&#xA;Una volta che l&#39;host è autorizzato, basterà effettuare una connect alla porta opportuna anche se il server venisse riavviato.&#xA;&#xA;Usare ADB su un host con dispositivi multipli connessi&#xA;Quando più device sono connessi ad un host, per lanciare un comando, dobbiamo sapere come selezionare il device corretto via adb.&#xA;Ci vengono in aiuto due flag, &#34;-s&#34; e &#34;-t&#34;, che selezionano il device rispettivamente in base al seriale o al &#34;transport id&#34;, informazione che ottengo con adb devices.&#xA;Faccio l&#39;esempio di 3 device, due connessi via usb e uno connesso via wireless (da Android 11 in poi) su cui vogliamo aprire una shell.&#xA;adb devices&#xA;List of devices attached&#xA;141*             device&#xA;52004******    device&#xA;ip:porta         device&#xA;Per avere l&#39;info sul transport id è necessaria la modalità &#34;verbose&#34;&#xA;adb devices -l&#xA;&#xA;List of devices attached&#xA;141             device usb:1-3 product:serialproduct1 model:modelname1 device:devicename1 transportid:10&#xA;&#xA;52004*******    device usb:1-2 product:serialproduct2 model:modelname2 device:devicename2 transportid:8&#xA;&#xA;ip:porta         device product:serialproduct3 model:modelname3 device:devicename3 transportid:11&#xA;da cui:&#xA;adb -s 141 shell o adb -t 10 shell, per aprire una shell sul primo device.&#xA;&#xA;adb -s 52004******* shell o adb -t 8 shell, per aprire una shell sul secondo device.&#xA;&#xA;adb -s ip:porta shell o adb -t 11 shell, per aprire una shell sul terzo device.&#xA;&#xA;small Riferimenti*:&#xA;&#xA;https://developer.android.com/tools/adb&#xA;/small&#xA;&#xA;#adb #android #bash]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/186f813ba-297a89/jzUMZA9z4W9x/zQnycWwZdmrvBmpmIuXu6DjA5ydMl9TGbbrOum56.jpg" alt="tools">
<small><i>Foto di Pixabay: <a href="https://www.pexels.com/it-it/foto/chiave-inglese-stretta-su-chiave-inglese-210881/" rel="nofollow">Pexels.com</a></i></small></p>

<p><strong>A</strong>ndroid <strong>D</strong>ebug <strong>B</strong>ridge, <strong>adb</strong>, permette di connettere device ad un pc. Oltre che consentire l&#39;accesso all&#39;intero file system del device invece che della sola parte multimediale, mette a disposizione un set di comandi per la manipolazione di:
</p>
<ul><li>rete</li>
<li>installazione app</li>
<li>filesystem</li>
<li>scripting</li>
<li>debugging</li></ul>

<p>Do per scontata la presenza degli android tools e la conoscenza di come si attivi la modalità sviluppatore per accedere alla parte di debugging.</p>

<h2 id="modalità-di-connessione">Modalità di connessione</h2>

<p>Posso collegare il device all&#39;host in 3 modi: <em>usb</em>, <em>tcpip</em>, <em>wireless</em> (da android 11 in poi).</p>

<p>Probabilmente la modalità <em>usb</em> è quella più sicura trattandosi di un collegamento fisico, ma il <em>wireless</em>, per me, è una gran comodità.</p>

<h3 id="usb">USB</h3>

<p>Giusto per dovere di cronaca. Chiunque possegga un telefono android, credo l&#39;abbia fatto almeno una volta.</p>
<ol><li>Collegare il telefono alla porta usb del pc.</li>
<li>Abilitare il <strong>Debug Usb</strong> dalle opzioni sviluppatore.</li>
<li>Autorizzare l&#39;host sul device.</li></ol>

<h4 id="come-avviene-l-autorizzazione">Come avviene l&#39;autorizzazione</h4>

<p>L&#39;autorizzazione dell&#39;host consiste in un&#39;<strong>autenticazione con chiave pubblica</strong>.
Sull&#39;host, in <code>~/.android</code> (almeno sui sistemi *nix) è presente:</p>
<ul><li>una coppia di chiavi pubblica e privata</li>
<li>il file <em>adb_known_hosts.pb</em>.</li></ul>

<p>Quando si avvia il server adb sull&#39;host per la prima volta, viene generata una coppia di chiavi.
La chiave pubblica dell&#39;host viene inviata al device durante la prima connessione, ed è quella che deve essere accettata dal device affinché sia possibile accedervi via adb. Quando si effettua una “revoca delle autorizzazioni” sul device, si cancellano le chiavi pubbliche che devono essere riaccettate se avviene una nuova connessione.</p>

<p>Il file <em>adb_known_hosts.pb</em> contiene gli identificativi dei device connessi all&#39;host via adb.</p>

<p>Se si vuole rigenerare la coppia di chiavi per ruotarle, si può ricorrere al seguente comando:</p>

<pre><code class="language-bash">adb keygen adbkey
</code></pre>

<p>che produce una chiave privata rsa-2048 e la corrispondente chiave pubblica.</p>

<p>Le chiavi così generate andranno posizionate in <code>~/.android</code> e il daemon adb andrà riavviato.</p>

<pre><code class="language-bash">adb kill-server
adb start-server
</code></pre>

<h3 id="tcpip">TCPIP</h3>

<p>L&#39;abilitazione della modalità <em>tcpip</em> richiede il preventivo collegamento fisico all&#39;host in modalità <em>usb</em>.
Una volta abilitata, questa rimarrà la modalità di collegamento predefinita finché il daemon sarà attivo.</p>
<ul><li>Collegare il device in modalità debug usb (vedi USB).</li>
<li><code>adb tcpip &lt;port&gt;</code> per abilitare la modalità <em>tcpip</em> su una porta. Verrà avviato un altro daemon in ascolto sulla porta .</li>
<li>recuperare l&#39;ip del device sulla nostra rete locale (normalmente disponibile su <strong>Info telefono / Info stato</strong>).</li>
<li><code>adb connect &lt;ip&gt;:&lt;port&gt;</code>.</li>
<li>Staccare il device dalla porta usb.
Per le successive operazioni di connessione | disconnessione basta il solito:</li>
<li><code>adb connect | disconnect &lt;ip&gt;:&lt;port&gt;</code>, connette | disconnette il device.</li></ul>

<h3 id="wi-fi">Wi-Fi</h3>

<p>A differenza della modalità <em>tcpip</em>, non richiede un collegamento fisico all&#39;host perché il pairing viene fatto sia sulla rete a cui ci si assesta, sia sull&#39;host attraverso un PIN. Inoltre ogni connessione avviene su una porta randomica.</p>
<ul><li>La prima volta:
<ul><li>abilitare <strong>Debug wireless</strong> da <strong>Opzioni per sviluppatori</strong>.</li>
<li>Confermare il pairing alla rete.</li>
<li>Andare su <strong>Debug wireless</strong>, accoppiare un nuovo dispositivo e prendere nota dell&#39;ip, della porta e del PIN.</li>
<li>Sull&#39;host <code>adb pair &lt;ip&gt;:&lt;port&gt;</code>, per associare il device all&#39;host attraverso il PIN segnato in precedenza.</li></ul></li>
<li>Ad ogni connessione:
<ul><li><code>adb connect | disconnect &lt;ip&gt;:&lt;port&gt;</code> per connettere | disconnettere il device.</li></ul></li></ul>

<p>Una volta che l&#39;host è autorizzato, basterà effettuare una connect alla porta opportuna anche se il server venisse riavviato.</p>

<h2 id="usare-adb-su-un-host-con-dispositivi-multipli-connessi">Usare ADB su un host con dispositivi multipli connessi</h2>

<p>Quando più device sono connessi ad un host, per lanciare un comando, dobbiamo sapere come selezionare il device corretto via adb.
Ci vengono in aiuto due flag, “-s” e “-t”, che selezionano il device rispettivamente in base al seriale o al “transport id”, informazione che ottengo con <code>adb devices</code>.
Faccio l&#39;esempio di 3 device, due connessi via usb e uno connesso via wireless (da Android 11 in poi) su cui vogliamo aprire una shell.</p>

<pre><code class="language-bash">adb devices
List of devices attached
141*****             device
52004************    device
&lt;ip&gt;:&lt;porta&gt;         device
</code></pre>

<p>Per avere l&#39;info sul transport id è necessaria la modalità “verbose”</p>

<pre><code class="language-bash">adb devices -l

List of devices attached
141*****             device usb:1-3 product:&lt;serial_product_1&gt; model:&lt;model_name_1&gt; device:&lt;device_name_1&gt; transport_id:10

52004************    device usb:1-2 product:&lt;serial_product_2&gt; model:&lt;model_name_2&gt; device:&lt;device_name_2&gt; transport_id:8

&lt;ip&gt;:&lt;porta&gt;         device product:&lt;serial_product_3&gt; model:&lt;model_name_3&gt; device:&lt;device_name_3&gt; transport_id:11
</code></pre>

<p>da cui:
<code>adb -s 141***** shell</code> o <code>adb -t 10 shell</code>, per aprire una shell sul primo device.</p>

<p><code>adb -s 52004************ shell</code> o <code>adb -t 8 shell</code>, per aprire una shell sul secondo device.</p>

<p><code>adb -s &lt;ip&gt;:&lt;porta&gt; shell</code> o <code>adb -t 11 shell</code>, per aprire una shell sul terzo device.</p>

<p><small> <strong>Riferimenti</strong>:</p>
<ul><li><a href="https://developer.android.com/tools/adb" rel="nofollow">https://developer.android.com/tools/adb</a>
</small></li></ul>

<p><a href="/aytin/tag:adb" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">adb</span></a> <a href="/aytin/tag:android" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">android</span></a> <a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/connettere-lo-smartphone-al-pc-via-adb</guid>
      <pubDate>Wed, 28 Jun 2023 20:10:38 +0000</pubDate>
    </item>
    <item>
      <title>Ancora su brew [upgrade]</title>
      <link>https://noblogo.org/aytin/ancora-su-brew-upgrade</link>
      <description>&lt;![CDATA[(pubblicato il 14 aprile 2021)&#xA;Photo by Mati Mango on Pexels.com&#xA;smalliPhoto by Mati Mango on a href=&#34;https://www.pexels.com/it-it/foto/persona-in-camicia-a-maniche-lunghe-nera-utilizzando-macbook-air-6330644/&#34;Pexels.com/a/i/small&#xA;&#xA;Faccio un po’ d’ordine rispetto all’ultimo post.&#xA;&#xA;brew ha “fattorizzato” la fase di aggiornamento.&#xA;&#xA;Non sono più disponibili le istruzioni esplicite di gestione dei cask perché l’update / upgrade ora comprende formule e cask rendendo il tutto molto più coerente. Quindi:&#xA;!--more--&#xA;&#xA;brew update : Aggiorna il database interno sulle nuove versioni disponibili per formule e cask&#xA;brew upgrade : Aggiorna formule e cask&#xA;brew upgrade --cask : Aggiorna solo i cask&#xA;&#xA;Per quel che riguarda i cask, sappiamo che l’upgrade di brew uprende in considerazione solo i cask che non dispongono della funzionalità di biautoupdate/i/b o che hanno bilatest/i/b come versione./u&#xA;&#xA;Per forzare l’aggiornameno dei cask residui basta una singola istruzione:&#xA;brew upgrade --cask -n --greedy|tail -n +2|grep -v latest|cut -d &#34; &#34; -f 1|xargs brew upgrade --cask&#xA;Aggiorna esplicitamente tutti i cask con autoupdate (esclusi i “latest”)&#xA;&#xA;#brew #bash #macos #scripting]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 14 aprile 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/42a8ecf32-5a8865/ja1LyaRcn7jS/NM0AI07Y3zsdIZt8bOnBixTprjbfQEbpC5KnqLEQ.jpg" alt="Photo by Mati Mango on Pexels.com">
<small><i>Photo by Mati Mango on <a href="https://www.pexels.com/it-it/foto/persona-in-camicia-a-maniche-lunghe-nera-utilizzando-macbook-air-6330644/" rel="nofollow">Pexels.com</a></i></small></p>

<p>Faccio un po’ d’ordine rispetto <a href="https://noblogo.org/aytin/effettuare-un-brew-upgrade-completo" rel="nofollow">all’ultimo post</a>.</p>

<p><strong>brew</strong> ha “fattorizzato” la fase di aggiornamento.</p>

<p>Non sono più disponibili le istruzioni esplicite di gestione dei cask perché l’update / upgrade ora comprende formule e cask rendendo il tutto molto più coerente. Quindi:
</p>
<ul><li><code>brew update</code> : Aggiorna il database interno sulle nuove versioni disponibili per formule e cask</li>
<li><code>brew upgrade</code> : Aggiorna formule e cask</li>
<li><code>brew upgrade --cask</code> : Aggiorna solo i cask</li></ul>

<p>Per quel che riguarda i cask, sappiamo che l’upgrade di brew <u>prende in considerazione solo i cask che non dispongono della funzionalità di <b><i>autoupdate</i></b> o che hanno <b><i>latest</i></b> come versione.</u></p>

<p>Per forzare l’aggiornameno dei cask residui basta una singola istruzione:</p>

<pre><code class="language-bash">brew upgrade --cask -n --greedy|tail -n +2|grep -v latest|cut -d &#34; &#34; -f 1|xargs brew upgrade --cask
</code></pre>
<ul><li>Aggiorna esplicitamente tutti i cask con autoupdate (esclusi i “latest”)</li></ul>

<p><a href="/aytin/tag:brew" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">brew</span></a> <a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:macos" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">macos</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/ancora-su-brew-upgrade</guid>
      <pubDate>Wed, 01 Mar 2023 06:17:35 +0000</pubDate>
    </item>
    <item>
      <title>Effettuare un brew upgrade completo</title>
      <link>https://noblogo.org/aytin/effettuare-un-brew-upgrade-completo</link>
      <description>&lt;![CDATA[(pubblicato il 25 marzo 2021)&#xA;laptop&#xA;smalliPhoto by olia danilevich on a href=&#34;https://www.pexels.com/it-it/foto/mani-scrivania-laptop-internet-4974912/&#34;Pexels.com/a/i/small&#xA;&#xA;Sul Mac trovo comodo usare brew . Permette di gestire tante applicazioni con un gestore di pacchetti.&#xA;&#xA;Nota positiva: in generale, funziona bene, per quello che lo conosco.&#xA;Nota negativa: in contraddizione col punto precedente, l’upgrade dei cask mi pare faccia cilecca a id=&#34;linknota1&#34; href=&#34;#nota1&#34; title=&#34;vai alla nota 1&#34;strongsup[1]/sup/strong/a&#xA;&#xA;In passato, eseguivo l’update / upgrade delle formule e l’upgrade dei cask.&#xA;!--more--&#xA;Col tempo, la gestione dei cask è diventata più omogenea con quella delle formule ma l’update / upgrade mi rimane ancora un po’ misterioso.&#xA;&#xA;Eseguendo un brew update &amp;&amp; brew upgrade, soprattutto dalle ultime versioni (dalla 3.0.7 in poi di sicuro), l’update sembra fare il suo lavoro e trova un po’ di cask aggiornare.&#xA;&#xA;Càpita però che l’upgrade ne aggiorni qualcuno, un brew upgrade --cask ne aggiorni qualcun altro (senza raggiungere il totale iniziale) e provando a rifare l’update subito dopo (e anche l’upgrade) il sistema rimane intatto come se fosse up-to-date.&#xA;&#xA;Agendo puntualmente sul cask invece, l’aggiornamento parte eccome. Come se fossero rimaste in cache delle informazioni discordanti sullo stato reale dell’applicazione e su quello potenziale.&#xA;&#xA;Ho cercato un po’ in rete, sembra un problema comune.&#xA;&#xA;Al solito, è partito lo script.&#xA;!/usr/bin/bash&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m**************\033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m brew update \033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m***********\033[0m&#34;&#xA;brew update&#xA; &#xA; &#xA;echo -e &#34;\n&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m************\033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m brew upgrade \033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m************\033[0m&#34;&#xA;if [[ -z $(brew upgrade) ]]; then&#xA;    echo -e &#34;Nothing.&#34;&#xA;fi&#xA; &#xA; &#xA;echo -e &#34;\n&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m*******************\033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m brew upgrade --cask \033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m*********************\033[0m&#34;&#xA;for c in $(brew list --cask -1); do&#xA;    printf %-30s &#34;$c&#34;&#xA;    info=$(brew info --cask &#34;$c&#34;)&#xA;     &#xA;    currentver=$(head -n1 &lt;&lt;&lt; &#34;$info&#34; | cut -d &#34; &#34; -f 2)&#xA;    installedver=$(head -n3 &lt;&lt;&lt; &#34;$info&#34; | tail -n1 | cut -d&#34;/&#34; -f 6 | cut -d &#34; &#34; -f 1)&#xA;     &#xA;    if [ &#34;$installedver&#34; != &#34;$currentver&#34; ]; then&#xA;        answer=&#34;&#34;&#xA;        while [[ -z $answer ]]; do&#xA;         &#xA;        read -r -p &#34;$c installed version is &#39;$installedver&#39;, but the current version is &#39;$currentver&#39;. Do you want to upgrade $c? [&#34;$&#39;\e[;32;1mYES\e[0m/no] &#39; answer&#xA; &#xA;        case ${answer,,} in&#xA;            yes|&#34;&#34;)&#xA;                brew upgrade --cask --force $c&#xA;                answer=&#34;yes&#34;&#xA;                ;;&#xA;            no)&#xA;                echo &#34;Installation of $c $currentver skipped&#34;&#xA;                ;;&#xA;            )&#xA;                answer=&#34;&#34;&#xA;                ;;&#xA;            esac&#xA;        done&#xA;    else&#xA;        printf &#34;: \033[38;5;70mis up-to-date (ver. $installedver)\033[0m\n&#34;&#xA;    fi&#xA;done&#xA; &#xA; &#xA;echo -e &#34;\n&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m***************\033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m brew cleanup \033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m************\033[0m&#34;&#xA;if [[ -z $(brew cleanup) ]]; then&#xA;    echo -e &#34;Nothing.&#34;&#xA;fi&#xA; &#xA; &#xA;echo -e &#34;\n&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m***********\033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m brew doctor \033[0m&#34;&#xA;echo -e &#39;\E[37;44m&#39;&#34;\033[1m***********\033[0m&#34;&#xA;brew doctor&#xA;Al di là dello zucchero sintattico, la soluzione è stata brutale e ruvida: un ciclo sulla lista dei cask con il force sull’upgrade (righe 21-50). La cosa ancora più singolare è che in questo modo vengono aggiornati dei cask che non risultavano nemmeno nel primo upgrade.&#xA;&#xA;Sicuramente si potrà fare anche meglio. a id=&#34;linknota2&#34; href=&#34;#nota2&#34; title=&#34;vai alla nota 2&#34;strongsup[2]/sup/strong/a&#xA;&#xA;Ho provato a usare brew livecheck per recuperare più elegantemente le info nelle righe 25-26 ma mi dà qualche problema, ogni tanto va in crash. Ho optato per la scansione secca della lista.&#xA;Non so bene come lavori brew outdated, se soffra degli stessi problemi degli altri comandi (sospetto di sì ma devo verificare).&#xA;In realtà basterebbe una sola riga:&#xA;&#xA;brew list --cask|xargs brew upgrade --cask&#xA;solo che così non riesco a controllare il numero di versione causando un aggiornamento quasi sempre inutile per tutti quei cask che hanno come versione “latest” (come Cisco Webex)&#xA;&#xA;Lo script fa il suo lavoro ma ho trovato anche questa alternativa (decisamente più elegante): https://github.com/buo/homebrew-cask-upgrade&#xA;&#xA;Un comando esterno, brew cu nella sua forma compatta, che sostituisce ed estende le funzionalità di brew upgrade. Avendo aggiornato da poco, non l’ho ancora visto all’opera.&#xA;Edit (31/03/2021):&#xA;&#xA;In realtà, leggendo anche il man e il log dell’applicazione su stdout, ho capito che il funzionamento di brew è corretto.&#xA;&#xA;upgrade e outadated prendono in esame solo i cask che non dispongono di autoupdate.&#xA;&#xA;  ==  Casks with ‘autoupdates’ or ‘version :latest’ will not be upgraded; pass \-\-greedy to upgrade them.&#xA;&#xA;Un atteggiamento prudenziale che è facile comprendere. Laddove sia disponibile una funzionalità di autoupdate, si preferisce che sia l’applicazione stessa a gestire questo aspetto, lasciando a brew upgrade --cask (e ai file .rb di configurazione del cask, di conseguenza) la gestione di tutto il resto.&#xA;&#xA;In questo modo, i cask che possono autoaggiornarsi, possono farlo in autonomia appena possibile, senza dipendere dalla modifica necessaria al file di configurazione che innesca l’aggiornamento.&#xA;&#xA;Volendo bypassare questo comportamento, si può ricorrere all’opzione \-\-greedy che forza l’upgrade sia per le app con autoupdate che per quelle che gestiscono il numero di versione con l’orrendo “latest”&#xA;&#xA;Quindi:&#xA;brew outdated --greedy --cask&#xA;e&#xA;brew upgrade --greedy --cask&#xA;permettono rispettivamente:&#xA;&#xA;di elencare tutti i cask che necessitano di un aggiornamento, compresi quelli con autoupdate e con versione “latest”&#xA;di forzare l’aggiornamento dei cask di cui sopra&#xA;&#xA;Forzando l’aggiornamento in questo modo, si potrà andare incontro ad un disallineamento, tipo:&#xA;&#xA;sul sistema può essere presente una versione più aggiornata di quanto non dica il file di configurazione&#xA;affidandosi all’autoupdate, l’opzione \-\-greedy* continuerà a rilevare comunque un aggiornamento da fare (ecco perché brew preferisce ignorare questo tipo di applicazioni)&#xA;&#xA;small&#xA;strongN.B./strong brew cu funziona benissimo permettendo di gestire separatamente sia l’autoupdate che il latest&#xA;/small&#xA;&#xA;ustrongNote all’articolo/strong/usmall&#xA;&#xA;No, non fa cilecca stronga id=&#34;nota1&#34; title=&#34;torna su&#34; href=&#34;#linknota1&#34;[↵]/a/strong&#xA;Ho fatto fuori il ciclo (e non solo) proprio qui stronga id=&#34;nota2&#34; title=&#34;torna su&#34; href=&#34;#linknota2&#34;[↵]/a/strong&#xA;&#xA;/small&#xA;&#xA;#brew #bash #macos #scripting&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 25 marzo 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/0fca8ea69-e1c06b/qPu12dRqXK5V/RhP4EHFheAGL3fhsyauMOhKchuWociC9TU3SmuoI.jpg" alt="laptop">
<small><i>Photo by olia danilevich on <a href="https://www.pexels.com/it-it/foto/mani-scrivania-laptop-internet-4974912/" rel="nofollow">Pexels.com</a></i></small></p>

<p>Sul Mac trovo comodo usare <strong>brew</strong> . Permette di gestire tante applicazioni con un gestore di pacchetti.</p>
<ul><li><strong>Nota positiva:</strong> in generale, funziona bene, per quello che lo conosco.</li>
<li><strong>Nota negativa:</strong> in contraddizione col punto precedente, l’upgrade dei cask mi pare faccia cilecca <a id="link_nota_1" href="#nota_1" title="vai alla nota 1" rel="nofollow"><strong><sup>[1]</sup></strong></a></li></ul>

<p>In passato, eseguivo l’update / upgrade delle formule e l’upgrade dei cask.

Col tempo, la gestione dei cask è diventata più omogenea con quella delle formule ma l’update / upgrade mi rimane ancora un po’ misterioso.</p>

<p>Eseguendo un <code>brew update &amp;&amp; brew upgrade</code>, soprattutto dalle ultime versioni (dalla 3.0.7 in poi di sicuro), l’update sembra fare il suo lavoro e trova un po’ di cask aggiornare.</p>

<p>Càpita però che l’upgrade ne aggiorni qualcuno, un <code>brew upgrade --cask</code> ne aggiorni qualcun altro (senza raggiungere il totale iniziale) e provando a rifare l’update subito dopo (e anche l’upgrade) il sistema rimane intatto come se fosse up-to-date.</p>

<p>Agendo puntualmente sul cask invece, l’aggiornamento parte eccome. Come se fossero rimaste in cache delle informazioni discordanti sullo stato reale dell’applicazione e su quello potenziale.</p>

<p>Ho cercato un po’ in rete, sembra un problema comune.</p>

<p>Al solito, è partito lo script.</p>

<pre><code class="language-bash">#!/usr/bin/bash
echo -e &#39;\E[37;44m&#39;&#34;\033[1m***************\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m* brew update *\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m***************\033[0m&#34;
brew update
 
 
echo -e &#34;\n&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m****************\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m* brew upgrade *\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m****************\033[0m&#34;
if [[ -z $(brew upgrade) ]]; then
    echo -e &#34;Nothing.&#34;
fi
 
 
echo -e &#34;\n&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m***********************\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m* brew upgrade --cask *\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m***********************\033[0m&#34;
for c in $(brew list --cask -1); do
    printf %-30s &#34;$c&#34;
    info=$(brew info --cask &#34;$c&#34;)
     
    current_ver=$(head -n1 &lt;&lt;&lt; &#34;$info&#34; | cut -d &#34; &#34; -f 2)
    installed_ver=$(head -n3 &lt;&lt;&lt; &#34;$info&#34; | tail -n1 | cut -d&#34;/&#34; -f 6 | cut -d &#34; &#34; -f 1)
     
    if [ &#34;$installed_ver&#34; != &#34;$current_ver&#34; ]; then
        answer=&#34;&#34;
        while [[ -z $answer ]]; do
         
        read -r -p &#34;$c installed version is &#39;$installed_ver&#39;, but the current version is &#39;$current_ver&#39;. Do you want to upgrade $c? [&#34;$&#39;\e[;32;1mYES\e[0m/no] &#39; answer
 
        case ${answer,,} in
            yes|&#34;&#34;)
                brew upgrade --cask --force $c
                answer=&#34;yes&#34;
                ;;
            no)
                echo &#34;Installation of $c $current_ver skipped&#34;
                ;;
            *)
                answer=&#34;&#34;
                ;;
            esac
        done
    else
        printf &#34;: \033[38;5;70mis up-to-date (ver. $installed_ver)\033[0m\n&#34;
    fi
done
 
 
echo -e &#34;\n&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m****************\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m* brew cleanup *\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m****************\033[0m&#34;
if [[ -z $(brew cleanup) ]]; then
    echo -e &#34;Nothing.&#34;
fi
 
 
echo -e &#34;\n&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m***************\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m* brew doctor *\033[0m&#34;
echo -e &#39;\E[37;44m&#39;&#34;\033[1m***************\033[0m&#34;
brew doctor
</code></pre>

<p>Al di là dello zucchero sintattico, la soluzione è stata brutale e ruvida: un ciclo sulla lista dei cask con il force sull’upgrade (righe 21-50). La cosa ancora più singolare è che in questo modo vengono aggiornati dei cask che non risultavano nemmeno nel primo upgrade.</p>

<p>Sicuramente si potrà fare anche meglio. <a id="link_nota_2" href="#nota_2" title="vai alla nota 2" rel="nofollow"><strong><sup>[2]</sup></strong></a></p>
<ol><li>Ho provato a usare <code>brew livecheck</code> per recuperare più elegantemente le info nelle righe 25-26 ma mi dà qualche problema, ogni tanto va in crash. Ho optato per la scansione secca della lista.</li>
<li>Non so bene come lavori <code>brew outdated</code>, se soffra degli stessi problemi degli altri comandi (sospetto di sì ma devo verificare).</li>
<li>In realtà basterebbe una sola riga:</li></ol>

<pre><code class="language-bash">brew list --cask|xargs brew upgrade --cask
</code></pre>

<p>solo che così non riesco a controllare il numero di versione causando un aggiornamento quasi sempre inutile per tutti quei cask che hanno come versione “latest” (come Cisco Webex)</p>

<p>Lo script fa il suo lavoro ma ho trovato anche questa alternativa (decisamente più elegante): <a href="https://github.com/buo/homebrew-cask-upgrade" rel="nofollow">https://github.com/buo/homebrew-cask-upgrade</a></p>

<p>Un comando esterno, <code>brew cu</code> nella sua forma compatta, che sostituisce ed estende le funzionalità di <code>brew upgrade</code>. Avendo aggiornato da poco, non l’ho ancora visto all’opera.</p>

<h2 id="edit-31-03-2021">Edit (31/03/2021):</h2>

<p>In realtà, leggendo anche il man e il log dell’applicazione su stdout, ho capito che il funzionamento di brew è corretto.</p>

<p><em>upgrade</em> e <em>outadated</em> prendono in esame solo i cask che <strong>non dispongono di autoupdate</strong>.</p>

<blockquote><p>==&gt; Casks with ‘auto_updates’ or ‘version :latest’ will not be upgraded; pass --greedy to upgrade them.</p></blockquote>

<p>Un atteggiamento prudenziale che è facile comprendere. Laddove sia disponibile una funzionalità di autoupdate, si preferisce che <strong>sia l’applicazione stessa a gestire questo aspetto</strong>, lasciando a <code>brew upgrade --cask</code> (e ai file .rb di configurazione del cask, di conseguenza) la gestione di tutto il resto.</p>

<p>In questo modo, i cask che possono autoaggiornarsi, possono farlo in autonomia appena possibile, senza dipendere dalla modifica necessaria al file di configurazione che innesca l’aggiornamento.</p>

<p>Volendo bypassare questo comportamento, si può ricorrere all’opzione <em>--greedy</em> che forza l’upgrade sia per le app con autoupdate che per quelle che gestiscono il numero di versione con l’orrendo “latest”</p>

<p>Quindi:</p>

<pre><code class="language-bash">brew outdated --greedy --cask
</code></pre>

<p>e</p>

<pre><code class="language-bash">brew upgrade --greedy --cask
</code></pre>

<p>permettono rispettivamente:</p>
<ul><li>di elencare <strong>tutti</strong> i cask che necessitano di un aggiornamento, compresi quelli con autoupdate e con versione “latest”</li>
<li>di forzare l’aggiornamento dei cask di cui sopra</li></ul>

<p>Forzando l’aggiornamento in questo modo, si potrà andare incontro ad un disallineamento, tipo:</p>
<ul><li>sul sistema può essere presente una versione più aggiornata di quanto non dica il file di configurazione</li>
<li>affidandosi all’autoupdate, l’opzione <em>--greedy</em> continuerà a rilevare comunque un aggiornamento da fare (ecco perché brew preferisce ignorare questo tipo di applicazioni)</li></ul>

<p><small>
<strong>N.B.</strong> <code>brew cu</code> funziona benissimo permettendo di gestire separatamente sia l’autoupdate che il latest
</small></p>

<p><u><strong>Note all’articolo</strong></u><small></p>
<ol><li>No, non fa cilecca <strong><a id="nota_1" title="torna su" href="#link_nota_1" rel="nofollow">[↵]</a></strong></li>
<li>Ho fatto fuori il ciclo (e non solo) proprio qui <strong><a id="nota_2" title="torna su" href="#link_nota_2" rel="nofollow">[↵]</a></strong></li></ol>

<p></small></p>

<p><a href="/aytin/tag:brew" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">brew</span></a> <a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:macos" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">macos</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/effettuare-un-brew-upgrade-completo</guid>
      <pubDate>Tue, 28 Feb 2023 22:16:57 +0000</pubDate>
    </item>
    <item>
      <title>Reload Radio Deejay in bash</title>
      <link>https://noblogo.org/aytin/reload-radio-deejay-in-bash</link>
      <description>&lt;![CDATA[(pubblicato il 22 marzo 2021)&#xA;vecchia radio&#xA;small/iPhoto by Nothing Ahead on a href=&#34;https://www.pexels.com/it-it/foto/shallow-focus-foto-di-una-vecchia-radio-3199028/&#34;Pexels.com/a/i/small&#xA;&#xA;Questo script permette di scaricare le puntate intere di Ciao Belli o DeeJay Time da Deejay Reload perché le ascolto offline praticamente da sempre. In alternativa, è possibile ascoltare direttamente il live streaming delle trasmissioni.&#xA;!--more--&#xA;I pattern che uso per lo scraping sono molto elementari e sono ovviamente sensibili ai minimi cambiamenti che avvengono sulle pagine di Radio Deejay.&#xA;&#xA;La versione attuale è attiva da un paio di mesi a questa parte perché il pattern prima era leggermente diverso. E più si va indietro, più le differenze aumentano. Per dirne una, lo streaming hls è molto recente, Prima di esso, il reload era disponibile solo in mp3. Questa sarà la 15supa/sup versione dello script 🙂&#xA;&#xA;Il download restituisce un file in mp3 o aac. Quest&#39;ultimo ottenuto in maniera rudimentale estrando i singoli segmenti aac ottenuti dall&#39;analisi della playlist hls e ricombinati attraverso ffmpeg.&#xA;&#xA;In linea di principio sarebbe possibile estendere lo script aumentando la scelta delle trasmissioni disponibili e, volendo migliorare l&#39;interazione con l&#39;utente sacrificando l&#39;automatismo e la &#34;scriptabilità&#34;, dotandolo di un menù.&#xA;&#xA;help() {&#xA;    echo &#34;$1&#34;&#xA;    echo&#xA;    echo &#34;Usa come: wgetdeejay.sh YYYYMMDD prg format&#34;&#xA;    echo &#34;Es. wgetdeejay.sh 2019062 cb mp3  [Scarica Ciao Belli in mp3]&#34;&#xA;    echo &#34;    wgetdeejay.sh 2019062 djt aac [Scarica Deejay Time in aac]&#34;&#xA;    echo &#34;    wgetdeejay.sh live            [Deejay Live]&#34;&#xA;}&#xA;&#xA;download() {&#xA;    if [[ ${FMT} == &#34;mp3&#34; || -z ${FMT} ]]; then&#xA;        if ! checkanddownloadmp3; then&#xA;            return 1&#xA;        fi&#xA;    elif [[ ${FMT} == &#34;aac&#34; ]]; then&#xA;        if ! checkaac; then&#xA;            return 1&#xA;        else&#xA;            downloadaac&#xA;        fi&#xA;    else&#xA;        help &#34;Formato non corretto.&#34;&#xA;        return 1&#xA;    fi&#xA;}&#xA;&#xA;checkanddownloadmp3() {&#xA;    #if ! wget -q https://${HOST}/${DATAURL}/episodes/${TITLE}/${DATA}.mp3 -O &#34;${PRG}${DATA}.mp3&#34;; then&#xA;    echo https://${HOST}/${DATAURL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3&#xA;    if ! wget https://${HOST}/${DATAURL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3 -O &#34;${PRG}${DATA}.mp3&#34;; then&#xA;        help &#34;Puntata di ${TITLE} del ${ERRDATA}, non trovata.&#34;&#xA;        return 1&#xA;    fi&#xA;}&#xA;&#xA;checkaac() {&#xA;    #if ! wget -qO- https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-010000/hls-010000.m3u8 1  /dev/null; then&#xA;    echo https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8&#xA;    if ! wget -qO- https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 1  /dev/null; then&#xA;        help &#34;Puntata di ${TITLE} del ${ERRDATA}, non trovata.&#34;&#xA;        return 1&#xA;    else&#xA;        return 0&#xA;    fi&#xA;}&#xA;&#xA;downloadaac() {&#xA;    echo -n &#34;Scarico la traccia............. &#34;&#xA;    ffmpeg -i https://${HOST}/&#34;${DATAURL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 -c copy $HOME/deejay/&#34;${PRG}${DATA}&#34;.aac 2  /dev/null&#xA;    echo &#34;Fatto.&#34;&#xA;&#xA;}&#xA;&#xA;DATA=$1&#xA;PRG=$2&#xA;FMT=$3&#xA;&#xA;if [[ $1 == &#34;live&#34; ]]; then&#xA;    mpv http://radiodeejay-lh.akamaihd.net/i/RadioDeejayLive1@189857/master.m3u8&#xA;else&#xA;    case &#34;${PRG}&#34; in&#xA;        &#34;cb&#34;)&#xA;            HOST=&#34;media.deejay.it&#34;&#xA;            TITLE=&#34;ciaobelli&#34;&#xA;            ;;&#xA;        &#34;djt&#34;)&#xA;            HOST=&#34;media.deejay.it&#34;&#xA;            TITLE=&#34;deejaytime&#34;&#xA;&#xA;            ;;&#xA;        &#34;live&#34;)&#xA;            TITLE=&#34;live&#34;&#xA;            ;;&#xA;        *)&#xA;            help &#34;Programma Deejay mancante o non esistente.&#34;&#xA;            exit 1&#xA;    esac&#xA;&#xA;    DATAURL=&#34;${DATA:0:4}/${DATA:4:2}/${DATA:6:2}&#34;&#xA;    ERR_DATA=&#34;${DATA:6:2}-${DATA:4:2}-${DATA:0:4}&#34;&#xA;&#xA;    if [[ -z ${DATA:0:4} ]]; then help &#34;Manca l&#39;anno.&#34;; exit 1&#xA;    elif [[ -z ${DATA:4:2} ]]; then help &#34;Manca il mese.&#34;; exit 1&#xA;    elif [[ -z ${DATA:6:2} ]]; then help &#34;Manca il giorno.&#34;; exit 1&#xA;    elif ! download; then exit 1&#xA;    fi&#xA;fi&#xA;&#xA;#bash #shell #scripting]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 22 marzo 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/0fca8ea69-e1c06b/gx5yE0mToMHs/pgHaEJhxeXnpa868gj4wz1feQ3PMi6077g1Q1eFd.jpg" alt="vecchia radio">
<small></i>Photo by Nothing Ahead on <a href="https://www.pexels.com/it-it/foto/shallow-focus-foto-di-una-vecchia-radio-3199028/" rel="nofollow">Pexels.com</a></i></small></p>

<p>Questo script permette di scaricare le puntate intere di <strong>Ciao Belli</strong> o <strong>DeeJay Time</strong> da Deejay Reload perché le ascolto offline praticamente da sempre. In alternativa, è possibile ascoltare direttamente il live streaming delle trasmissioni.

I pattern che uso per lo scraping sono molto elementari e sono ovviamente sensibili ai minimi cambiamenti che avvengono sulle pagine di <a href="https://www.deejay.it" rel="nofollow">Radio Deejay</a>.</p>

<p>La versione attuale è attiva da un paio di mesi a questa parte perché il pattern prima era leggermente diverso. E più si va indietro, più le differenze aumentano. Per dirne una, lo <em>streaming hls</em> è molto recente, Prima di esso, il reload era disponibile solo in mp3. Questa sarà la 15<sup>a</sup> versione dello script 🙂</p>

<p>Il download restituisce un file in mp3 o aac. Quest&#39;ultimo ottenuto in maniera rudimentale estrando i singoli segmenti aac ottenuti dall&#39;analisi della playlist hls e ricombinati attraverso ffmpeg.</p>

<p>In linea di principio sarebbe possibile estendere lo script aumentando la scelta delle trasmissioni disponibili e, volendo migliorare l&#39;interazione con l&#39;utente sacrificando l&#39;automatismo e la “scriptabilità”, dotandolo di un menù.</p>

<pre><code class="language-bash">help() {
    echo &#34;$1&#34;
    echo
    echo &#34;Usa come: wget_deejay.sh &lt;YYYYMMDD&gt; &lt;prg&gt; &lt;format&gt;&#34;
    echo &#34;Es. wget_deejay.sh 2019062 cb mp3  [Scarica Ciao Belli in mp3]&#34;
    echo &#34;    wget_deejay.sh 2019062 djt aac [Scarica Deejay Time in aac]&#34;
    echo &#34;    wget_deejay.sh live            [Deejay Live]&#34;
}



download() {
    if [[ ${FMT} == &#34;mp3&#34; || -z ${FMT} ]]; then
        if ! check_and_download_mp3; then
            return 1
        fi
    elif [[ ${FMT} == &#34;aac&#34; ]]; then
        if ! check_aac; then
            return 1
        else
            download_aac
        fi
    else
        help &#34;Formato non corretto.&#34;
        return 1
    fi
}



check_and_download_mp3() {
    #if ! wget -q https://${HOST}/${DATA_URL}/episodes/${TITLE}/${DATA}.mp3 -O &#34;${PRG}_${DATA}.mp3&#34;; then
    echo https://${HOST}/${DATA_URL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3
    if ! wget https://${HOST}/${DATA_URL}/episodes/${TITLE}/${TITLE}-${DATA}.mp3 -O &#34;${PRG}_${DATA}.mp3&#34;; then
        help &#34;Puntata di ${TITLE} del ${ERR_DATA}, non trovata.&#34;
        return 1
    fi
}



check_aac() {
    #if ! wget -qO- https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-010000/hls-010000.m3u8 1&gt;/dev/null; then
    echo https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8
    if ! wget -qO- https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 1&gt;/dev/null; then
        help &#34;Puntata di ${TITLE} del ${ERR_DATA}, non trovata.&#34;
        return 1
    else
        return 0
    fi
}


download_aac() {
    echo -n &#34;Scarico la traccia............. &#34;
    ffmpeg -i https://${HOST}/&#34;${DATA_URL}&#34;/episodes/${TITLE}/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;/hls-&#34;${TITLE}&#34;-&#34;${DATA}&#34;.m3u8 -c copy $HOME/deejay/&#34;${PRG}_${DATA}&#34;.aac 2&gt;/dev/null
    echo &#34;Fatto.&#34;

}


DATA=$1
PRG=$2
FMT=$3


if [[ $1 == &#34;live&#34; ]]; then
    mpv http://radiodeejay-lh.akamaihd.net/i/RadioDeejay_Live_1@189857/master.m3u8
else
    case &#34;${PRG}&#34; in
        &#34;cb&#34;)
            HOST=&#34;media.deejay.it&#34;
            TITLE=&#34;ciao_belli&#34;
            ;;
        &#34;djt&#34;)
            HOST=&#34;media.deejay.it&#34;
            TITLE=&#34;deejay_time&#34;

            ;;
        &#34;live&#34;)
            TITLE=&#34;live&#34;
            ;;
        *)
            help &#34;Programma Deejay mancante o non esistente.&#34;
            exit 1
    esac


    DATA_URL=&#34;${DATA:0:4}/${DATA:4:2}/${DATA:6:2}&#34;
    ERR_DATA=&#34;${DATA:6:2}-${DATA:4:2}-${DATA:0:4}&#34;

    if [[ -z ${DATA:0:4} ]]; then help &#34;Manca l&#39;anno.&#34;; exit 1
    elif [[ -z ${DATA:4:2} ]]; then help &#34;Manca il mese.&#34;; exit 1
    elif [[ -z ${DATA:6:2} ]]; then help &#34;Manca il giorno.&#34;; exit 1
    elif ! download; then exit 1
    fi
fi
</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:shell" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">shell</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/reload-radio-deejay-in-bash</guid>
      <pubDate>Tue, 28 Feb 2023 20:16:39 +0000</pubDate>
    </item>
    <item>
      <title>Generazione OTP via bash</title>
      <link>https://noblogo.org/aytin/generazione-otp-via-bash</link>
      <description>&lt;![CDATA[(pubblicato il 30 gennaio 2021)&#xA;otp&#xA;smalliFonte: a href=&#34;https://www.flaticon.com/free-icons/otp&#34; title=&#34;otp icons&#34;Otp icons created by Chanut-is-Industries - Flaticon/a/i/small&#xA;&#xA;Per buona parte del mio tempo sul pc apro una shell, per cui trovo comodo spostare il meno possibile le mani dalla tastiera. Usando spesso 2FA, ho pensato ad un modo per avere un OTP usando bash.&#xA;!--more--&#xA;Il tool che fa al caso mio è oathtool che, per i miei scopi, è sufficiente invocare in questo modo:&#xA;oathtool --totp -b key&#xA;dove \key\ è una stringa base32 bella lunga.&#xA;&#xA;Chiaramente non voglio doverla scrivere ogni volta, nè voglio lasciarla in chiaro. Occorre che:&#xA;&#xA;la chiave sia cifrata e decifrata solamente quando occorre;&#xA;l&#39;output non arrivi su stdout per non doverlo copiare a mia volta (e lasciarlo comunque visibile)&#xA;&#xA;Al punto 1. si risponde con gpg o openssl.&#xA;&#xA;Al punto 2. si risponde con xsel (Gnu/Linux) o pbcopy (MacOS) che trasferiscono l&#39;output di un comando nella clipboard.&#xA;oathtool --totp -b &lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &lt;filechiavecifrato) | xsel -bi&#xA;E veniamo quindi alla:&#xA;Versione 1&#xA;!/bin/bash&#xA;&#xA;# Path del file cifrato contentente la chiave # supponiamo sia di google&#xA;KEY2FAFILE=&#34;$1&#34;&#xA;&#xA;oathtool --totp -b &lt;&lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &#34;${KEY2FAFILE}&#34;) | xsel -bi&#xA;Non è nemmeno uno script. Potrebbe essere direttamente un alias.&#xA;&#xA;ustrongVANTAGGI/strong/u&#xA;&#xA;Estremamente compatto&#xA;Si sfrutta nativamente l&#39;autocomplete col TAB per i file da dare all&#39;input&#xA;L&#39;univocità dei nomi è garantita dal file system&#xA;&#xA;ustrongSVANTAGGI/strong/u&#xA;&#xA;Crea un file per ogni codice e, alla lunga potrebbe generare confusione&#xA;Necessità di creare una password di cifratura per ogni file (potenzialmente tutte diverse)&#xA;&#xA;Versione 2&#xA;Mi occorre quindi un file nome-valore con separatore. Ad es. il simbolo  &#34;:&#34;&#xA;account1:JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA&#xA;account2:JDNBLASPUQRIEKM89478JMFKOVJDOQKJ&#xA;account3:OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF&#xA;account4:MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ&#xA;    &#xA;    ...&#xA;    &#xA;accountn:IWPQSLNCGK49TOWODIR483IRIWOFOPIQO&#xA;Nello script, la variabile KEYS\2FA\FILE memorizza il full path del file cifrato dei codici, mentre l&#39;account deve essere fornito in input. In caso contrario, viene restituita la lista degli account disponibili.&#xA;&#xA; Se l&#39;account è valido, il file viene decifrato e viene estratto il corrispondente codice che viene memorizzato direttamente nella clipboard.&#xA;!/usr/bin/bash&#xA;&#xA;Path del file cifrato contentente i codici 2FA&#xA;KEYS2FAFILE=&#34;path/keys2fa.gpg&#34;&#xA;&#xA;Acquisisce il nome account&#xA;ACCOUNT=$1&#xA;&#xA;Decifra e carica in ram il contenuto del file cifrato&#xA;KEYS2FAFILEDEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${KEYS2FAFILE}&#34; 2  /dev/null)&#xA;&#xA;Se non c&#39;è nessun argomento, restituisce la lista degli account&#xA;if [[ -z &#34;${ACCOUNT}&#34; ]]; then&#xA;    while read LINE; do&#xA;        cut -d&#34;:&#34; -f 1 &lt;&lt;&lt; ${LINE}&#xA;    done &lt;&lt;&lt; $(echo &#34;${KEYS2FAFILEDEC}&#34;)&#xA;    exit&#xA;else&#xA;    # Estrae l&#39;elemento contenente l&#39;id dato in input&#xA;    KEY=$(echo &#34;${KEYS2FAFILEDEC}&#34; | grep ${ACCOUNT} | cut -d&#34;:&#34; -f 2)&#xA;&#xA;    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard&#xA;    # altrimenti esce con errore&#xA;    if [[ -z &#34;${KEY}&#34; ]]; then&#xA;        echo &#34;Account non esistente&#34;&#xA;        exit 1&#xA;    else&#xA;        # Calcola l&#39;otp e lo trasferisce nella clipboard con pbcopy&#xA;        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi&#xA;        echo &#34;Fatto.&#34;&#xA;    fi&#xA;fi&#xA;ustrongVANTAGGI/strong/u&#xA;&#xA;Unico entry-point per la gestione degli account (un&#39;unica password per la cifratura del file&#xA;Più semplice da allocare&#xA;&#xA;ustrongSVANTAGGI/strong/u&#xA;&#xA;L&#39;univocità degli account deve essere garantita dall&#39;utente.&#xA;Perdita della funzionalità di autocomplete in fase di inserimento dati&#xA;&#xA;Provo ad esplorare anche una struttura differente, più espressiva se vogliamo. Invece di un file nome-valore, uso un json così strutturato:&#xA;&#xA;Versione 3&#xA;{&#xA;  &#34;accounts&#34;: [&#xA;    {&#xA;      &#34;id&#34;: &#34;account1&#34;,&#xA;      &#34;secret&#34;: &#34;JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA&#34;&#xA;    },&#xA;    {&#xA;      &#34;id&#34;: &#34;account2&#34;,&#xA;      &#34;secret&#34;: &#34;JDNBLASPUQRIEKM89478JMFKOVJDOQKJ&#34;&#xA;    },&#xA;    {&#xA;      &#34;id&#34;: &#34;account3&#34;,&#xA;      &#34;secret&#34;: &#34;OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF&#34;&#xA;    },&#xA;    {&#xA;      &#34;id&#34;: &#34;account4&#34;,&#xA;      &#34;secret&#34;: &#34;MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ&#34;&#xA;    },&#xA;    &#xA;    ...&#xA;    &#xA;    {&#xA;      &#34;id&#34;: &#34;accountn&#34;,&#xA;      &#34;secret&#34;: &#34;IWPQSLNCGK49TOWODIR483IRIWOFOPIQO&#34;&#xA;    }&#xA;  ]&#xA;}&#xA;Un array, accounts, di oggetti nome-valore.&#xA;&#xA;Lo script che fa il parsing e l&#39;estrazione non è molto diverso dal precedente.&#xA;!/usr/local/bin/bash&#xA;&#xA;Path del file cifrato contentente i codici 2FA&#xA;JSON2FAFILE=path/json2fa.gpg&#xA;&#xA;Acquisisce il nome account&#xA;ACCOUNT=$1&#xA;&#xA;Carica in ram il contenuto del file json cifrato&#xA;JSON2FAFILEDEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${JSON2FAFILE}&#34; 2  /dev/null)&#xA;&#xA;Se non c&#39;è nessun argomento, restituisce la lista degli account&#xA;- jq -c &#39;.accounts[]&#39; restituisce una lista contenenente tante linee per quanti sono gli elementi.&#xA;- La lista viene data in pasto al ciclo while attraverso l&#39;operatore &lt;&lt;&lt; (here-string)&#xA;- Da ogni linea viene estratto il valore del campo &#34;id&#34; sempre attraverso l&#39;operatore &lt;&lt;&lt;&#xA;&#xA;if [[ -z &#34;${ACCOUNT}&#34; ]]; then&#xA;    while read LINE; do&#xA;        jq -r &#39;.id&#39; &lt;&lt;&lt; ${LINE}&#xA;    done &lt;&lt;&lt; $(echo &#34;${JSON2FAFILEDEC}&#34; | jq -c &#39;.accounts[]&#39;)&#xA;    exit&#xA;else&#xA;    # Estrae dall&#39;array l&#39;elemento contenente l&#39;id dato in input&#xA;    KEY=$(jq -r --arg ID ${ACCOUNT} &#39;.accounts[] | select(.id | contains($ID)).secret&#39; &lt;&lt;&lt; ${JSON2FAFILE_DEC})&#xA;&#xA;    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard&#xA;    # altrimenti esce con errore&#xA;    if [[ -z &#34;${KEY}&#34; ]]; then&#xA;        echo &#34;Account non esistente&#34;&#xA;        exit 1&#xA;    else&#xA;        # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel&#xA;        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi&#xA;        echo &#34;Fatto.&#34;&#xA;    fi&#xA;fi&#xA;#bash #otp #scripting #shell ]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 30 gennaio 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/0fca8ea69-e1c06b/Jm77gCXX9v4y/7IVttwKSmhThLBGuFctXh2BMZau1cgRi2Grgjhmh.jpg" alt="otp">
<small><i>Fonte: <a href="https://www.flaticon.com/free-icons/otp" title="otp icons" rel="nofollow">Otp icons created by Chanut-is-Industries – Flaticon</a></i></small></p>

<p>Per buona parte del mio tempo sul pc <a href="https://noblogo.org/aytin/merge-delle-history-in-tempo-reale" rel="nofollow">apro una shell</a>, per cui trovo comodo spostare il meno possibile le mani dalla tastiera. Usando spesso 2FA, ho pensato ad un modo per avere un OTP usando bash.

Il tool che fa al caso mio è <a href="http://www.nongnu.org/oath-toolkit/oathtool.1.html" rel="nofollow">oathtool</a> che, per i miei scopi, è sufficiente invocare in questo modo:</p>

<pre><code class="language-bash">oathtool --totp -b &lt;key&gt;
</code></pre>

<p>dove <em>&lt;key&gt;</em> è una stringa base32 bella lunga.</p>

<p>Chiaramente non voglio doverla scrivere ogni volta, nè voglio lasciarla in chiaro. Occorre che:</p>
<ol><li>la chiave sia cifrata e decifrata solamente quando occorre;</li>
<li>l&#39;output non arrivi su stdout per non doverlo copiare a mia volta (e lasciarlo comunque visibile)</li></ol>

<p>Al punto 1. si risponde con <a href="https://www.gnupg.org" rel="nofollow">gpg</a> o <a href="https://www.openssl.org" rel="nofollow">openssl</a>.</p>

<p>Al punto 2. si risponde con <strong>xsel</strong> (Gnu/Linux) o <strong>pbcopy</strong> (MacOS) che trasferiscono l&#39;output di un comando nella clipboard.</p>

<pre><code class="language-bash">oathtool --totp -b &lt;&lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &lt;file_chiave_cifrato&gt;) | xsel -bi
</code></pre>

<p>E veniamo quindi alla:</p>

<h2 id="versione-1">Versione 1</h2>

<pre><code class="language-bash">#!/bin/bash

# Path del file cifrato contentente la chiave # supponiamo sia di google
KEY_2FA_FILE=&#34;$1&#34;

oathtool --totp -b &lt;&lt;&lt; $(gpg -q --decrypt --pinentry-mode loopback &#34;${KEY_2FA_FILE}&#34;) | xsel -bi
</code></pre>

<p>Non è nemmeno uno script. Potrebbe essere direttamente un alias.</p>

<p><u><strong>VANTAGGI</strong></u></p>
<ul><li>Estremamente compatto</li>
<li>Si sfrutta nativamente l&#39;autocomplete col TAB per i file da dare all&#39;input</li>
<li>L&#39;univocità dei nomi è garantita dal file system</li></ul>

<p><u><strong>SVANTAGGI</strong></u></p>
<ul><li>Crea un file per ogni codice e, alla lunga potrebbe generare confusione</li>
<li>Necessità di creare una password di cifratura per ogni file (potenzialmente tutte diverse)</li></ul>

<h2 id="versione-2">Versione 2</h2>

<p>Mi occorre quindi un file nome-valore con separatore. Ad es. il simbolo  “:”</p>

<pre><code>account_1:JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA
account_2:JDNBLASPUQRIEKM89478JMFKOVJDOQKJ
account_3:OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF
account_4:MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ
    
    ...
    
account_n:IWPQSLNCGK49TOWODIR483IRIWOFOPIQO
</code></pre>

<p>Nello script, la variabile <strong>KEYS_2FA_FILE</strong> memorizza il full path del file cifrato dei codici, mentre l&#39;account deve essere fornito in input. In caso contrario, viene restituita la lista degli account disponibili.</p>

<p> Se l&#39;account è valido, il file viene decifrato e viene estratto il corrispondente codice che viene memorizzato direttamente nella clipboard.</p>

<pre><code class="language-bash">#!/usr/bin/bash

# Path del file cifrato contentente i codici 2FA
KEYS_2FA_FILE=&#34;&lt;path&gt;/keys_2fa.gpg&#34;

# Acquisisce il nome account
ACCOUNT=$1

# Decifra e carica in ram il contenuto del file cifrato
KEYS_2FA_FILE_DEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${KEYS_2FA_FILE}&#34; 2&gt;/dev/null)

# Se non c&#39;è nessun argomento, restituisce la lista degli account
if [[ -z &#34;${ACCOUNT}&#34; ]]; then
    while read LINE; do
        cut -d&#34;:&#34; -f 1 &lt;&lt;&lt; ${LINE}
    done &lt;&lt;&lt; $(echo &#34;${KEYS_2FA_FILE_DEC}&#34;)
    exit
else
    # Estrae l&#39;elemento contenente l&#39;id dato in input
    KEY=$(echo &#34;${KEYS_2FA_FILE_DEC}&#34; | grep ${ACCOUNT} | cut -d&#34;:&#34; -f 2)

    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard
    # altrimenti esce con errore
    if [[ -z &#34;${KEY}&#34; ]]; then
        echo &#34;Account non esistente&#34;
        exit 1
    else
        # Calcola l&#39;otp e lo trasferisce nella clipboard con pbcopy
        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi
        echo &#34;Fatto.&#34;
    fi
fi
</code></pre>

<p><u><strong>VANTAGGI</strong></u></p>
<ul><li>Unico entry-point per la gestione degli account (un&#39;unica password per la cifratura del file</li>
<li>Più semplice da allocare</li></ul>

<p><u><strong>SVANTAGGI</strong></u></p>
<ul><li>L&#39;univocità degli account deve essere garantita dall&#39;utente.</li>
<li>Perdita della funzionalità di autocomplete in fase di inserimento dati</li></ul>

<p>Provo ad esplorare anche una struttura differente, più espressiva se vogliamo. Invece di un file nome-valore, uso un json così strutturato:</p>

<h2 id="versione-3">Versione 3</h2>

<pre><code>{
  &#34;accounts&#34;: [
    {
      &#34;id&#34;: &#34;account_1&#34;,
      &#34;secret&#34;: &#34;JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA&#34;
    },
    {
      &#34;id&#34;: &#34;account_2&#34;,
      &#34;secret&#34;: &#34;JDNBLASPUQRIEKM89478JMFKOVJDOQKJ&#34;
    },
    {
      &#34;id&#34;: &#34;account_3&#34;,
      &#34;secret&#34;: &#34;OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF&#34;
    },
    {
      &#34;id&#34;: &#34;account_4&#34;,
      &#34;secret&#34;: &#34;MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ&#34;
    },
    
    ...
    
    {
      &#34;id&#34;: &#34;account_n&#34;,
      &#34;secret&#34;: &#34;IWPQSLNCGK49TOWODIR483IRIWOFOPIQO&#34;
    }
  ]
}
</code></pre>

<p>Un array, <em>accounts</em>, di oggetti nome-valore.</p>

<p>Lo script che fa il parsing e l&#39;estrazione non è molto diverso dal precedente.</p>

<pre><code class="language-bash">#!/usr/local/bin/bash

# Path del file cifrato contentente i codici 2FA
JSON_2FA_FILE=&lt;path&gt;/json_2fa.gpg

# Acquisisce il nome account
ACCOUNT=$1

# Carica in ram il contenuto del file json cifrato
JSON_2FA_FILE_DEC=$(gpg --decrypt  --pinentry-mode loopback  &#34;${JSON_2FA_FILE}&#34; 2&gt;/dev/null)

# Se non c&#39;è nessun argomento, restituisce la lista degli account
# - jq -c &#39;.accounts[]&#39; restituisce una lista contenenente tante linee per quanti sono gli elementi.
# - La lista viene data in pasto al ciclo while attraverso l&#39;operatore &lt;&lt;&lt; (here-string)
# - Da ogni linea viene estratto il valore del campo &#34;id&#34; sempre attraverso l&#39;operatore &lt;&lt;&lt;

if [[ -z &#34;${ACCOUNT}&#34; ]]; then
    while read LINE; do
        jq -r &#39;.id&#39; &lt;&lt;&lt; ${LINE}
    done &lt;&lt;&lt; $(echo &#34;${JSON_2FA_FILE_DEC}&#34; | jq -c &#39;.accounts[]&#39;)
    exit
else
    # Estrae dall&#39;array l&#39;elemento contenente l&#39;id dato in input
    KEY=$(jq -r --arg ID ${ACCOUNT} &#39;.accounts[] | select(.id | contains($ID)).secret&#39; &lt;&lt;&lt; ${JSON_2FA_FILE_DEC})

    # Se la chiave esiste, restituisce l&#39;otp direttamente nella clipboard
    # altrimenti esce con errore
    if [[ -z &#34;${KEY}&#34; ]]; then
        echo &#34;Account non esistente&#34;
        exit 1
    else
        # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel
        oathtool --totp -b &#34;${KEY}&#34; | xsel -bi
        echo &#34;Fatto.&#34;
    fi
fi
</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:otp" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">otp</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a> <a href="/aytin/tag:shell" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">shell</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/generazione-otp-via-bash</guid>
      <pubDate>Tue, 28 Feb 2023 07:59:24 +0000</pubDate>
    </item>
    <item>
      <title>Merge delle history in tempo reale</title>
      <link>https://noblogo.org/aytin/merge-delle-history-in-tempo-reale</link>
      <description>&lt;![CDATA[(pubblicato il 27 gennaio 2021)&#xA;coding&#xA;smallia href=https://www.rawpixel.com/image/432709/free-photo-image-coding-cyber-softwareCoding on a computer screen/a by a href=https://www.rawpixel.com/markusspiskeMarkus Spiske/a is licensed under a href=https://creativecommons.org/publicdomain/zero/1.0/CC-CC0 1.0/a/i/small&#xA;1. Introduzione&#xA;&#xA;Quando si eseguono varie istanze di shell bash (zsh mi pare l&#39;abbia già) nasce l&#39;esigenza di disporre di una history unificata per non correre il rischio che comandi digitati su una certa istanza non siano più disponibili per un riutilizzo futuro.&#xA;!--more--&#xA;In alcune configurazioni, le history si sincronizzano quando si chiude esplicitamente la sessione con &#34;exit&#34; o con CTRL-D ma il vantaggio vero sarebbe quello di disporre del merge da subito, senza aspettare di chiudere la sessione.&#xA;Anche perché, se per errore, l&#39;emulatore di terminale viene chiuso senza prima aver chiuso le sessioni (crash o anche terminando esplicitamente l&#39;applicazione se questo non coincide con il termine della sessione), le history non saranno più disponibili.&#xA;&#xA;Il merge delle history, in sè, è banale. C&#39;è bisogno di un pizzico di magia del comando history stesso e di un minimo di conoscenza della cosidetta &#34;shell&#34;.&#xA;&#xA;ubNota a margine:/b/u Su Gnu/Linux, tutto ciò è applicabile senza eccezioni. Su Mac OS uso bash 5 fornito da homebrew definendo un nome custom per il file history (HISTFILE in .bashrc) che altrimenti viene resettato impietosamente dopo non più di 500 linee, ogni tot riavvi e dopo tot tempo. Le ho provate tutte per andare oltre questo vincolo (sia modificando .bashrc e .profile sia usando alcune chicche specifiche del Mac che però hanno fallito tutte miseramente) e l&#39;unica cosa che ha funzionato è stato il nome custom.&#xA;2. La ricettina&#xA;&#xA;La shell&#xA;configurazione history&#xA;creazione script&#xA;&#xA;1. La shell (Spiegone iniziale)&#xA;&#xA;In un&#39;istanza di shell i comandi di uno script vengono solitamente eseguiti come sottoprocesso del processo genitore.&#xA;Uno stesso comando può essere invocato esternamente o essere implementato direttamente dalla shell (incorporato o builtin).&#xA;I comandi implementati nativamente possono essere più efficienti degli equivalenti esterni perché questi ultimi richiedono la generazione di un processo figlio per essere eseguiti. Inoltre un comando builtin può accedere a parti interne della shell.&#xA;D&#39;altro canto, l&#39;esecuzione di comandi esterni, col loro meccanismo di sottoprocesso, rende più semplice il ricorso ad un certo grado di parallelismo. Piuttosto che eseguire sequenzialmente n comandi interni, potrei decidere di ricorrere agli equivalenti esterni meno efficienti ma in grado di sfruttare il parallelismo dei sottoprocessi e lasciare al sistema operativo la gestione di questo aspetto.&#xA;&#xA;Il merge delle history viene ottenuto sfruttando questi concetti.&#xA;&#xA;Più facile a farsi che a dirsi:&#xA;&#xA;si scrive una funzione history (che sarà quella invocata dalla shell come se fosse un alias)&#xA;nel corpo della funzione history, si usa opportunamente il builtin history che effettuerà il merge vero e proprio&#xA;particolare attenzione verrà data all&#39;innesco di questa invocazione&#xA;&#xA;2. Configurazione history&#xA;&#xA;In ogni istanza di shell:&#xA;&#xA;si appende la sessione corrente al file history (history -a)&#xA;si cancella la history attualmente in sessione (history -c)&#xA;si ripristina la history in sessione dal file history (history -r)&#xA;si riscrive history come funzione in modo che possa essere invocata dall&#39;utente in maniera trasparente, eseguendo il sync e richiamando il builtin history (volendo scomodare impropriamente i design pattern, tutto ciò potrebbe ricordare l&#39;adapter per es.)&#xA;&#xA;Queste due funzioni andranno collocate in .bashrc (o .profile o .bash\profile, dipende dalla vostra configurazione) e richiamate attraverso il PROMPTCOMMAND in modo che, prima di ogni visualizzazione del prompt, si esegua il merge come volevamo&#xA;3. creazione script&#xA;&#xA;In .bashrc:&#xA;...&#xA;history(){&#xA;  syncHistory&#xA;  builtin history &#34;$@&#34;&#xA;}&#xA; &#xA;syncHistory(){&#xA;  builtin history -a&#xA;  HISTFILESIZE=$HISTFILESIZE&#xA;  builtin history -c&#xA;  builtin history -r&#xA;}&#xA;...&#xA;PROMPTCOMMAND=syncHistory&#xA;Appendice&#xA;&#xA;terminale vs. shell vs. emulatore di terminale&#xA;Quando ci si vuole sentire fighi e veri hacker, confessando con un falso senso di colpa e una strizzatina d&#39;occhio, &#34;di lavorare solo aprendo delle shell&#34;, commettiamo due piccole leggerezze:&#xA;&#xA;si dice una sciocchezza&#xA;se la contrazione intrinseca nella frase &#34;aprire una shell&#34; non è voluta, allora non si ha nessuna idea di quello che si sta facendo.&#xA;&#xA;La shell è un interprete di comandi (bash, dash, ksh, csh, zsh ecc.) e basta. Come può esserlo python per es. Mai sentito dire: &#34;ora apro un bel python&#34;, al di là del senso vagamente inquietante della frase.&#xA;Un terminale reale è una console collegata ad uno pseudo-dispositivo tty, collegato a sua volta fisicamente ad una porta seriale.&#xA;Un terminale virtuale in ambienti *nix è una console collegata ad uno pseudo-dispositivo tty virtuale (note anche come &#34;console tty&#34;) che permette di eseguire una shell all&#39;interno di una sessione X. Di solito accessibili con CTRL-ALT-Fn (per n=1..7 ma comunque configurabile).&#xA;Infine, quando si &#34;apre una shell&#34;, quello che si fa in realtà al 99,9999%, è aprire un emulatore di terminale (xterm, Gnome Terminal, Konsole, Iterm2, PuTTY, ecc.), ossia un terminale virtuale all&#39;interno di un ambiente grafico, su cui eseguire una shell come per i terminali reali/virtuali.&#xA;&#xA;Ulteriore suddivisione può essere operata considerando la tipologia di sessioni aperte e la possibilità o meno di disporre di un accesso al sistema.&#xA;&#xA;Il terminale virtuale esegue una shell interattiva con login (o shell di login). È ciò che avviene quando non si dispone di un login manager come KDM, GDM, LightDM ecc. che sono shell di login con GUI.&#xA;L&#39;emulatore di terminale permette di eseguire invece una shell interattiva senza login.&#xA;L&#39;esecuzione di uno script invece eseguirà una shell non interattiva.&#xA;&#xA;Quindi &#34;eseguo&#34; una shell, la &#34;uso&#34;. Ma non la &#34;apro&#34;, pur essendo una &#34;conchiglia&#34;.&#xA;&#xA;#bash #shell]]&gt;</description>
      <content:encoded><![CDATA[<p><strong><em>(pubblicato il 27 gennaio 2021)</em></strong>
<img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/0fca8ea69-e1c06b/yTigNa1AvcvS/wAu9UAMi60jwJxOVuuQHAbPfOdmMBsQqST0NQMHC.jpg" alt="coding">
<small><i><a href="https://www.rawpixel.com/image/432709/free-photo-image-coding-cyber-software" rel="nofollow">Coding on a computer screen</a> by <a href="https://www.rawpixel.com/markusspiske" rel="nofollow">Markus Spiske</a> is licensed under <a href="https://creativecommons.org/publicdomain/zero/1.0/" rel="nofollow">CC-CC0 1.0</a></i></small></p>

<h2 id="1-introduzione">1. Introduzione</h2>

<p>Quando si eseguono varie istanze di shell bash (zsh mi pare l&#39;abbia già) nasce l&#39;esigenza di disporre di una <strong>history unificata</strong> per non correre il rischio che comandi digitati su una certa istanza non siano più disponibili per un riutilizzo futuro.

In alcune configurazioni, le history si sincronizzano quando si chiude esplicitamente la sessione con “exit” o con CTRL-D ma il vantaggio vero sarebbe quello di disporre del merge da subito, senza aspettare di chiudere la sessione.
Anche perché, se per errore, l&#39;emulatore di terminale viene chiuso senza prima aver chiuso le sessioni (crash o anche terminando esplicitamente l&#39;applicazione se questo non coincide con il termine della sessione), le history non saranno più disponibili.</p>

<p>Il merge delle history, in sè, è banale. C&#39;è bisogno di un pizzico di magia del comando <em>history</em> stesso e di un minimo di conoscenza della cosidetta “shell”.</p>

<p><u><b>Nota a margine:</b></u> Su Gnu/Linux, tutto ciò è applicabile senza eccezioni. Su Mac OS uso bash 5 fornito da homebrew definendo un nome custom per il file history (<em>HISTFILE</em> in .bashrc) che altrimenti viene resettato impietosamente dopo non più di 500 linee, ogni tot riavvi e dopo tot tempo. Le ho provate tutte per andare oltre questo vincolo (sia modificando .bashrc e .profile sia usando alcune chicche specifiche del Mac che però hanno fallito tutte miseramente) e l&#39;unica cosa che ha funzionato è stato il nome custom.</p>

<h2 id="2-la-ricettina">2. La ricettina</h2>
<ol><li>La shell</li>
<li>configurazione history</li>
<li>creazione script</li></ol>

<h3 id="1-la-shell-spiegone-iniziale">1. La shell (Spiegone iniziale)</h3>

<p>In un&#39;istanza di shell i comandi di uno script vengono solitamente eseguiti come sottoprocesso del processo genitore.
Uno stesso comando può essere invocato esternamente o essere implementato direttamente dalla shell (incorporato o <strong>builtin</strong>).
I comandi implementati nativamente possono essere più efficienti degli equivalenti esterni perché questi ultimi richiedono la generazione di un processo figlio per essere eseguiti. Inoltre un comando builtin può accedere a parti interne della shell.
D&#39;altro canto, l&#39;esecuzione di comandi esterni, col loro meccanismo di sottoprocesso, rende più semplice il ricorso ad un certo grado di parallelismo. Piuttosto che eseguire sequenzialmente <em>n</em> comandi interni, potrei decidere di ricorrere agli equivalenti esterni meno efficienti ma in grado di sfruttare il parallelismo dei sottoprocessi e lasciare al sistema operativo la gestione di questo aspetto.</p>

<p>Il merge delle history viene ottenuto sfruttando questi concetti.</p>

<p>Più facile a farsi che a dirsi:</p>
<ol><li>si scrive una funzione <em>history</em> (che sarà quella invocata dalla shell come se fosse un alias)</li>
<li>nel corpo della funzione <em>history</em>, si usa opportunamente il builtin <strong>history</strong> che effettuerà il merge vero e proprio</li>
<li>particolare attenzione verrà data all&#39;innesco di questa invocazione</li></ol>

<h3 id="2-configurazione-history">2. Configurazione history</h3>

<p>In ogni istanza di shell:</p>
<ol><li>si appende la sessione corrente al file history (<code>history -a</code>)</li>
<li>si cancella la history attualmente in sessione (<code>history -c</code>)</li>
<li>si ripristina la history in sessione dal file history (<code>history -r</code>)</li>
<li>si riscrive <code>history</code> come funzione in modo che possa essere invocata dall&#39;utente in maniera trasparente, eseguendo il sync e richiamando il builtin <em>history</em> (volendo scomodare impropriamente i design pattern, tutto ciò potrebbe ricordare l&#39;adapter per es.)</li></ol>

<p>Queste due funzioni andranno collocate in <em>.bashrc</em> (o <em>.profile</em> o <em>.bash_profile</em>, dipende dalla vostra configurazione) e richiamate attraverso il PROMPT_COMMAND in modo che, prima di ogni visualizzazione del prompt, si esegua il merge come volevamo</p>

<h3 id="3-creazione-script">3. creazione script</h3>

<p>In .bashrc:</p>

<pre><code class="language-bash">...
history(){
  syncHistory
  builtin history &#34;$@&#34;
}
 
syncHistory(){
  builtin history -a
  HISTFILESIZE=$HISTFILESIZE
  builtin history -c
  builtin history -r
}
...
PROMPT_COMMAND=syncHistory
</code></pre>

<h2 id="appendice">Appendice</h2>

<p><strong>terminale vs. shell vs. emulatore di terminale</strong>
Quando ci si vuole sentire fighi e veri hacker, confessando con un falso senso di colpa e una strizzatina d&#39;occhio, “di lavorare solo aprendo delle shell”, commettiamo due piccole leggerezze:</p>
<ul><li>si dice una sciocchezza</li>
<li>se la contrazione intrinseca nella frase “aprire una shell” non è voluta, allora non si ha nessuna idea di quello che si sta facendo.</li></ul>
<ol><li>La shell è <strong>un interprete di comandi</strong> (bash, dash, ksh, csh, zsh ecc.) e basta. Come può esserlo python per es. Mai sentito dire: “ora apro un bel python”, al di là del senso vagamente inquietante della frase.</li>
<li>Un <strong>terminale reale</strong> è una console collegata ad uno pseudo-dispositivo tty, collegato a sua volta fisicamente ad una porta seriale.</li>
<li>Un <strong>terminale virtuale</strong> in ambienti *nix è una console collegata ad uno pseudo-dispositivo tty <strong>virtuale</strong> (note anche come “console tty”) che permette di <strong>eseguire</strong> una shell all&#39;interno di una sessione X. Di solito accessibili con CTRL-ALT-Fn (per n=1..7 ma comunque configurabile).</li>
<li>Infine, quando si “apre una shell”, quello che si fa in realtà al 99,9999%, è aprire <strong>un emulatore di terminale</strong> (xterm, Gnome Terminal, Konsole, Iterm2, PuTTY, ecc.), ossia un <strong>terminale virtuale all&#39;interno di un ambiente grafico</strong>, su cui eseguire una shell come per i terminali reali/virtuali.</li></ol>

<p>Ulteriore suddivisione può essere operata considerando la tipologia di sessioni aperte e la possibilità o meno di disporre di un accesso al sistema.</p>
<ul><li>Il terminale virtuale esegue una <strong>shell interattiva con login (o shell di login)</strong>. È ciò che avviene quando non si dispone di un login manager come KDM, GDM, LightDM ecc. che sono shell di login con GUI.</li>
<li>L&#39;emulatore di terminale permette di eseguire invece una <strong>shell interattiva senza login</strong>.</li>
<li>L&#39;esecuzione di uno script invece eseguirà una <strong>shell non interattiva.</strong></li></ul>

<p>Quindi “eseguo” una shell, la “uso”. Ma non la “apro”, pur essendo una “conchiglia”.</p>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:shell" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">shell</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/merge-delle-history-in-tempo-reale</guid>
      <pubDate>Mon, 27 Feb 2023 22:37:09 +0000</pubDate>
    </item>
  </channel>
</rss>