Zulip raus und Matrix rein

Seit August betrieb ich auf einer VM einen Zulip-Server zum Testen. Ich dachte, dass sich das Ding in der Familie evtl. ausbreiten würde, als Ergänzung zu unserem XMPP-Server. Dem war aber nicht so, was vor allem daran lag, dass die Android-App von Zulip wenig intuitiv daher kam und weil die Inhaltsverschlüsselung für Chats fehlte. Jetzt also Matrix-Synapse mit OLM als Verschlüsselung – wie einst Zulip hier im Haus auf einer VM betrieben mit ddnss als DynDNS Betreiber.

So richtig datenschutz-sauber ist Matrix bzw. die Android App Riot nicht. Auch dann nicht, wenn man die build aus FDroid verwendet. Diesbezüglich führt unser XMPP-Server weiterhin das Feld an und bleibt deswegen auch die Kommunikationszentrale der Familie. Aber was bei den ersten Tests von Matrix sofort überzeugte, ist die Zuverlässigkeit, mit der auch die dicksten Anhänge zugestellt werden.

Was ebenfalls für Matrix spricht ist die einfache Installation und Konfiguration. XMPP – selbst mit Prosody – und die gefühlt immer verwickelten Konfigurationsorgien bei den benötigten XEP-Modulen sind da schwerer in den Griff zu bekommen.

Mako refresh

Bela hat Lewins altes Mako überarbeitet, einen frischen Akku eingesetzt und den gammeligen Power Button ausgetauscht. Dank einiger Anleitungen auf Youtube und Fingerfertigkeit war das ganze Projekt in nicht einmal 1,5 Stunden abgeschlossen – inklusive Installation eines frischen LineageOS. Die meiste Zeit ging für das Entfernen der Rückwand drauf, was ich übernahm.

Ich selbst nutze auch noch ein Mako und will es nicht hergeben. Läuft hier seit vielen Jahren stabil mit wöchentlich frischem LineageOS und FDroid als Appstore. Der einzige Nachteil ist die ranzige Kamera, die extrem viel Sonnenlicht benötigt, um gute Bilder zu machen.

TC und EDS-Lite

Als Notiz für die, die es brauchen: Für EDS-Lite können TrueCrypt-Container (ja – auch VeraCrypt geht, aber das Schutzniveau muss ja nicht in und für jeden Fall so gesetzt werden) mit den Einstellungen oben schneller auf dem Rechner, als auf dem Tablet angelegt werden:

Encryption Algorithm: Twofish
Hash-Algorithm: SHA-512

Die Standardvorgaben von TrueCrypt scheinen für EDS-Lite nicht zu funktionieren.

WebUntis Scraper

Der Stundeplanrechner wirft die WebUntis HTML Dateien in ein WebDAVs Share. Dieses ist auf dem Moodle Server per Symlink in das Arbeitsverzeichnis des folgenden untisparser Skriptes eingebunden. Von dort wird das Ergebnis in ein File Repository des Lehrermoodles geschrieben und aus dem Kursraum „Schwarzes Brett“ (die Kommunikationsplattform der Schule) verlinkt. Das stellt sicher, dass nur Menschen, die a) am Moodle angemeldet und b) Mitglied des entsprechenden Kursraumes sind den Inhalt (hier: Vertretungsplan) einsehen können.

Mit geschlossenem Accordion

Accordion offen

Das muss leider so umständlich sein, weil ich keinen Weg gefunden habe, den WebDavs Ordner als solchen navigierbar im Kursraum so einzubinden, dass man diesen nicht auch von außerhalb des Moodles aufrufen könnte. Mir bleibt (sofern ich richtig liege) nix anderes über, als aus den X html Dateien, die WebUntis mir hinwirft, eine einzige zu bauen, die ich klar benamen und dann „verlinken“ kann. Dazu muss ich die vielen WebUntis HTML Dateien in ihre Bestandteile zerlegen. Ich brauche: Die Tabellen und die Zeitangaben.

Im Prinzip geht das mit beautifulsoup und Python. Meine Python-Kenntnisse sind jedoch leider noch rudimentärer als meine Bash-Kenntnisse … also muss  ein bash Skript her. Und HTML mit Bash nativ parsen  – nun: ich weiß, dass das Probleme macht. Das will man nicht.

Die Lösung ist pup, das ich in ein Bash-Skript einbinde.

Zur Information die Struktur der WebUntis Dateien in ihren Ordnern / Unterordnern:

Die zu erzeugende Output-Datei soll HTML sein und braucht dazu einen Kopf:

<html>
<head>
<title>Vertretungsplan</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0">
<meta http-equiv="pragma" content="no-cache" />

