Support fuer Telnet option CHARSET

Wenn der Client diese Telnet Option unterstuetzt,
kann er mit dem MG einen Charset fuer die
Verbindung aushandeln.
Diese hat dann Prioritaet vor der manuellen
Einstellung des Spielers.

Change-Id: I6bfbc6aedfbe9cf964876cc246ac9752a43279e2
diff --git a/doc/pcmd/telnet_charset b/doc/pcmd/telnet_charset
index 03b5cd0..3520f81 100644
--- a/doc/pcmd/telnet_charset
+++ b/doc/pcmd/telnet_charset
@@ -22,7 +22,8 @@
 
     Wenn Dein Client die Telnet-Option CHARSET unterstuetzt, kann Dein Client
     den gewuenschten Zeichensatz automatisch aushandeln. Dies tun aber nur
-    sehr wenige Clients.
+    sehr wenige Clients. Wenn Dein Client dies allerdings macht, hat die
+    von Deinem Client ausgehandelte Einstellung aber Prioritaet!
 
     Daher kann diese Einstellung auch manuell mit diesem Befehl eingestellt
     werden. Welche das sein muss, haengt von Deinem Client (und ggf. dessen
@@ -40,8 +41,14 @@
 
     Ohne Argumente wird der aktuelle Zustand angezeigt.
 
+ BEMERKUNG:
+
+    Damit die Aushandlung des Zeichensatzes durch den Client funktioniert,
+    muss dieser neben der Telnet-Option CHARSET, auch die Telnet-Option BINARY
+    unterstuetzen.
+
  SIEHE AUCH:
     telnegs, telnet gmcp,
 
  LETZTE AeNDERUNG:
-    16.01.2020, Zesstra
+    20.01.2020, Zesstra
diff --git a/secure/telnetneg.c b/secure/telnetneg.c
index 2fae3ae..4fea5d0 100644
--- a/secure/telnetneg.c
+++ b/secure/telnetneg.c
@@ -19,9 +19,11 @@
 #define NEED_PROTOTYPES
 #include "/secure/telnetneg.h"
 #undef NEED_PROTOTYPES
+#include <configuration.h>
 
 // unterstuetzte Optionen:
-// TELOPT_EOR, TELOPT_NAWS, TELOPT_LINEMODE, TELOPT_TTYPE, TELOPT_BINARY
+// TELOPT_EOR, TELOPT_NAWS, TELOPT_LINEMODE, TELOPT_TTYPE, TELOPT_BINARY,
+// TELOPT_CHARSET
 
 //#define __DEBUG__ 1
 
@@ -34,7 +36,9 @@
 # define DTN(x,y)
 #endif
 
-
+// first element "" to yield the separator
+#define OFFERED_CHARSETS ({"", "UTF-8", "ISO8859-15", "LATIN-9", "ISO8859-1",\
+                             "LATIN1", "WINDOWS-1252", "US-ASCII"})
 
 // Aus mini_props.c:
 public varargs mixed Query( string str, int type );
@@ -72,6 +76,7 @@
     case TELOPT_STATUS: return "TELOPT_STATUS";
     case TELOPT_TM: return "TELOPT_TM";
     case TELOPT_BINARY: return "TELOPT_BINARY";
+    case TELOPT_CHARSET: return "TELOPT_CHARSET";
     case TELOPT_COMPRESS2: return "TELOPT_COMPRESS2";
     case TELOPT_MSP: return "TELOPT_MSP";
     case TELOPT_MXP: return "TELOPT_MXP";
@@ -337,7 +342,7 @@
 private void _std_re_handler_binary(struct telopt_s opt, int action,
                                    int *data)
 {
-  DTN("tn_binary client-seite",({action}));
+  DTN("binary handler client",({action}));
 }
 
 // Der Handler fuer die BINARY option, wenn sie auf unserer Seite
@@ -347,9 +352,199 @@
 private void _std_lo_handler_binary(struct telopt_s opt, int action,
                                    int *data)
 {
-  DTN("tn_binary mg-seite",({action}));
+  DTN("binary handler mg",({action}));
 }
 
