Ht-Manager - etwas Komfort beim Bearbeiten von htpasswd und htgroup

Donnerstag, 14. August, 2025

Um ein Web oder ein Verzeichnis mit Username und Passwort zu schützen, kann man mit Basic Authentication arbeiten. Man kann Benutzer oder Gruppen berechtigen, eine Seite zu öffnen.
Dazu hatte ich kürzlich 2 PHP Klassen zum Verwalten von htpasswd- und htgroup-Dateien veröffentlicht. Diese kann man in Programmierprojekten einsetzen.

Nun habe ich mit jenen Klassen ein Konsolen-Werkzeug in PHP geschrieben, um Benutzer und Gruppen auf etwas bequemere Art und interaktiv im Terminal zu verwalten. Das Tool benötigt keinerlei fremde Werkzeuge, wie openssl oder htpasswd, um die Benutzerpasswörter zu erstellen.

Und so sieht das Ganze aus:

2025-08-14-screenshot-htman-01.png

Mit -h oder –help sieht man die unterstützten Parameter. Man wählt mit –folder ein Verzeichnis (Default ist das das aktuelle Verzeichnis) und darin wird eine Datei mit Benutzern und eine für Gruppen verwaltet.

Es gibt ein wenig Comfort.

  • Beim Löschen von Benutzern wird gewarnt, wenn dieser in Gruppen ist.
  • Bei der Eingabe von Benutzernamen oder Gruppen gibt es eine Vervollständigung: mit der Tab-Taste kommt man schneller ans Ziel.
  • Das Auflisten von Benutzern zeigt deren Gruppenzugehörigkit an - das Listing der Gruppen die Mitglieder.
  • Eine Check-Funktion zeigt nicht existierende Gruppenmitglieder, User ohne Gruppen.

Dieses lässt sich mit SPC zu einem Binary compilieren. Für Linux werden vorcompilierte 64-Bit Binaries bereitgestellt. Im Projekt-Quellcode ist ein ./compiler/ Verzeichnis in dem der Download und Installation von SPC und das Compilieren zur ausfühtbaren Datei mitgeliefert wird.

Euer Hosting oder lokaler Rechner läuft mit Linux? Dann könnt ihr das Binary dorthin kopieren - nach Möglichkeit in ein Verzeichnis von $PATH. Und (nach SSH-Login) in der Konsole htpasswd- und htgroup-Dateien bearbeiten.

Euer Rechner/ Hosting hat PHP bereits an Bord? Dann kann man auch die gemergte PHP-Datei auf das Zielsystem kopieren und jenes auf der Konsole starten (per oder aber php )

Das Tool ist Freie Software unter GNU GPL 3 und kann kostenlos eingesetzt werden.

Weiterführende Links:

Meine PHP-Klasse zum Verwalten von .htpasswd und .htgroup Dateien

Donnerstag, 24. Juli, 2025

Ich habe so diverse Applikationen, die einen Passwortzugang für ein Backend benötigen.

Für so kleinere Sachen reicht eine .htpasswd-Datei und Basic Authentication. Um diese per PHP zu erzeugen und zu bearbeiten, habe ich keine geeignete Klassen gefunden … ergo schrieb ich sie mal selbst :-)

Das Projekt besteht aus je einer Klasse für die Verwaltung einer .htpasswd Datei und einer für die .htgroups. Sie können diese Dateien auslesen, Gruppen oder User hinzufügen oder aber löschen. Mit diesen PHP-Klassen kann man eine Benutzerverwaltung mit reinen PHP-Mitteln und ohne ein exec() der htpasswd Datei umsetzen.

Die Dokumentation enhält zudem Konfigurationsbeispiele verschiedener Szenarien für .htaccess Dateien.

Ich hoffe, der ein oder andere findet diese Klassen nützlich.

Weiterführende Links:

IML Artikel: IT-Monitoring mit “Freier Software” ist online

Montag, 21. Juli, 2025

Es hat 1.5 Jahre gebraucht vom ersten Entwurf bis zur Veröffentlichung. Scheinbar ist es komplizierter, als gedacht. Aber nun ist es vollbracht: der Artikel ist online.