<meta http-equiv="refresh" content="120" />
<style type="text/css">
body { margin-top: 20px; margin-left: 20px; margin-right: 20px;
background: #fff; color: #272727; font: 80% Arial, Helvetica, sans-serif; }
h1 { color: #ee7f00; font-size: 200%; font-weight: bold;}
h2 { color: #ee7f00; font-size: 175%;}
h1, h2 { margin: 0; padding: 25px 0px 5px 0px;}

 /* put your css here or copy from untishtml */
 
 /* Style the buttons that are used to open and close the accordion panel */
.accordion {
    background-color: #eee;
    color: #444;
    cursor: pointer;
    padding: 18px;
    width: 100%;
    text-align: left;
    border: none;
    outline: none;
    transition: 0.4s;
}

/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active, .accordion:hover {
    background-color: #ccc;
}

/* Style the accordion panel. Note: hidden by default */
.panel {
    padding: 0 18px;
    background-color: white;
    display: none;
    overflow: hidden;
} 


</style>
</head>
<body>

<script>
  window.onload=function(){
    var acc = document.getElementsByClassName("accordion");
    for (var i = 0; i < acc.length; i++) {
      acc[i].addEventListener("click", function() {
          this.classList.toggle("active");
          var panel = this.nextElementSibling;
          if (panel.style.display === "block") {
              panel.style.display = "none";
          } else {
              panel.style.display = "block";
          }
      });
    }
  };
</script>

<h1>KvFG Vertretungsplan nach Datum und Anzeigetafel</h1>
<p><strong>Es gilt der Plan auf den Anzeigetafeln im Haus bzw. auf den Aush&auml;ngen. Diese Datei ist immer nur Beta!</strong></p>

Siehe zum Code für das Accordion im Kopf und im „Kleister“: https://www.w3schools.com/howto/howto_js_accordion.asp  Leider hat das aber nicht gereicht. Zuerst muss gewartet werden, bis die gesamte Seite geladen ist, dann erst darf die for-Schleife beginnen. Also wird das Skript in den Footer gelegt oder gekapselt in:

window.onload=function(){
... }
/* thanx Janis for debugging */

Was einen Kopf hat, braucht auch einen Fuß:

<p></p>
<p>----</p>
<p>(C) dowel 2018 | UntisParser Version 0.1</p>
<p>Fehler sind immer m&oumlglich und sollten <a href="https://yourschoolsbugtracker.tld" target="_blank">im Bugtracker gemeldet werden</a>, damit diese behoben werden k&ouml;nnen. Wer keinen Bugreport (= Bericht) formulieren kann, muss schweigen.</p>
</body>
</html>

Und dann braucht es den „Kleister“, der die Arbeit macht und alles zusammenpackt. Nennen wir es untisparser.sh und legen es in das passende Verzeichnis auf dem Server:

#!/bin/bash
# UntisParser
# 
# Parse HTML Export Files created by WebUntis
# do "e pluribus unum" with a Bash script
# throw the output into a Moodle file repository
# and link the output from inside a Moodle Courseroom
# 
# You need the pup executable from https://github.com/ericchiang/pup
# 
# (C) dowel
# License: CC BY SA https://creativecommons.org/licenses/by-sa/4.0/deed.de
# Date: 2018-08-23
# Version 0.2
# ####

# def of some base vars
RUNT=$(date '+%Y-%m-%d %H:%M:%S')
RUNF="20 Minuten" # set this in cronjob
RELOAD="120 Sekunden" # see head_filename
WORKDIR="/path/untisparser"
PUPPATH="/path/bin"
HEAD_FILENAME="kopf.html" # asumed to be in workdir
FOOT_FILENAME="fuss.html" # asumed to be in workdir
OUTPUT_PATH="/path/moodledata/repository/anzeigebretter" # set this to your Moodle file repository
OUTPUT_FILENAME="output.html"

# headline def
LUL1H="Anzeige Lehrer/innen Teil 1 Linke Seite (Heute?)"
LUL2H="Anzeige Lehrer/innen Teil 2 Rechte Seite (Morgen?)"
SUS1H="Anzeige Sch&uuml;ler/innen Teil 1 Linke Seite (Heute?)"
SUS2H="Anzeige Sch&uuml;ler/innen Teil 2 Rechte Seite (Morgen?)"


# path stuff
# Where are the Untis files in the workdir?
L_SUBDIR="$WORKDIR/Lehrerbrett"
S_SUBDIR="$WORKDIR/Schuelerbrett"

# How many of the suckers are there?
COUNT_LF1=$(/usr/bin/find $L_SUBDIR/f1 -maxdepth 1 -name '*.htm' | /usr/bin/wc -l)
COUNT_LF2=$(/usr/bin/find $L_SUBDIR/f2 -maxdepth 1 -name '*.htm' | /usr/bin/wc -l)
COUNT_SF1=$(/usr/bin/find $S_SUBDIR/f1 -maxdepth 1 -name '*.htm' | /usr/bin/wc -l)
COUNT_SF2=$(/usr/bin/find $S_SUBDIR/f2 -maxdepth 1 -name '*.htm' | /usr/bin/wc -l)

# Warning jabber because we do not know how many old files there are
# and if cleaner.script has worked the way it should have
# we asume that it failed
WARNMESS="<p>
    --------------------------------------------------------------------------------------<br>
    Stand: $RUNT | Beachte: Das Skript l&auml;uft nur alle $RUNF und die Seite wird nach $RELOAD neu geladen!<br>
    Achte auf das Datum. Es kann sein, dass ab hier alte Dateien ausgelesen werden. Wenn das so ist, dann den Rest ignorieren.</br>
    <a href=\"#top\">Ganz nach oben</a> || LuL: <a href=\"#lul1\">Links</a> | <a href=\"#lul2\">Rechts</a> || SuS <a href=\"#sus1\">Links</a> | <a href=\"#sus2\">Rechts</a>
    </p>"

# create empty output file and fill it with html head and some simple navigation stuff
/bin/cat $WORKDIR/$HEAD_FILENAME > $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/echo "<id=\"top\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/echo "<p><em>Links</em> ist meist <em>Heute</em> und <em>Rechts</em> ist meist <em>Morgen</em> - aber nicht immer.</p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/echo "<p>Anzeige Lehrer/innen: <a href=\"#lul1\">Links</a> | <a href=\"#lul2\">Rechts</a></p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/echo "<p>Anzeige Sch&uuml;ler/innen: <a href=\"#sus1\">Links</a> | <a href=\"#sus2\">Rechts</a></p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/echo "<p>Stand: $RUNT | Beachte: Das Skript l&auml;uft nur alle $RUNF!</p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/echo "<p></p>">> $OUTPUT_PATH/$OUTPUT_FILENAME

# ticker export
if [ -f $L_SUBDIR/ticker.htm ]
then
    /bin/echo "<h4>L Ticker</h4>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    $PUPPATH/pup --charset iso-8859-1 -f $L_SUBDIR/ticker.htm 'marquee text{}' >> $OUTPUT_PATH/$OUTPUT_FILENAME
fi

if [ -f $S_SUBDIR/ticker.htm ]
then
    /bin/echo "<h4>S Ticker</h4>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    $PUPPATH/pup --charset iso-8859-1 -f $S_SUBDIR/ticker.htm 'marquee text{}' >> $OUTPUT_PATH/$OUTPUT_FILENAME
fi

# Create teacher part of output file

/bin/echo "<h2 id=\"lul1\">$LUL1H</h2>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
COUNT=1
MAXCOUNT=1
let MAXCOUNT=COUNT_LF1+1

# teacher links
while [ $COUNT -lt $MAXCOUNT ] ; do
    # padding
    NUM=$(printf %03d $COUNT)
    # Date extract to button
    /bin/echo "<button class=\"accordion\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    $PUPPATH/pup --charset iso-8859-1 -f $L_SUBDIR/f1/subst_$NUM.htm 'div.mon_title' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</button>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<div class=\"panel\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Headline is set to H4
    /bin/echo "<h4>Lehrer/innen</h4>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<p></p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Table extraction
    $PUPPATH/pup --charset iso-8859-1 -f $L_SUBDIR/f1/subst_$NUM.htm 'table' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</div>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Warning 
    /bin/echo $WARNMESS >> $OUTPUT_PATH/$OUTPUT_FILENAME
    let COUNT=COUNT+1
done

# Reset for second part of LuL 

/bin/echo "<h2 id=\"lul2\">$LUL2H</h2>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
COUNT=1
MAXCOUNT=1
let MAXCOUNT=COUNT_LF2+1

# teacher rechts
while [ $COUNT -lt $MAXCOUNT ] ; do
    # padding
    NUM=$(printf %03d $COUNT)
    # Date extract to button
    /bin/echo "<button class=\"accordion\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    $PUPPATH/pup --charset iso-8859-1 -f $L_SUBDIR/f2/subst_$NUM.htm 'div.mon_title' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</button>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<div class=\"panel\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Headline is set to H4
    /bin/echo "<h4>Lehrer/innen</h4>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<p></p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Table extraction
    $PUPPATH/pup --charset iso-8859-1 -f $L_SUBDIR/f2/subst_$NUM.htm 'table' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</div>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Warning
    /bin/echo $WARNMESS >> $OUTPUT_PATH/$OUTPUT_FILENAME
    let COUNT=COUNT+1
done

# Create pupil part of output file
/bin/echo "<h2 id=\"sus1\">$SUS1H</H2>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
COUNT=1
MAXCOUNT=1
let MAXCOUNT=COUNT_SF1+1


# pupil links
while [ $COUNT -lt $MAXCOUNT ] ; do
    # padding
    NUM=$(printf %03d $COUNT)
    # Date extract to button
    /bin/echo "<button class=\"accordion\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    $PUPPATH/pup --charset iso-8859-1 -f $S_SUBDIR/f1/subst_$NUM.htm 'div.mon_title' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</button>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<div class=\"panel\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Headline is set to H4
    /bin/echo "<h4>Sch&uuml;ler/innen</h4>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<p></p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Table extraction
    $PUPPATH/pup --charset iso-8859-1 -f $S_SUBDIR/f1/subst_$NUM.htm 'table' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</div>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Warning
    /bin/echo $WARNMESS >> $OUTPUT_PATH/$OUTPUT_FILENAME
    let COUNT=COUNT+1
done

# Reset for second part of SuS 

/bin/echo "<h2 id=\"sus2\">$SUS2H</h2>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
COUNT=1
MAXCOUNT=1
let MAXCOUNT=COUNT_SF2+1

# pupil rechts
while [ $COUNT -lt $MAXCOUNT ] ; do
    # padding
    NUM=$(printf %03d $COUNT)
    # Date extract to button
    /bin/echo "<button class=\"accordion\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    $PUPPATH/pup --charset iso-8859-1 -f $S_SUBDIR/f2/subst_$NUM.htm 'div.mon_title' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</button>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<div class=\"panel\">" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Headline is set to H3
    /bin/echo "<h4>Sch&uuml;ler/innen</h4>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "<p></p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Table extraction
    $PUPPATH/pup --charset iso-8859-1 -f $S_SUBDIR/f2/subst_$NUM.htm 'table' >> $OUTPUT_PATH/$OUTPUT_FILENAME
    /bin/echo "</div>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
    # Warning
    /bin/echo $WARNMESS >> $OUTPUT_PATH/$OUTPUT_FILENAME
    let COUNT=COUNT+1
done

# create footer
/bin/echo "<p></p>" >> $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/cat $WORKDIR/$FOOT_FILENAME >> $OUTPUT_PATH/$OUTPUT_FILENAME

# cleanup
/bin/chown www-data.www-data $OUTPUT_PATH/$OUTPUT_FILENAME
/bin/chmod 2750 $OUTPUT_PATH/$OUTPUT_FILENAME
# and we are done
exit 0

Cron ruft das Skript alle 20 Minuten auf. Schöner wäre natürlich, wenn das Skript nur liefe, wenn sich was ändert im WebDavs Share. Aber inotify und Freunde sind auch nicht ohne. So ist es simpel und scheint zu funktionieren, ohne viel Ressourcen zu fressen.

Update 23.08

Einige Fehler im Skript (meist die vergessenenen \ vor den „) behoben und mit Hilfe von JavaScript mehr Übersichtlichkeit im Output erzeugt.

Greebo und syntax_plugin_cryptor

Vor einer gefühlten Ewigkeit hab ich das Plugin crypto für ein DokuWiki installiert und damit einigen Text auch „verschlüsselt“ (das Plugin macht aus dem eingegebenen Text auf der Basis einer Passphrase mit einem in JavaScript implementierten AES256 Algorithmus Buchstaben- und Zahlensalat). Das Ding warf nun nach dem Update auf Greebo und PHP7 das hier in die Logs:

[Sun Jun 03 13:55:36.724556 2018] [:error] [pid 14354] [client 11.111.111.111:62116] PHP Warning:  Declaration of action_plugin_cryptor::register($control
ler) should be compatible with DokuWiki_Action_Plugin::register(Doku_Event_Handler $controller) in /pfad/lib/plugins/cryptor/action.php on lin
e 0, referer: https://sub.domain.tld/dokuwikipfad/doku.php?id=start

Ich hab da nun so lange – und ehrlich gesagt: frei von PHP-Kenntnissen – mit Hilfe von Anleitungen zur Plugin-Erstellung auf dokwiki.org sowie Forenbeiträgen und Bugfixes auf Github zu anderen Dokuwiki-Plugins dran rumgefummelt, bis meine Logs keine Fehlermeldungen mehr zeigen und das Plugin trotzdem funktioniert.

Aus meiner Sicht haben sich seit der Erstellung dieses Plugins nur die Bezeichnungen der Funktionsaufrufe in Dokuwiki geändert. Ein echtes PHP5 – PHP7 Problem lag vermutlich nicht vor.

Wer da mal reingucken und selbst testen will sei mir herzlich willkommen:

2018-06-03-cryptor-php7-greebo [ZIP] [21 kb]

Etherpad auf 16.04

Ein Update zu der etwas in die Jahre gekommenen Anleitung zur Installation von Etherpad Lite auf Ubuntu, auch wenn sich viele Dinge nicht wirklich grundlegend geändert haben.

Erst einmal versorgen wir unseren Server mit einem aktuellen NodeJ.js sowie NPM. Die Anleitung hierzu: https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions

Dieser wird gefolgt, bis mit

sudo apt-get install -y nodejs

nicht nur NodeJS, sondern auch NPM an Bord ist.

Es folgen die Vorbereitungen für die lokale Installation von EP:

apt-get install gzip git curl python libssl-dev pkg-config build-essential

Dazu gehört ein Benutzerkonto etherpad:

sudo adduser --system --home=/opt/etherpad --group etherpad

In dessen Kontext dann gewechselt wird, um EP zu installieren:

sudo su - etherpad -s /bin/bash
# Dann als User etherpad weiter
git clone git://github.com/ether/etherpad-lite.git
cd etherpad-lite
bin/run.sh

Der erste Start installiert die Abhängigkeiten und sollte es danach ermöglichen, die Etherpad Installation unter http://example.org:9001 aufzurufen. Gelingt dies, dann brechen wir EP mit STRG C ab, um in Ruhe die Datei settings.json in /opt/etherpad/etherpad-lite sowie den Web- und DB-Server anzupassen.

Da wir nun immer wieder EP neu starten (als user etherpad) und außerdem als root weitere Pakete nachinstallieren sowie Anpassungen vornehmen müssen macht eine zweite Shell zum Server Sinn.

Nach der Installation von Apache2 und der Einrichtung von SSL-Zertifikaten folgt die Aktivierung der entsprechenden Module im Apachen:

a2enmod proxy proxy_http deflate headers ssl wstunnel

Ich folge hier im Wesentlichen der Anleitung hier: https://github.com/ether/etherpad-lite/wiki/How-to-put-Etherpad-Lite-behind-a-reverse-Proxy und erhalte am Ende eine VirtualHost Definition, die so aussieht:

<VirtualHost *:443>
      ServerAdmin webmaster@example.com
      ServerName etherpad.example.com
      DocumentRoot /var/www/example.com

     SSLEngine on
     ServerSignature On
     SSLHonorCipherOrder on
     SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS

     SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
     SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem
     SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

        ProxyVia On
        ProxyRequests Off
        ProxyPreserveHost on

        <Location />
            ProxyPass http://localhost:9001/ retry=0 timeout=30
            ProxyPassReverse http://localhost:9001/
        </Location>

        <Location /socket.io>
            RewriteEngine On
            RewriteCond %{QUERY_STRING} transport=websocket    [NC]
            RewriteRule /(.*) ws://localhost:9001/socket.io/$1 [P,L]
            ProxyPass http://localhost:9001/socket.io retry=0 timeout=30
            ProxyPassReverse http://localhost:9001/socket.io
        </Location>

        <Proxy *>
            Options FollowSymLinks MultiViews
            AllowOverride All
            Order allow,deny
            allow from all
        </Proxy>


        # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
        # error, crit, alert, emerg.
        # It is also possible to configure the loglevel for particular
        # modules, e.g.
        #LogLevel info ssl:warn

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

        #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire
        <FilesMatch "\.(cgi|shtml|phtml|php)$">
                        SSLOptions +StdEnvVars
        </FilesMatch>
        <Directory /usr/lib/cgi-bin>
                        SSLOptions +StdEnvVars
        </Directory>

</VirtualHost>

Nach einem Neustart des Apachen und einem erneuten Start von EP lite (aus dem Konto von etherpad heraus) sollte EP über https abgerufen werden können.

Hinweis zu den geladenen Apache-Modulen: wstunnel beseitigte bei mir Fehlermeldungen wie die diese:

[proxy:error] [pid 7926] [client 79.1.8.45:40652] AH00898: Error reading from remote server returned by /socket.io/
[proxy:warn] [pid 7934] [client 79.1.8.45:40848] AH01144: No protocol handler was valid for the URL /socket.io/. If you are using a DSO version of mod_proxy, make sure the proxy submodules are included in the configuration using LoadModule.

Wir können EP nun wieder mit STRG C anhalten und eine Datenbank für EP einrichten. Dazu benötigen wir einen MySQL-Server sowie, bei Bedarf, phpMyAdmin für die einfachere Verwaltung. Weiter sollte auch abiword mit an Bord geholt werden, damit Pads exportiert werden können.

Sind diese Schritte vollbracht, wird die settings.json überarbeitet:

/*
  This file must be valid JSON. But comments are allowed

  Please edit settings.json, not settings.json.template

  To still commit settings without credentials you can
  store any credential settings in credentials.json
*/
{
  // Name your instance!
  "title": "KvFG Etherpad",

  // favicon default name
  // alternatively, set up a fully specified Url to your own favicon
  "favicon": "favicon.ico",

  //IP and port which etherpad should bind at
  "ip": "0.0.0.0",
  "port" : 9001,

  // Option to hide/show the settings.json in admin page, default option is set to true
  "showSettingsInAdminPage" : true,

  /*
  // Node native SSL support
  // this is disabled by default
  //
  // make sure to have the minimum and correct file access permissions set
  // so that the Etherpad server can access them

  "ssl" : {
            "key"  : "/path-to-your/epl-server.key",
            "cert" : "/path-to-your/epl-server.crt",
            "ca": ["/path-to-your/epl-intermediate-cert1.crt", "/path-to-your/epl-intermediate-cert2.crt"]
          },

  */

  //The Type of the database. You can choose between dirty, postgres, sqlite and mysql
  //You shouldn't use "dirty" for for anything else than testing or development
   "dbType" : "mysql",
   "dbSettings" : {
                    "user"    : "dbname",
                    "host"    : "localhost",
                    "password": "dbpassword",
                    "database": "dbuser",
                    "charset" : "utf8mb4"
                  },

  //the default text of a pad
  "defaultPadText" : "Welcome to Etherpad on Karlo!\n\nMit der Nutzung dieser Etherpad-Installation erklaerst Du Dich mit den folgenden Bedingungen einverstanden: https://example.com/doku.php?id=etherpad\n",

  /* Default Pad behavior, users can override by changing */
  "padOptions": {
    "noColors": false,
    "showControls": true,
    "showChat": true,
    "showLineNumbers": true,
    "useMonospaceFont": false,
    "userName": false,
    "userColor": false,
    "rtl": false,
    "alwaysShowChat": false,
    "chatAndUsers": false,
    "lang": "de"
  },

  /* Pad Shortcut Keys */
  "padShortcutEnabled" : {
    "altF9"     : true, /* focus on the File Menu and/or editbar */
    "altC"      : true, /* focus on the Chat window */
    "cmdShift2" : true, /* shows a gritter popup showing a line author */
    "delete"    : true,
    "return"    : true,
    "esc"       : true, /* in mozilla versions 14-19 avoid reconnecting pad */
    "cmdS"      : true, /* save a revision */
    "tab"       : true, /* indent */
    "cmdZ"      : true, /* undo/redo */
    "cmdY"      : true, /* redo */
    "cmdI"      : true, /* italic */
    "cmdB"      : true, /* bold */
    "cmdU"      : true, /* underline */
    "cmd5"      : true, /* strike through */
    "cmdShiftL" : true, /* unordered list */
    "cmdShiftN" : true, /* ordered list */
    "cmdShift1" : true, /* ordered list */
    "cmdShiftC" : true, /* clear authorship */
    "cmdH"      : true, /* backspace */
    "ctrlHome"  : true, /* scroll to top of pad */
    "pageUp"    : true,
    "pageDown"  : true
  },

  /* Should we suppress errors from being visible in the default Pad Text? */
  "suppressErrorsInPadText" : false,

  /* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
  "requireSession" : false,

  /* Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. */
  "editOnly" : false,

  /* Users, who have a valid session, automatically get granted access to password protected pads */
  "sessionNoPassword" : false,

  /* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly,
     but makes it impossible to debug the javascript/css */
  "minify" : true,

  /* How long may clients use served javascript code (in seconds)? Without versioning this
     may cause problems during deployment. Set to 0 to disable caching */
  "maxAge" : 21600, // 60 * 60 * 6 = 6 hours

  /* This is the absolute path to the Abiword executable. Setting it to null, disables abiword.
     Abiword is needed to advanced import/export features of pads*/
  "abiword" : "/usr/bin/abiword",

  /* This is the absolute path to the soffice executable. Setting it to null, disables LibreOffice exporting.
     LibreOffice can be used in lieu of Abiword to export pads */
  "soffice" : null,

  /* This is the path to the Tidy executable. Setting it to null, disables Tidy.
     Tidy is used to improve the quality of exported pads*/
  "tidyHtml" : null,

  /* Allow import of file types other than the supported types: txt, doc, docx, rtf, odt, html & htm */
  "allowUnknownFileEnds" : true,

  /* This setting is used if you require authentication of all users.
     Note: /admin always requires authentication. */
  "requireAuthentication" : false,

  /* Require authorization by a module, or a user with is_admin set, see below. */
  "requireAuthorization" : false,

  /*when you use NginX or another proxy/ load-balancer set this to true*/
  "trustProxy" : false,

  /* Privacy: disable IP logging */
  "disableIPlogging" : false,

  /* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
     message is shown to user. Set to 0 to disable automatic reconnection */
  "automaticReconnectionTimeout" : 0,

  /* Users for basic authentication. is_admin = true gives access to /admin.
     If you do not uncomment this, /admin will not be available! */
  "users": {
    "admin": {
      "password": "adminpassword",
      "is_admin": true
    },
    "user": {
      "password": "changeme1",
      "is_admin": false
    }
  },

  // restrict socket.io transport methods
  "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"],

  // Allow Load Testing tools to hit the Etherpad Instance.  Warning this will disable security on the instance.
  "loadTest": false,

  // Disable indentation on new line when previous line ends with some special chars (':', '[', '(', '{')
  /*
  "indentationOnNewLine": false,
  */

  /* The toolbar buttons configuration.
  "toolbar": {
    "left": [
      ["bold", "italic", "underline", "strikethrough"],
      ["orderedlist", "unorderedlist", "indent", "outdent"],
      ["undo", "redo"],
      ["clearauthorship"]
    ],
    "right": [
      ["importexport", "timeslider", "savedrevision"],
      ["settings", "embed"],
      ["showusers"]
    ],
    "timeslider": [
      ["timeslider_export", "timeslider_returnToPad"]
    ]
  },
  */

  /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
  "loglevel": "INFO",

  //Logging configuration. See log4js documentation for further information
  // https://github.com/nomiddlename/log4js-node
  // You can add as many appenders as you want here:
  "logconfig" :
    { "appenders": [
        { "type": "console"
        //, "category": "access"// only logs pad access
        }
    /*
      , { "type": "file"
      , "filename": "your-log-file-here.log"
      , "maxLogSize": 1024
      , "backups": 3 // how many log files there're gonna be at max
      //, "category": "test" // only log a specific category
        }*/
    /*
      , { "type": "logLevelFilter"
        , "level": "warn" // filters out all log messages that have a lower level than "error"
        , "appender":
          {  Use whatever appender you want here  }
        }*/
    /*
      , { "type": "logLevelFilter"
        , "level": "error" // filters out all log messages that have a lower level than "error"
        , "appender":
          { "type": "smtp"
          , "subject": "An error occurred in your EPL instance!"
          , "recipients": "bar@blurdybloop.com, baz@blurdybloop.com"
          , "sendInterval": 300 // 60 * 5 = 5 minutes -- will buffer log messages; set to 0 to send a mail for every message
          , "transport": "SMTP", "SMTP": { // see https://github.com/andris9/Nodemailer#possible-transport-methods
              "host": "smtp.example.com", "port": 465,
              "secureConnection": true,
              "auth": {
                  "user": "foo@example.com",
                  "pass": "bar_foo"
              }
            }
          }
        }*/
      ]
    }
}

Viel angepasst habe ich nicht: Die Datenbankverbindung, den einführenden Text in jedes Pad mit einem Verweis auf die Benutzerordnung und das Passwort für den administrativen Benutzer.

Ob die Datenbankverbindung glückt, wird erneut aus dem Kontext des Benutzers etherpad geprüft. Danach wird Etherpad als Service eingerichtet.

Dazu erstellt man sich eine Datei /etc/systemd/system/etherpad.service mit folgendem Inhalt:

[Unit]
Description=Etherpad
After=syslog.target network.target

[Service]
Type=simple
User=etherpad
Group=etherpad
WorkingDirectory=/opt/etherpad/etherpad-lite
ExecStart=/usr/bin/nodejs /opt/etherpad/etherpad-lite/node_modules/ep_etherpad-lite/node/server.js
Restart=always

[Install]
WantedBy=multi-user.target

Das entspricht bis auf die Pfade der Anleitung hier: https://github.com/ether/etherpad-lite/wiki/How-to-deploy-Etherpad-Lite-as-a-service 

Gelingt der Start mit service etherpad start kann man mit ufw den Port 9001 zu machen und in den Betrieb übergehen. Wer will kann etherpad auch automatisch starten lassen: systemctl enable etherpad

Der Login-Screen von Etherpad kann in /opt/etherpad/etherpad-lite/src/templates/index.html an die eigenen Wünsche angepasst und z.B. um Links zum Impressum und zur Benutzerordnung erweitert werden.

# ca ab Zeile 160

 <div id="wrapper">
         <% e.begin_block("indexWrapper"); %>
             <div id="inner">
                 <button id="button" onclick="go2Random()" data-l10n-id="index.newPad"></button>
                 <label id="label" for="padname" data-l10n-id="index.createOpenPad"></label>
                 <form action="#" onsubmit="go2Name();return false;">
                     <input type="text" id="padname" maxlength="50" autofocus x-webkit-speech>
                     <button type="submit">OK</button>
                 </form>
                 <p><a href="https://link.zum.impressum" target="_blank">Impressum</a> | <a href="https://link.zur.nutzungsordnung" target="_blank">Benutzerordnung</a></    p>          
             </div>

Nur noch ein Punkt: Das Plugin, das man als schulischer Admin unbedingt haben will, ist das hier: https://www.npmjs.com/package/ep_adminpads

PDF Formulare

Seit acroread nicht mehr in den Repos ist und sich auch nicht mehr ohne Klimmzüge auf aktuelleren Linuxen installieren lässt, sieht es Mau aus mit dem Ausfüllen von PDF-Formularen. Man kann Okular nehmen … und dann damit leben, dass die Formularinhalte als XML Dateien im Homeverzeichnis „verschwinden“, statt direkt beim Dokument zu liegen. Für mich ist das ein No-go, weil ich PDF-Formulare noch Jahre später oder auch auf einer anderen Maschine ausgefüllt ansehen können will.

Eine unfreie aber praktikable Lösung scheint mir hier zu liegen:

https://code-industry.net/free-pdf-editor/

Erste Versuche mit den Beihilfeformularen zeigen, dass sich Master PDF Editor benimmt wie ein Acrobat Reader. Die Formulareinträge landen im PDF. Man kann das PDF also verschieben und kopieren und die Inhalte bleiben erhalten.

Die Installation wiegt mit rund 10MB nicht viel und abgesehen von einigen Qt Abhängigkeiten kommt wenig an Bord.

VM Transfer

EIne VM braucht so seine Zeit bis sie eingerichtet ist. Wenn nur das Netz im Hause lahmt, dann kann man diese zu Hetzner umziehen. Ich hab das mal mit Proxmox (Hetzner) und VirtualBox (im Haus) getestet.

Hier die /etc/network/interfaces des Rootservers bei Hetzner mit den Routing für die VMs:

source /etc/network/interfaces.d/*                                                                                                                                                                   
                                                                                                                                                                                                     
auto lo
iface lo inet loopback

iface lo inet6 loopback

auto enp0s31f6
iface enp0s31f6 inet static
        address  111.111.111.158 # Main Server IP set by Hetzner
        gateway  111.111.111.129 # Main Server Gateway set by Hetzner
        up route add -net 111.111.111.128 netmask 255.255.255.192 gw 111.111.111.129 dev enp0s31f6
        netmask  255.255.255.192 # Netmask set by Hetzner

iface enp0s31f6 inet6 static
        address  2222:333:172:25dd::2 # Main Server IP set by Hetzner
        netmask  64
        gateway  fe80::1

auto vmbr0 # red interface for Internet connection of VMs
iface vmbr0 inet static
        address  111.111.111.158
        netmask  255.255.255.255
        bridge_ports none
        bridge_stp off
        bridge_fd 0
        bridge_maxwait 0
        pre-up brctl addbr vmbr0
        up ip route add 99.99.99.81/32 dev vmbr0 # first VM
        up ip route add 99.99.99.82/32 dev vmbr0 # second VM
        up ip route add 99.99.99.83/32 dev vmbr0 # third VM
        up ip route add 99.99.99.84/32 dev vmbr0 # fourth VM
        up ip route add 99.99.99.85/32 dev vmbr0 # fifth VM
        up ip route add 99.99.99.86/32 dev vmbr0 # sixth VM

auto vmbr1 # green interface for local traffix between VMs
iface vmbr1 inet static
        address  10.16.0.1
        netmask  255.255.0.0
        bridge_ports none
        bridge_stp off
        bridge_fd 0

auto vmbr2 # pink interface for VMs which do NAT only
iface vmbr2 inet static
        address  192.168.0.1
        netmask  255.255.0.0
        bridge_ports none
        bridge_stp off
        bridge_fd 0
        post-up iptables -t nat -A POSTROUTING -s '192.168.0.0/16' -o enp0s31f6 -j MASQUERADE
        post-down iptables -t nat -D POSTROUTING -s '192.168.0.0/16' -o enp0f31f6 -j MASQUERADE

Man erstellt sich in Proxmox eine KVM VM (hier mit der IP 99.99.99.83) mit ausreichend HDD-Platz, um den zu Hause liegenden Server aufzunehmen. Dabei achtet man in der Proxmox-GUI darauf, dass die Einstellungen möglichst genau denen der zu übetragenden VM in VBox entsprechen – z.B. virtio für die Netzwerkkarten.

Dann zieht man sich ein Live-Medium als ISO (bei Hetzner mit wget -4 weil die IPv6 Namensauflösung länger dauert als der Download) auf dem Proxmox-Server nach /var/lib/vz/template/iso und bindet dieses in der KVM-VM in Proxmox ein. Davon dann booten.

Die Wahl des Tastatur-Layouts gelang VNC in Proxmox bei Ubuntu ISOs hierbei nicht immer, weswegen man auch gleich bei US bleiben kann. Bessere Erfahrungen machte ich mit Lubuntu ISOs.

Ist das Live-Medium gebootet, müssen dessen Netzwerkeinstellungen händisch vorgenommen werden. Dabei können IP und Gateway noch über die grafische Oberfläche festgelegt (meist handelt es sich bei den Live-Medien um den Network-Manager) werden. Als DNS kann man z.B. den von Google (8.8.8.8) nutzen. Das Feld für Gateway bleibt leer. Denn: Die Routen für die KVM-VM müssen händisch mit ip route add gesetzt werden.

# address 99.99.99.83 # set this in NM
# netmask 255.255.255.255 # set this in NM
ip route add 111.111.111.158 dev eth0
ip route add default via 111.111.111.158 dev eth0

Meist muss für eth0 noch der Name des Interfaces an den der KVM-VM angepasst werden. Wie das Interface sich im gegebenen Fall nennt, zeigt ein ifconfig.

Netzwerkverbindung testen.

In der KVM-VM wird zu einer root shell gewechselt und für root ein Passwort vergeben

sudo su -
passwd

Dann einen openssh-server installieren und dessen Konfiguration so anpassen, dass sich root über SSH mit Passwort einloggen darf. Den SSH Server neu starten.

Jetzt kann man schon ausprobieren, ob man sich von der lokalen VBox VM auf dem Live-Medium bei Hetzner einloggen kann.

Klappt das, dann kann es mit dem Transfer der lokalen VBox VM losgehen.

Dazu legt man auf der HDD der KVM-VM zuerst ein Dateisystem an. Im Live-Medium kann man hierzu sogar Gparted nutzen.

Dann mounted man die root Partition (müsste /dev/sda1 sein) der KVM-VM nach /mnt und der eigentliche Transfer erfolgt nun in mindestens zwei Schritten, will man die downtime des lokalen Servers klein halten: Einmal einen rsync aus dem laufenden System heraus. Ein zweites mal, nachdem das lokale System ebenfalls mit einem Live-Medium gebootet wurde, um die Veränderungen auch noch zu übertragen. Ist einem die downtime egal, dann nutzt man am besten gleich lokal ein Live-Medium.

screen rsync -e "ssh -o PubkeyAuthentication=no" --numeric-ids --delete --progress -axAhHSP --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found"} / root@99.99.99.83:/mnt/

Ab jetzt sind wieder die im Vorteil, die viel Bandbreite im Upstream haben. In meinem Fall wird die Übertragung 33 Stunden dauern, was dazu führt, dass mir die Zwangstrennung der Internetverbindung die Übertragung in einem Rutsch vermasselt.

Literatur 1, 2, 3

OCR revisited

Zwar liefert Finereader die besseren Ergebnisse und obendrein noch ein Layout für die Scans, aber für eine lokale Suche nach einem PDF reicht auch ein bischen weniger, so dass man sich die Ausgaben bei ABBYY für jede einzelne Seite zumindest teilweise sparen kann.

Unter einem Debian 9:

sudo apt-get install poppler-utils ocrmypdf tesseract-ocr-deu

Details und weitere Konfigurationsmöglichkeiten, Batch-Skripte und mehr sind hier zu haben. Ich setze ocrmypdf bisher gezielt auf einzelne Verzeichnis an mit diesem Einzeiler:

for i in $( ls *.pdf ) ; do ocrmypdf --skip-text -l deu --deskew --clean --rotate-pages --clean-final $i - | pdftotext - $i.txt ; done

Das ergibt dann TXT Dateien mit zu über 95% richtig erkanntem Inhalt, wenn die Vorlage gut ist. Presst man PDF-Faxe und ähnlichen Mist durch die tool chain, dann kommt leider weitaus weniger Brauchbares hinten raus – aber zum Wiederfinden auf der lokalen Platte mit recoll reicht es.

Falsches Userhome im LDAP

Nach Beendigung des Klassenarbeitsmodus auf einer LD vergisst diese gelegentlich, die richtigen Pfade zurück in den LDAP Baum zu schreiben. Meldet sich der Benutzer danach an, erhält er ein Homeverzeichnis, das dem aus dem Klassenarbeitsmodus entspricht

homeDirectory: /home/dynamic/Benutzer/lehrer/_Klassenarbeiten_/InfKA2

und kommt an seine eigenen Dateien nicht mehr ran. Außerdem hat er keinen Zugriff auf die Tauschverzeichnisse.

Einfachster Weg: Den LDAP Baum in eine Datei kippen

ldapsearch -U admin > ldap-tree

Den Dump dann mit grep und Freunden nach „Klassenarbeit“ durchsuchen und die Fundstellen zusammen mit den betroffenen Benutzern inklusive username notieren.

Auf einem Client jxplorer installieren (der sich in den Ubuntu Repos befindet) und sich mit diesem mit dem LDAP auf dem Server verbinden. Der anzugebende LDAP Server ist hierbei die IP des Schulservers / LD-Servers (also z.B. 10.16.1.1), als Benutzer nehmen wir den ldap-admin, dessen genaue Bezeichnung / DN wir aus dem gerade exportierten LDAP Baum nehmen können

cn=ldap-admin,dc=kvfg,dc=tue,dc=schule-bw,dc=de

Verwendet man für diesen das Passwort des admin (Administrator), dann erhält man nur lesenden Zugriff. Schreibenden Zugriff bekommt man mit dem Passwort, das in

/etc/ldap.secret

liegt.  Hier dann im Bäumchen users unter

homeDirectory: /home/dynamic/Benutzer/benutzername

eintragen – und nicht den Eintrag aus ldRealHomeDirectory übernehmen, der nur den Serverpfad enthält. Auf dem LD-Server ist der Pfad oben ein Symlink.

Am Ende des Prozesses noch durch die Homeverzeichnisse – auch den ver-symlinkten Teil unterhalb von /home/dynamic – durchsehen und nach evtl. noch bestehenden Symlinks auf das ehemalige Klassenarbeitsverzeichnis suchen. Diese löschen.