+private int activate_charset(struct telopt_s opt, string charset)
+{
+  // Wenn der Client die Option nicht BINARY nicht unterstuetzt/will, duerfen
+  // wir auch keine nicht-ASCII-Zeichen uebertragen. In diesem Fall ist der
+  // einzige akzeptable Zeichensatz (US-)ASCII.
+  struct telopt_s binary = TN[TELOPT_BINARY];
+  if ( (!binary->state->remoteside || !binary->state->localside)
+       && (upper_case(charset) != "US-ASCII"
+          && upper_case(charset) != "ASCII") )
+  {
+    return 0;
+  }
+  // Wenn der Zeichensatz keine //-Variante ist, machen wir den zu
+  // einer. Das verhindert letztlich eine Menge Laufzeitfehler, wenn ein
+  // Zeichen mal nicht darstellbar ist.
+  if (strstr(charset, "//") == -1)
+    charset += "//TRANSLIT";
+  // Falls das zu sehr scrollt, weil Clients staendig ungueltige/nicht
+  // verwendbare Zeichensaetz schicken, muss das publish weg und ggf. sogar
+  // ein nolog hin...
+  if (!catch(configure_interactive(this_object(), IC_ENCODING, charset);
+             publish))
+  {
+    m_delete(opt->data, "failed_negotiations");
+    opt->data["accepted_charset"] = interactive_info(this_player(),
+                                                     IC_ENCODING);
+    return 1;
+  }
+  return 0;
+}
+#define REQUEST  1
+#define ACCEPTED 2
+#define REJECTED 3
+#define TTABLE_IS 4
+#define TTABLE_REJECTED 5
+// Der Handler fuer die CHARSET option, wenn sie auf/fuer Clientseite
+// aktiviert/deaktivert wird oder fuer empfangene SB.
+private void _std_re_handler_charset(struct telopt_s opt, int action,
+                                   int *data)
+{
+  DTN("charset handler client",({action}));
+
+  // Wenn action == REMOTEON: Ab diesem Moment darf uns der Client einen
+  // CHARSET REQUEST schicken (weil wir haben ihm auch schon ein DO
+  // geschickt).
+  if (action  == REMOTEON)
+  {
+    if (!mappingp(opt->data))
+      opt->data = ([]);
+  }
+  else if (action == REMOTEOFF)
+  {
+    // Wenn auch auf mg-seite aus, kann data geloescht werden.
+    if (!opt->state->localside)
+      opt->data = 0;
+  }
+  else if (action == SB)
+  {
+    mapping statedata = opt->data;
+    // <data> is the part following IAC SB TELOPT_CHARSET
+    switch(data[0])
+    {
+      case REQUEST:
+        // is the client allowed to REQUEST?
+        if (opt->state->remoteside)
+          return;
+        // And enough data?
+        if (sizeof(data) > 1 )
+        {
+          DTN("re_charset request:",data);
+          string *suggestions = explode(to_text(data[2..], "ASCII"),
+                                        sprintf("%c",data[1]));
+          // Wenn UTF-8 drin vorkommt, nehmen wir das. (Gross-/Kleinschreibung
+          // ist egal, aber wir muessen einen identischen String
+          // zurueckschicken). (Gemischte Schreibweise: *ignorier* *stoehn*)
+          string *selected = suggestions & ({"UTF-8","utf-8"});
+          if (sizeof(selected)
+              && activate_charset(opt, selected[0]))
+          {
+            send_telnet_neg(({ SB, TELOPT_CHARSET, ACCEPTED,
+                               to_array(selected[0]) }));
+            return;
+          }
+          else
+          {
+            // die ersten 10 Vorschlaege durchprobieren
+            foreach(string cs : suggestions[0..min(sizeof(suggestions)-1, 10)])
+            {
+              if (activate_charset(opt, cs))
+              {
+                send_telnet_neg(({ SB, TELOPT_CHARSET, ACCEPTED,
+                                   to_array(cs) }));
+                return; // yeay, found one!
+              }
+            }
+            // none acceptable
+            send_telnet_neg(({ SB, TELOPT_CHARSET, REJECTED }));
+            ++opt->data["failed_negotiations"];
+            // fall-through, no return;
+          }
+        }
+        else // malformed message
+        {
+          send_telnet_neg(({ SB, TELOPT_CHARSET, REJECTED }));
+          ++opt->data["failed_negotiations"];
+          // fall-through, no return;
+        }
+        // when arriving here, the negotiation was not successful. Check if
+        // too many unsuccesful tries in a row.
+        if (opt->data["failed_negotiations"] > 10)
+        {
+          send_telnet_neg(({ TELOPT_CHARSET, DONT }));
+          send_telnet_neg(({ TELOPT_CHARSET, WONT }));
+        }
+        break;
+      case ACCEPTED:
+        // great - the client accepted one of our suggested charsets.
+        // Negotiation concluded. However, check if we REQUESTed a charset in
+        // the first place... And if the accepted one is one of our
+        // suggestions
+        if (sizeof(data) > 1)
+        {
+          DTN("re_charset accepted:",data);
+          string charset = upper_case(to_text(data[1..], "ASCII"));
+          string *offered = statedata["offered"];
+          // in any case, we don't need the key in the future.
+          m_delete(statedata, "offered");
+          if (pointerp(offered) && member(offered, charset) > -1)
+          {
+            activate_charset(opt, charset);
+            return;
+          }
+          // else: client did not sent us back one of our suggestions or we
+          // did not REQUEST. :-(
+        }
+        ++opt->data["failed_negotiations"];
+        // else? Huh. malformed message.
+        break;
+      case REJECTED:
+        // none of our suggested charsets were acceptable. Negotiation is
+        // concluded, we keep the current charset (and maybe we will receive a
+        // suggestion of the client)
+        if (member(statedata, "offered"))
+          m_delete(statedata, "offered");
+        ++opt->data["failed_negotiations"];
+        DTN("re_charset_rejected:",data);
+        break;
+      case TTABLE_IS:
+        // we plainly don't support TTABLES
+        send_telnet_neg(({ SB, TELOPT_CHARSET, TTABLE_REJECTED }));
+        ++opt->data["failed_negotiations"];
+        break;
+    }
+  }
+}
+
+// Der Handler fuer die BINARY option, wenn sie auf/fuer unserere Seite
+// aktiviert/deaktivert wird.
+private void _std_lo_handler_charset(struct telopt_s opt, int action,
+                                   int *data)
+{
+  DTN("charset handler mg",({action}));
+  if (action == LOCALON)
+  {
+    // Ab diesem Moment duerfen wir dem Client einen CHARSET REQUEST schicken
+    // (denn wir haben auch schon ein DO erhalten). Und das tun wir auch
+    // direkt.
+    if (!mappingp(opt->data))
+      opt->data = ([ "offered": OFFERED_CHARSETS ]);
+    else
+      opt->data["offered"] = OFFERED_CHARSETS;
+    send_telnet_neg(({ SB, TELOPT_CHARSET, REQUEST })
+                    + to_array(implode(opt->data["offered"], ";"))) ;
+  }
+  else if (action == LOCALOFF)
+  {
+    // ok, keine REQUESTS mehr nach dem LOCALOFF, aber viel muss nicht getan
+    // werden. Wenn auch auf client-seite aus, kann data geloescht werden.
+    if (!opt->state->remoteside)
+      opt->data = 0;
+  }
+  // und SB gibt es nicht in diesem Handler.
+}
+#undef REQUEST
+#undef ACCEPTED
+#undef REJECTED
+#undef TTABLE-IS
+#undef TTABLE-REJECTED
+
+
 // Bindet/registriert Handler fuer die jew. Telnet Option. (Oder loescht sie
 // auch wieder.) Je nach <initneg> wird versucht, die Option neu zu
 // verhandeln.