2025-07-21-screenshot_iml-website-monitoring.png

Ich bin seit 12 Jahren am IML angestellt und erstmals sind Worte wie “Open source” oder “Freie Software” in einem Artikel unserer Instituts-Webseite aufgetaucht. Und das, obwohl seit über 10 Jahren in der Server-IT ausschliesslich Linux und Freie Software zum Einsatz kommt.

Was braucht es, um im Blick zu halten, ob die Infrastruktur rund läuft? Wir verwenden mehrere Monitoring-Werkzeuge für

  • Logging
  • Cronjobs
  • System-Monitoring und
  • Applikations-Monitoring

Das System-Monitoring Icinga2 ist unser zentraler Sammelpunkt, bei der Störungsinformationen zusammenfliessen - so hat man alles im Blick, ohne zwischen verschiedenen Fenstern (Tabs) hin- und herspringen zu müssen.

Wir setzen auf Freie Software und stellen im Umkehrzug Eigenentwicklungen als Freie Software zur Verfügung.

Am konkreten Beispiel unseres IT Monitorings wird das gelebte Prinzip von Nehmen und Geben beleuchtet.

Link: IML Webseite: IT-Monitoring mit “Freier Software”

JS-Snippet: Http-Request

Mittwoch, 23. April, 2025

Hier ein weiterer Javascript-Schnipsel…

Manchmal möchte ich in einer webbasierten Admin eine Schaltfläche haben, die etwas löscht oder eine Aktion auslöst, die aber nicht mit einem GET Request ausgelöst werden können soll. Also mein A-Tag oder Button-Tag soll beispielsweise einen POST request auslösen und Parameter mitgeben können. Aber auch PUT oder DELETE Requests sind möglich.

Die nachfolgende Funktion führt einen Http-Request aus und zeigt den Output in einer DOM-ID oder aber bei Fehlen der ID im gesamten Browser-Fenster. Wobei Letzteres noch den schönen Effekt hat, dass ein F5 im Browser-Fenster den POST request nicht wiederholt.

/**
 * Make an http request with given method, url, form data.
 * The response can be shown in a given dom id or in the full browser window.
 * 
 * @param {string}  method  http method; eg. GET, POST, PUT, ...
 * @param {string}  url     url to request
 * @param {json}    data    request body as key -> value in a JSON
 * @param {string}  idOut   optional: id of output element in DOM; default: write response in browser
 * @return void
 */
async function httprequest(method="GET", url = "", data = {}, idOut = null) {

    // console.log("httprequest("+method+", "+url+", "+data+", "+idOut+")");

    if (method == "POST" || method == "PUT") {
        var fd = new FormData();
        for (var key in data) {
            fd.append(key, data[key]);
        }
    } else {
        var fd = null;
    }

    // Default options are marked with *
    const response = await fetch(url, {
        method: method, // *GET, POST, PUT, DELETE, etc.
        mode: "cors", // no-cors, *cors, same-origin
        cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
        credentials: "same-origin", // include, *same-origin, omit
        headers: {
            // "Content-Type": "application/json",
            // 'Content-Type': 'application/x-www-form-urlencoded',
            // 'Content-Type': 'multipart/form-data',
        },
        redirect: "follow", // manual, *follow, error
        referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
        body: fd, // body data type must match "Content-Type" header
    });
    // return response.json(); // parses JSON response into native JavaScript objects

    var responsebody = await response.text();
    if (idOut) {
        document.getElementById(idOut).innerHTML = responsebody;
    } else {
        document.open();
        document.write(responsebody);
        document.close();
    }

}

Und so wird sie verwendet:

Nachfolgendes HTML-Snippet zeigt einen Button. Beim onclick-Event sehen wir die obige Funktion einmal eingebunden.
Man sieht

  • die Http-Methode: POST
  • die aufzurufende URL: die aktuelle - mittels location.href
  • POST-Parameter: als JSON wird die Variable “action” mit dem Wert “backup” übergeben. Mit PHP liesse sich dies mit $_POST['’action’] lesen.
<button 
  class="btn"
  onclick="httprequest('POST', location.href , {'action': 'backup'})" 
>Create new backup</button>

Die Funktion ist noch nicht featurecomplete. Sie kann noch erweitert werden, um Http-Request-Header mit zu übergeben. Aber ich hoffe, für den ein oder anderen ist sie bereits in dieser Form nützlich.

Weiterführende Links:

ahCache Klasse ist nun PHP 8.4 kompatibel

Montag, 16. Dezember, 2024

PHP 8.4 ist seit Ende November live.

Meine PHP-Klasse ahCache, die beliebige Daten (Strings, Hashes, Objekte) im lokalen Dateisystem cachen kann, ist hiermit offiziell PHP 8.4 kompatibel. Es musste lediglich eine Methode angepasst werden. Das war um Glück überschaubar.

Die Dokumentation wurde gleichsam umgestellt. Sie ist nun im Repository direkt enthalten und wird - wie schon eineige meiner anderen Projekte - mit Daux.io gerendert.

Weiterführende Linke

Bash: mit jq über Listen-Elemente loopen

Donnerstag, 21. November, 2024

Mit Bash code ich diverse Sachen, um mir das Admin Leben zu vereinfachen.

Sobald man JSON Dateien oder JSON als API-Response vor sich hat, ist plattformübergreifend jq das Tool der Wahl für ein schnelles Pretty Print oder seinen starken Filter-Funktionen.

Wenn ich in einer Variable die komplette Liste hineinhole …

# Holen einer JSON-Liste
$OUT_TOKENS= $( curl $URL )

Für jeden Eintrag wünsche ich eine Verarbeitung. Mit seq kann ich von 1 .. N loopen. Die Anzahl der Elemente müssen wir uns daher zunächst einmal holen. Am enfachsten, wenn ich eine “id” oder einen anderen Key drin habe.

# IDs / Einträge zählen:
iTokenCount=$( jq ".[].id " < "$OUT_TOKENS" | wc -l )

Mit einem jq Filter kann ich mir innerhalb der Schleife das N-te Element aus der Variable $OUT_TOKENS herausziehen, in eine eigene Variable legen und dann anschliessend jene zerlegen.

Snippet:


# einmal drüber loopen
for i in $( seq 1 $iTokenCount )
do

   # get N-th token
   entry="$( jq ".[$i]" < "$OUT_TOKENS")"

   # ...

done

Nehmen wir mal an, ich möchte aus jedem Listen-Eintrag den Key “name” auslesen. Und vielleicht noch einen 2. oder 3…
Dann schreibt man zunächst eine kleine Funktion

# Get a single value from json
# param  string  json data
# param  string  key
# return string
function getKey(){
    jq -r ".$2" <<< "$1" | grep -v "null"
}

… welche man dann innerhalb des Loops verwendet:

for i in $( seq 1 $iTokenCount )
do

   # get N-th token
    entry="$( jq ".[$i]" < "$OUT_TOKENS")"

    sName=$( getKey "$entry" "name" )
    if [ -z "$sName" ]; then
        continue
    fi

    sExpire=$( getKey "$entry" "expires_at" )

    # ...
done

Weiterführende Links

PHP Email Catcher

Mittwoch, 23. Oktober, 2024

Ich habe für diverse Projekte eine PHP-Entwicklungsumgebung. Mittlerweile als Docker Container.

Wenn meine Applikation in der Live Umgebung Emails versendet - wie gehe ich damit in der Dev-Umgebung um?

Ich wollte einen Email-Catcher haben, der statt Sendmail oder Postfix die Emails zu versenden, für mich abfängt.
Zum Glück ist das nicht soo schwer, dies ohne weitere Abhängigkeiten zu coden. So entstanden eine PHP-Klasse und 2 Skripte: eines, um die Emails vom STDIN einzufangen - und ein Viewer.

Ganz wichtig: es musste einfach zu verwenden sein! Voila: die Konfiguration besteht aus einer Zeile in der php.ini.

Wenn du in einer PHP Enwicklungsumgebung die versendeten Email abfangen und lesen willst: hier ist ein quick winner!

📜 License: GNU GPL 3.0
📄 Source: https://git-repo.iml … rce/php-emailcatcher
📗 Docs: https://os-docs.iml. … ch/php-emailcatcher/ (en)

Screenshots - so sieht es aus:

2024-10-23-screenshot_list_emails.png 2024-10-23-screenshot_show_email.png 2024-10-23-screenshot_show_html_email.png

Und so geht es:

Installation mit Git:

Unterhalb des Webroot lege ich im Unterverzeichnis “vendor” die Software ab.

cd [WEBROOT]
cd vendor
git clone https://git-repo.iml.unibe.ch/iml-open-source/php-emailcatcher.git emailcatcher

Einfangen der Email aktivieren:

In der php.ini ist mit sendmail_path auf das Skript php-sendmail.php zu verweisen.

[PHP]
...
sendmail_path = "php [WEBROOT]/vendor/emailcatcher/php-sendmail.php"

Anm: Sofern php nicht im Pfad ist, muss man statt “php ” den kompletten Pfad angeben z.B. “/usr/bin/php “.

Test-Email versenden.

Viewer starten.

Unterhalb “vendor” ist im emailcatcher Verzeichnis eine viewer.php - die öffnet man im Webbrowser.

Z.B.: http://localhost/vendor/emailcatcher/viewer.php

Tja, und das war es bereits. Viel Spass damit!

Update:

Wenn man HTML Emails versendet und das Layout ansehen möchte, dann brauchte es in der präsentierten Version einige Klicks. Unnötige Klicks. Und immer wieder. HTML Emails wollte ich dann doch direkt sehen können. Im Localstorage des Webbrowsers wird sich der letzte Zustand der Anzeige gemerkt: Header anzeigen ja oder nein - und Ansicht als HTML oder Source.
Sieht wer weiteres Verbesserungspotetial zur Effiziensteigerung? Lasst es mich wissen oder macht einen Pull-Request.

AAI Logon Seite mit PHP

Mittwoch, 16. Oktober, 2024

An meinem Institut habe ich ein Projekt für eine Logon-Seite mit AAI / EduGain in einer funktionsfähigen Version released.

AAI ist eine Anmeldeform im universitären Bereich. Studierende und Dozenen anderer Universitäten melden sich mit dem Account ihrer eigenen Universität bei ihrem gewohnten Anmeldeprovider (IDP) an. Wenn die Authentifizierung erfolgreich war, dann kann der fremde IDP unserer Applikation den Erfolg des Logons in Form einer ID sowie einige minimale Metadaten senden. Mit jener ID lässt sich in der eigenen Anwendung ein User anlegen oder wiedererkennen. Regelbasiert oder manuell administrativ kann man einem Account Gruppen und Rollen zuweisen.

Das gibt es alles schon und ist spezifiziert, dokumentiert und schon lange produktiv im Einsatz. Sowas erfindet man nicht neu.

Mein Pro AAI Login ist in PHP programmiert - und folglich hilfreich für eine PHP-Anwendung mit Shibboleth. Der Ursprung der Entstehung war eine Ilias9 Installation.In früheren Ilias Versionen konnte man für die Start-/ Anmelde-Seite HTML-Code und Javascript einbinden. Auf diese Weise wurde ein externes Javascript WAYF (=Where are you from) von Switch eingebunden. Das funktionierte super.
In Ilias 9 kann man seine Startseite umfangreich zusammenklicken, aber Javascript konnte ich nicht einbetten. In Feldern für HTML-Code wurde es nach dem Speichern ausgefiltert. Nachdem ich mit dem Anmelde-Konfigurator aufgegeben hatte, sollte eine Seite her, die die Aufgabe der Anmeldeseite übernimmt. Zwar ist initial Ilias das erste Ziel-Projekt, aber es ist bewusst flexibel konfigurierbar ausgelegt, so dass es sich für andere Shibboleth geschützte Anwendung einsetzen liesse.

Die Logon-Seite bietet eine Liste der zugelassenen IDPs an. Mit enem Filterfeld lassen sich auf Tastendruck die angezeigten IDP reduzieren, damit ein Benutzer schnell das eigene Login finden kann.

Der Filter nach TLD (Länder-Domains) wird automatisch bereitgestellt.

2024-10-16-aai_login_mode_boxes.png

Die Anmelde-(Auswahl-)Seite unterstützt Mehrsprachigkeit, mehrere Layouts (als Boxen, Liste, WAYF Auswahlseite). Hier die mitgelieferte Darstellungsform als Liste:

2024-10-16-aai_login_mode_list.png

Weiterhin ist es flexibel bezüglich

  • Sprachen. Ein deutsches und englisches Spachfile sind mitgeliefert. Der Aufwand für ein neues Sprachfile ist sehr überschaubar
  • Ausgabetexte: Es gibt 3..4 Textelemente, die man frei konfigurieren kann. So kann man den HTML-Code vor und nach der Provider-Auswahl anpassen.
  • Layout der Provider-Ausflistung: es werden 3 Layouts mitgliefert. 2 davon sieht man in obigen Screenshots. Ein neues Layout lässt sich schnell erstellen. Eine Klasse liefert bereits alle nötigen Daten, die man nur noch im gewünschten HTML-Code ausgeben lassen muss. Jedes Layout wird seine eigene CSS Datei einbinden - damit ist man völlig frei in der Gestaltung
  • Farben: das Basislayout kommt von einer vorgegebenen CSS Datei. Mit Hilfe einer eigenen CSS Datei kann man alle CSS Rules üübersteuern und das Gesamtlayout verändern.

Die Lizenz erlaubt kostenfreie Verwendung, Einsicht in den Quellcode und Anpassung jedweder Art.
Feedbacks und Verbesserungsvorschläge sind willkommen.

📜 License: GNU GPL 3.0
📄 Source: https://git-repo.iml … pen-source/login-aai
📗 Docs: https://os-docs.iml.unibe.ch/login-aai/

Gaza 1 Jahr nach dem HAMAS Attentat

Montag, 7. Oktober, 2024

Vor 1 Jahr ermordete die Terrormiliz Hama in Israel 1.200 Menschen und nahm über 200 Geiseln [1]. Ein widerliches Verbrechen - ohne jeden Zweifel.

Und fast ein Jahr dauert nun auch die erbarmungslose israelische Gegenoffensive im Gazastreifen an, die bereits zehntausende Opfer gefordert hat [2].

Im Interview: Rami Abou Jamous (palästinensischer Journalist).

  1. Einst war Gaza bei der Obst- und Gemüseversorgung autonom. 2/3 der Anbauflächen wurden verwüstet. Die Lebensmittelpreise in Gaza sind unvorstellbar: 100 Euro für 1 kg Tomaten, 70 Euro für 1 kg Kartoffeln. Das ist das Perverse: die Israelis bombardieren unsere Felder aber isreaelische Ware lassen sie durch. Sie machen noch Profit mit dem was wir kaufen müssen, um essen zu können
  2. Von 2.3 Millionen Einwohnern sind 2 Millionen Binnenflüchtlinge
  3. Im Frühjahr wurden durch Israelis die Hälfte der 45 Friedhöfe vernichtet
  4. Es gibt kein Trinkwasser mehr. Ich schäme mich, es zu erzählen, aber wir haben fast nichts mehr. Keine Seife, kein Shampoo, keine Windeln für meinen Sohn, nicht einmal Monatsbinden für die Frauen. Die Israelis töten uns nicht nur, machen nicht nur Hunger zur Kriegswaffe sondern auch die Hygiene.
  5. Mehr als 600 Moscheen und 3 Kirchen wurden zerstört
  6. über 400 Schulen und Universitäten wurden zerstört oder beschädigt. Sie dienten oft Binnenflüchtlingen als Notunterkunft.
  7. 34 Krankenhäuser sind komplett stillgelegt - 25 weitere beschädigt. Jedes Mal, wenn die Besatzungsarmee in eine Stadt einmarschiert, ist das Krankenhaus ihr erstes Ziel.Sie behaupten jedes Mal, dass sich HAMAS Kämpfer dort verstecken. Jedes Mal reden sie von einer Kommandostelle. Es läuft immer gleich. … Sie haben alles niedergebrannt und verwüstet…. Sie wollen alle von der HAMAS betriebenen öffentlichen Dienste vernichten.Und die erste Zielscheibe ist das Gesundheitswesen.
  8. Die Israelis haben 360.000 Gebäude zerstört.
  9. Sie haben Museen zerbombt und niedergebrannt. Tausendjahre alte Kulturschätze, wie den Hamam von Gaza dem Erdboden gleichgemacht. Genau wie unsere archäologischen Stätten. Hinter all dem steckt die Absicht, unsere Kultur, unsere Zugehörigkeit als Menschen auszulöschen! Obwohl: Mensch ist schon zuviel gesagt. Sie betrachten uns nicht als Menschen. Sie wollen unsere Beziehung zu dem Land auf dem wir leben, zu brechen. Deswegen zerstören sie alles.

Hinzu kommen Sattelitedaten und deren Auswertung von Vertical52.

Es wurden fast 42.000 Menschen getötet. Jedem getöteten Israeli am 7. Oktober stehen 40 getötete Palästinenser gegenüber. Einmal bildlich:

2024-10-07_dead_isreael_vs_palestina.png

Pro am 7. Oktober 2023 getötetem Israeli am 7. Oktober 2023 kommen 2000 geflüchtete Palästinenser hinzu. 1 Millionen geflüchtete Libanesen der an der nun im Norden eröffneten Front sind in dieser Rechnung noch gar nicht dabei.
Das wirkt auf mich so, als würde eine Kollektivstrafe erhoben und eliminierte HAMAS Kämpfer sind dabei unter all den Opferzahlen nur reine Zufallstreffer.

Ich wünsche mir sehr, dass es gewichtige vernünftige Stimmen gibt, die den Wahnsinn und Blutrausch beenden können.

Update

  • Israelis greifen am Wochenende einen Stützpunkt der UN Blauhelme an. Wenn die Israelis sagen, die Soldaten wurden vorab gewarnt, geben sie explizit zu, bewusst als auch gezielt diese Bomardierung vorgenommen zu haben. Der seit Mai wg. Kriegsverbrechen international zur Haft ausgeschriebene Netanyahu [3] faselt in einem lokalen Fernsehsender (!) etwas davon, die Blauhelme sollen aus Libanon auch abziehen. Wohlwisend, dass UN Mandate eher in New York mit den zig Staaten diskutiert werden sollten, die die Blauhelmsoldaten stellen. Was für eine primitive Polemik. Er macht kindische Mätzchen - aber einen Kriegsverbrecher intressieren kein internationales Recht oder Stimmern der Staatengemeinschaft. Und das, obwohl Israel erst durch Kraftanstrengung der UNO entstehen konnte. Das empfinde ich ebenso respektlos.

Weiterführende Informationen:

JS: Inhalt einer Json Datei in einen Hash holen

Mittwoch, 18. September, 2024

Ich habe eine Sprachdatei für ein PHP-Skript in einer JSON Datei abgelegt.
Meine HTML Seite sollte in der eingebundenen Javascript-Datei die Datei ebenfalls laden und deren Inhalt einer Variable “aLang” zuweisen.

Sicher geht es noch eleganter, aber ich bin bei diesem Snippet hängengeblieben:


var sLang='de-de';
var aLang={};

(...)

/**
 * Init step 1: load language file and make it available in aLang
 */
async function init(){
    fetch('lang/' + sLang + '.json')
    .then(response => response.json())
    .then(aLang => {
        initStep2(aLang)
    })
    .catch(error => console.log(error))
    ;

}

/**
 * Init step 2
 */
async function initStep2(langArray) {
    aLang = langArray;

    // just to check it in the console:
    console.log(aLang);

    // optional: next js function to call or more js code
}

// MAIN
init();