@@ -406,6 +601,7 @@
     bind_telneg_handler(TELOPT_MSSP, 0, #'_std_lo_handler_mssp, 1);
   // fuer TELOPT_TM jetzt keine Verhandlung anstossen.
   bind_telneg_handler(TELOPT_TM, #'_std_re_handler_tm, 0, 0);
+  // und auch CHARSET wird verzoegert bis das Spielerobjekt da ist.
 }
 
 
@@ -713,6 +909,10 @@
 
       Set( P_TTY_TYPE, Terminals[0] );
   }
+  // und zum Schluss wird der Support fuer CHARSET aktiviert.
+  bind_telneg_handler(TELOPT_CHARSET, #'_std_re_handler_charset,
+                      #'_std_lo_handler_charset, 1);
+
 }
 
 // somehow completely out of the ordinary options processing/negotiation. But
diff --git a/std/player/base.c b/std/player/base.c
index 8273874..f96abd6 100644
--- a/std/player/base.c
+++ b/std/player/base.c
@@ -4298,39 +4298,73 @@
   return 1;
 }
 
-//TODO: beim manuellen Setzen sollte - sofern TELOPT CHARSET ausgehandelt
-//TODO::wurde, versucht werden, diesen neu mit dem Client zu verhandeln...
+// Falls es eine per telnet vom Client ausgehandelte Einstellung fuer CHARSET
+// gibt, hat die manuelle Einstellung von Spielern hier geringere Prioritaet
+// und bildet nur den Fallback.
 private int set_telnet_charset(string enc) {
-  // Wenn es "loeschen" ist, wird die Prop genullt und wir stellen den Default
-  // ein.
+  struct telopt_s tdata = query_telnet_neg()[TELOPT_CHARSET];
   if (!sizeof(enc))
   {
-    tell_object(ME, break_string(sprintf(
+    if (!tdata->data || !tdata->data["accepted_charset"])
+    {
+      tell_object(ME, break_string(sprintf(
+          "Zur Zeit ist der Zeichensatz \'%s\' aktiv. "
+          "Alle Ausgaben an Dich werden in diesem Zeichensatz gesendet "
+          "und wir erwarten alle Eingaben von Dir in diesem Zeichensatz. ",
+          interactive_info(ME, IC_ENCODING)), 78));
+    }
+    else
+    {
+      tell_object(ME, break_string(sprintf(
           "Zur Zeit ist der Zeichensatz \'%s\' aktiv. "
           "Alle Ausgaben an Dich werden in diesem Zeichensatz gesendet "
           "und wir erwarten alle Eingaben von Dir in diesem Zeichensatz. "
-          "Moeglicherweise hat Dein Client diesen Zeichensatz automatisch "
-          "ausgehandelt.", interactive_info(ME, IC_ENCODING)), 78));
+          "Dieser Zeichensatz wurde von Deinem Client ausgehandelt.",
+          interactive_info(ME, IC_ENCODING)), 78));
+      if (QueryProp(P_TELNET_CHARSET))
+        tell_object(ME, break_string(sprintf(
+          "Dein manuell eingestellter Zeichensatz ist \'%s\', welcher "
+          "aber nur genutzt wird, wenn Dein Client keinen Zeichensatz "
+          "aushandelt.", QueryProp(P_TELNET_CHARSET)),78));
+
+    }
   }
+  // Wenn es "loeschen" ist, wird die Prop genullt und wir stellen den Default
+  // ein. Allerdings nur, wenn nix per telnet ausgehandelt wurde, dann wird
+  // das beibehalten.
   else if (lower_case(enc) == "loeschen")
   {
     SetProp(P_TELNET_CHARSET, 0);
-    configure_interactive(ME, IC_ENCODING, interactive_info(0,IC_ENCODING));
-    tell_object(ME, break_string(sprintf(
+    // wurde was per telnet option charset ausgehandelt? dann wird (weiterhin)
+    // das genommen und nicht umgestellt.
+    if (!tdata->data || !tdata->data["accepted_charset"])
+    {
+      configure_interactive(ME, IC_ENCODING, interactive_info(0,IC_ENCODING));
+      tell_object(ME, break_string(sprintf(
           "Der Default \'%s\' wurde wieder hergestellt. "
           "Alle Ausgaben an Dich werden in diesem Zeichensatz gesendet "
           "und wir erwarten alle Eingaben von Dir in diesem Zeichensatz. "
           "Sollte Dein Client die Telnet-Option CHARSET unterstuetzen, kann "
-          "dieser allerdings direkt einen Zeichensatz aushandeln, der dann "
-          "stattdessen gilt.",
+          "dieser allerdings direkt einen Zeichensatz aushandeln oder "
+          "ausgehandelt haben, der dann stattdessen gilt.",
           interactive_info(ME, IC_ENCODING)), 78));
+    }
+    else
+    {
+      tell_object(ME, break_string(sprintf(
+          "Der Default \'%s\' wurde wieder hergestellt. Allerdings hat "
+          "Dein Client mit dem MG den Zeichensatz \'%s\' ausgehandelt, "
+          "welcher immer noch aktiv ist.",
+          interactive_info(0, IC_ENCODING),
+          interactive_info(ME, IC_ENCODING)), 78));
+    }
   }
   else
   {
-    // Wenn der Zeichensatz keine //TRANSLIT-Variante ist, machen wir den zu
+    // Wenn der Zeichensatz keine //-Variante ist, machen wir den zu
     // einer. Das verhindert letztlich eine Menge Laufzeitfehler, wenn ein
     // Zeichen mal nicht darstellbar ist.
-    if (strstr(enc, "//TRANSLIT") == -1)
+    if (strstr(enc, "//") == -1)
       enc += "//TRANSLIT";
     if (catch(configure_interactive(ME, IC_ENCODING, enc); nolog))
     {
@@ -4341,7 +4375,9 @@
     else
     {
       SetProp(P_TELNET_CHARSET, interactive_info(ME, IC_ENCODING));
-      tell_object(ME, break_string(sprintf(
+      if (!tdata->data || !tdata->data["accepted_charset"])
+      {
+        tell_object(ME, break_string(sprintf(
             "Der Zeichensatz \'%s\' wurde eingestellt. Alle Ausgaben an "
             "Dich werden in diesem Zeichensatz gesendet und wir erwarten "
             "alle Eingaben von Dir in diesem Zeichensatz. Sollte Dein "
@@ -4349,7 +4385,22 @@
             "dieser allerdings direkt einen Zeichensatz aushandeln, der "
             "dann stattdessen gilt.",
             interactive_info(ME, IC_ENCODING)),78));
+      }
+      else
+      {
+        // Der via telnet ausgehandelte Charset muss wieder hergestellt
+        // werden.
+        configure_interactive(ME, IC_ENCODING,
+                              tdata->data["accepted_charset"]);
+        tell_object(ME, break_string(sprintf(
+          "Der Zeichensatz \'%s\' wurde gespeichert. Allerdings hat "
+          "Dein Client mit dem MG den Zeichensatz \'%s\' ausgehandelt, "
+          "welcher immer noch aktiv ist.",
+          QueryProp(P_TELNET_CHARSET),
+          interactive_info(ME, IC_ENCODING)), 78));
+      }
     }
+
   }
   return 1;
 }
diff --git a/sys/telnet.h b/sys/telnet.h
index 2779a2a..9950392 100644
--- a/sys/telnet.h
+++ b/sys/telnet.h
@@ -103,6 +103,7 @@
 #define  TELOPT_AUTHENTICATION 37       /* authentication */
 #define  TELOPT_ENCRYPT       38        /* authentication */
 #define  TELOPT_NEWENV        39        /* Environment opt for Port ID */
+#define  TELOPT_CHARSET       42        /* Negotiate about charsets */
 #define  TELOPT_STARTTLS      46        /* Transport Layer Security */
 #define  TELOPT_KERMIT        47        /* Telnet KERMIT */
 #define  TELOPT_SEND_URL      48        /* Send URL */