From 3de9ccf190e22f46d6ba7b7167d32ee5c8505bd6 Mon Sep 17 00:00:00 2001
From: antirez <antirez@gmail.com>
Date: Thu, 21 Feb 2019 17:23:17 +0100
Subject: [PATCH 1/6] Gopher: config setting to turn support on/off.

---
 src/config.c | 8 ++++++++
 src/server.c | 1 +
 src/server.h | 3 +++
 3 files changed, 12 insertions(+)

diff --git a/src/config.c b/src/config.c
index 68a2ea22..8fe5cdbb 100644
--- a/src/config.c
+++ b/src/config.c
@@ -216,6 +216,10 @@ void loadServerConfigFromString(char *config) {
             if ((server.protected_mode = yesnotoi(argv[1])) == -1) {
                 err = "argument must be 'yes' or 'no'"; goto loaderr;
             }
+        } else if (!strcasecmp(argv[0],"gopher-enabled") && argc == 2) {
+            if ((server.gopher_enabled = yesnotoi(argv[1])) == -1) {
+                err = "argument must be 'yes' or 'no'"; goto loaderr;
+            }
         } else if (!strcasecmp(argv[0],"port") && argc == 2) {
             server.port = atoi(argv[1]);
             if (server.port < 0 || server.port > 65535) {
@@ -1141,6 +1145,8 @@ void configSetCommand(client *c) {
 #endif
     } config_set_bool_field(
       "protected-mode",server.protected_mode) {
+    } config_set_bool_field(
+      "gopher-enabled",server.gopher_enabled) {
     } config_set_bool_field(
       "stop-writes-on-bgsave-error",server.stop_writes_on_bgsave_err) {
     } config_set_bool_field(
@@ -1472,6 +1478,7 @@ void configGetCommand(client *c) {
     config_get_bool_field("activerehashing", server.activerehashing);
     config_get_bool_field("activedefrag", server.active_defrag_enabled);
     config_get_bool_field("protected-mode", server.protected_mode);
+    config_get_bool_field("gopher-enabled", server.gopher_enabled);
     config_get_bool_field("repl-disable-tcp-nodelay",
             server.repl_disable_tcp_nodelay);
     config_get_bool_field("repl-diskless-sync",
@@ -2301,6 +2308,7 @@ int rewriteConfig(char *path) {
     rewriteConfigYesNoOption(state,"activerehashing",server.activerehashing,CONFIG_DEFAULT_ACTIVE_REHASHING);
     rewriteConfigYesNoOption(state,"activedefrag",server.active_defrag_enabled,CONFIG_DEFAULT_ACTIVE_DEFRAG);
     rewriteConfigYesNoOption(state,"protected-mode",server.protected_mode,CONFIG_DEFAULT_PROTECTED_MODE);
+    rewriteConfigYesNoOption(state,"gopher-enabled",server.gopher_enabled,CONFIG_DEFAULT_GOPHER_ENABLED);
     rewriteConfigClientoutputbufferlimitOption(state);
     rewriteConfigNumericalOption(state,"hz",server.config_hz,CONFIG_DEFAULT_HZ);
     rewriteConfigYesNoOption(state,"aof-rewrite-incremental-fsync",server.aof_rewrite_incremental_fsync,CONFIG_DEFAULT_AOF_REWRITE_INCREMENTAL_FSYNC);
diff --git a/src/server.c b/src/server.c
index 89e79504..fd9d856b 100644
--- a/src/server.c
+++ b/src/server.c
@@ -2222,6 +2222,7 @@ void initServerConfig(void) {
     server.ipfd_count = 0;
     server.sofd = -1;
     server.protected_mode = CONFIG_DEFAULT_PROTECTED_MODE;
+    server.gopher_enabled = CONFIG_DEFAULT_GOPHER_ENABLED;
     server.dbnum = CONFIG_DEFAULT_DBNUM;
     server.verbosity = CONFIG_DEFAULT_VERBOSITY;
     server.maxidletime = CONFIG_DEFAULT_CLIENT_TIMEOUT;
diff --git a/src/server.h b/src/server.h
index 99495265..daa0b004 100644
--- a/src/server.h
+++ b/src/server.h
@@ -121,6 +121,7 @@ typedef long long mstime_t; /* millisecond time type. */
 #define CONFIG_DEFAULT_UNIX_SOCKET_PERM 0
 #define CONFIG_DEFAULT_TCP_KEEPALIVE 300
 #define CONFIG_DEFAULT_PROTECTED_MODE 1
+#define CONFIG_DEFAULT_GOPHER_ENABLED 0
 #define CONFIG_DEFAULT_LOGFILE ""
 #define CONFIG_DEFAULT_SYSLOG_ENABLED 0
 #define CONFIG_DEFAULT_STOP_WRITES_ON_BGSAVE_ERROR 1
@@ -1054,6 +1055,8 @@ struct redisServer {
     dict *migrate_cached_sockets;/* MIGRATE cached sockets */
     uint64_t next_client_id;    /* Next client unique ID. Incremental. */
     int protected_mode;         /* Don't accept external connections. */
+    int gopher_enabled;         /* If true the server will reply to gopher
+                                   queries. Will still serve RESP2 queries. */
     /* RDB / AOF loading information */
     int loading;                /* We are loading data from disk if true */
     off_t loading_total_bytes;

From e00b22e090c68434b9f44986b2ac9fa9b8d96896 Mon Sep 17 00:00:00 2001
From: antirez <antirez@gmail.com>
Date: Thu, 21 Feb 2019 23:13:08 +0100
Subject: [PATCH 2/6] Gopher: initial request handling.

---
 src/Makefile     |  2 +-
 src/gopher.c     | 74 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/networking.c |  8 ++++++
 src/server.h     |  1 +
 4 files changed, 84 insertions(+), 1 deletion(-)
 create mode 100644 src/gopher.c

diff --git a/src/Makefile b/src/Makefile
index adf32d55..d4874f7c 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -164,7 +164,7 @@ endif
 
 REDIS_SERVER_NAME=redis-server
 REDIS_SENTINEL_NAME=redis-sentinel
-REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o acl.o
+REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o acl.o gopher.o
 REDIS_CLI_NAME=redis-cli
 REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o anet.o ae.o crc64.o siphash.o crc16.o
 REDIS_BENCHMARK_NAME=redis-benchmark
diff --git a/src/gopher.c b/src/gopher.c
new file mode 100644
index 00000000..a1b04ed1
--- /dev/null
+++ b/src/gopher.c
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2019, Salvatore Sanfilippo <antirez at gmail dot com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *   * Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *   * Neither the name of Redis nor the names of its contributors may be used
+ *     to endorse or promote products derived from this software without
+ *     specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "server.h"
+
+/* Emit an item in Gopher directory listing format:
+ * <type><descr><TAB><selector><TAB><hostname><TAB><port>
+ * If descr or selector are NULL, then the "(NULL)" string is used instead. */
+void addReplyGopherItem(client *c, const char *type, const char *descr,
+                        const char *selector, const char *hostname, int port)
+{
+    sds item = sdscatfmt(sdsempty(),"%s%s\t%s\t%s\t%i\r\n",
+                         type, descr,
+                         selector ? selector : "(NULL)",
+                         hostname ? hostname : "(NULL)",
+                         port);
+    addReplyProto(c,item,sdslen(item));
+    sdsfree(item);
+}
+
+/* This is called by processInputBuffer() when an inline request is processed
+ * with Gopher mode enabled, and the request happens to have zero or just one
+ * argument. In such case we get the relevant key and reply using the Gopher
+ * protocol. */
+void processGopherRequest(client *c) {
+    robj *keyname = c->argc == 0 ? createStringObject("/",1) : c->argv[1];
+    robj *o = lookupKeyRead(c->db,keyname);
+
+    /* If there is no such key, return with a Gopher error. */
+    if (o == NULL || o->type != OBJ_STRING) {
+        char *errstr;
+        if (o == NULL)
+            errstr = "Error: no content at the specified key";
+        else
+            errstr = "Error: selected key type is invalid "
+                     "for Gopher output";
+        addReplyGopherItem(c,"i",errstr,NULL,NULL,0);
+        addReplyGopherItem(c,"i","Redis Gopher server",NULL,NULL,0);
+    } else {
+    }
+
+    /* Cleanup, also make sure to emit the final ".CRLF" line. Note that
+     * the connection will be closed immediately after this because the client
+     * will be flagged with CLIENT_CLOSE_AFTER_REPLY, in accordance with the
+     * Gopher protocol. */
+    if (c->argc == 0) decrRefCount(keyname);
+    addReplyProto(c,".\r\n",3);
+}
diff --git a/src/networking.c b/src/networking.c
index 23bc97ee..17f46bb2 100644
--- a/src/networking.c
+++ b/src/networking.c
@@ -1564,6 +1564,14 @@ void processInputBuffer(client *c) {
 
         if (c->reqtype == PROTO_REQ_INLINE) {
             if (processInlineBuffer(c) != C_OK) break;
+            /* If the Gopher mode and we got zero or one argument, process
+             * the request in Gopher mode. */
+            if (server.gopher_enabled && (c->argc == 1 || c->argc == 0)) {
+                processGopherRequest(c);
+                resetClient(c);
+                c->flags |= CLIENT_CLOSE_AFTER_REPLY;
+                break;
+            }
         } else if (c->reqtype == PROTO_REQ_MULTIBULK) {
             if (processMultibulkBuffer(c) != C_OK) break;
         } else {
diff --git a/src/server.h b/src/server.h
index daa0b004..8b2e524e 100644
--- a/src/server.h
+++ b/src/server.h
@@ -1514,6 +1514,7 @@ void setDeferredAttributeLen(client *c, void *node, long length);
 void setDeferredPushLen(client *c, void *node, long length);
 void processInputBuffer(client *c);
 void processInputBufferAndReplicate(client *c);
+void processGopherRequest(client *c);
 void acceptHandler(aeEventLoop *el, int fd, void *privdata, int mask);
 void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask);
 void acceptUnixHandler(aeEventLoop *el, int fd, void *privdata, int mask);

From f94d711c8373288995623090d83a02bfdd347342 Mon Sep 17 00:00:00 2001
From: antirez <antirez@gmail.com>
Date: Fri, 22 Feb 2019 10:21:24 +0100
Subject: [PATCH 3/6] Gopher: basic serving of string type.

---
 src/gopher.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/gopher.c b/src/gopher.c
index a1b04ed1..fad2d106 100644
--- a/src/gopher.c
+++ b/src/gopher.c
@@ -49,7 +49,7 @@ void addReplyGopherItem(client *c, const char *type, const char *descr,
  * argument. In such case we get the relevant key and reply using the Gopher
  * protocol. */
 void processGopherRequest(client *c) {
-    robj *keyname = c->argc == 0 ? createStringObject("/",1) : c->argv[1];
+    robj *keyname = c->argc == 0 ? createStringObject("/",1) : c->argv[0];
     robj *o = lookupKeyRead(c->db,keyname);
 
     /* If there is no such key, return with a Gopher error. */
@@ -63,6 +63,7 @@ void processGopherRequest(client *c) {
         addReplyGopherItem(c,"i",errstr,NULL,NULL,0);
         addReplyGopherItem(c,"i","Redis Gopher server",NULL,NULL,0);
     } else {
+        addReply(c,o);
     }
 
     /* Cleanup, also make sure to emit the final ".CRLF" line. Note that

From 8b087dff3409969210db1299a7aa4a504e3e2fd8 Mon Sep 17 00:00:00 2001
From: antirez <antirez@gmail.com>
Date: Fri, 22 Feb 2019 11:17:39 +0100
Subject: [PATCH 4/6] Gopher: TODO list.

---
 src/gopher.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/gopher.c b/src/gopher.c
index fad2d106..81a98448 100644
--- a/src/gopher.c
+++ b/src/gopher.c
@@ -27,6 +27,15 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
+/* TODO:
+ *
+ * - Replace ".<CR><LF>" with ",<CR><LF>" in documents to avoid early stop.
+ * - Allow to configure a gopher-hostname, so that it can be used as default
+ *   for streams converted into listings.
+ * - If __gopher_header__ and/or __gopher_footer__ are defined, they are
+ *   put before/after directory listings generated by streams.
+ * - Find useful ways to convert the other Redis types to gopher output. */
+
 #include "server.h"
 
 /* Emit an item in Gopher directory listing format:

From e247c9ac6ac5dafc3127a1d1fbe696f5087fbc38 Mon Sep 17 00:00:00 2001
From: antirez <antirez@gmail.com>
Date: Sun, 24 Feb 2019 21:38:15 +0100
Subject: [PATCH 5/6] Gopher: don't add the Lastline.

---
 src/gopher.c | 33 +++++++++++++++++++++++----------
 1 file changed, 23 insertions(+), 10 deletions(-)

diff --git a/src/gopher.c b/src/gopher.c
index 81a98448..38e44f75 100644
--- a/src/gopher.c
+++ b/src/gopher.c
@@ -27,15 +27,6 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-/* TODO:
- *
- * - Replace ".<CR><LF>" with ",<CR><LF>" in documents to avoid early stop.
- * - Allow to configure a gopher-hostname, so that it can be used as default
- *   for streams converted into listings.
- * - If __gopher_header__ and/or __gopher_footer__ are defined, they are
- *   put before/after directory listings generated by streams.
- * - Find useful ways to convert the other Redis types to gopher output. */
-
 #include "server.h"
 
 /* Emit an item in Gopher directory listing format:
@@ -80,5 +71,27 @@ void processGopherRequest(client *c) {
      * will be flagged with CLIENT_CLOSE_AFTER_REPLY, in accordance with the
      * Gopher protocol. */
     if (c->argc == 0) decrRefCount(keyname);
-    addReplyProto(c,".\r\n",3);
+
+    /* Note that in theory we should terminate the Gopher request with
+     * ".<CR><LF>" (called Lastline in the RFC) like that:
+     *
+     * addReplyProto(c,".\r\n",3);
+     *
+     * However after examining the current clients landscape, it's probably
+     * going to do more harm than good for several reasons:
+     *
+     * 1. Clients should not have any issue with missing .<CR><LF> as for
+     *    specification, and in the real world indeed certain servers
+     *    implementations never used to send the terminator.
+     *
+     * 2. Redis does not know if it's serving a text file or a binary file:
+     *    at the same time clients will not remove the ".<CR><LF>" bytes at
+     *    tne end when downloading a binary file from the server, so adding
+     *    the "Lastline" terminator without knowing the content is just
+     *    dangerous.
+     *
+     * 3. The utility gopher2redis.rb that we provide for Redis, and any
+     *    other similar tool you may use as Gopher authoring system for
+     *    Redis, can just add the "Lastline" when needed.
+     */
 }

From 40a01a945d0161efe2b9206f1cfae84256f2d822 Mon Sep 17 00:00:00 2001
From: antirez <antirez@gmail.com>
Date: Mon, 25 Feb 2019 17:20:43 +0100
Subject: [PATCH 6/6] Gopher: document the feature in redis.conf.

---
 redis.conf | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/redis.conf b/redis.conf
index e4de9ac6..5ea91590 100644
--- a/redis.conf
+++ b/redis.conf
@@ -1183,6 +1183,61 @@ latency-monitor-threshold 0
 #  specify at least one of K or E, no events will be delivered.
 notify-keyspace-events ""
 
+############################### GOPHER SERVER #################################
+
+# Redis contains an implementation of the Gopher protocol, as specified in
+# the RFC 1436 (https://www.ietf.org/rfc/rfc1436.txt).
+#
+# The Gopher protocol was very popular in the late '90s. It is an alternative 
+# to the web, and the implementation both server and client side is so simple
+# that the Redis server has just 100 lines of code in order to implement this
+# support.
+#
+# What do you do with Gopher nowadays? Well Gopher never *really* died, and
+# lately there is a movement in order for the Gopher more hierarchical content
+# composed of just plain text documents to be resurrected. Some want a simpler
+# internet, others believe that the mainstream internet became too much
+# controlled, and it's cool to create an alternative space for people that
+# want a bit of fresh air.
+#
+# Anyway for the 10nth birthday of the Redis, we gave it the Gopher protocol
+# as a gift.
+#
+# --- HOW IT WORKS? ---
+#
+# The Redis Gopher support uses the inline protocol of Redis, and specifically
+# two kind of inline requests that were anyway illegal: an empty request
+# or any request that starts with "/" (there are no Redis commands starting
+# with such a slash). Normal RESP2/RESP3 requests are completely out of the
+# path of the Gopher protocol implementation and are served as usually as well.
+#
+# If you open a connection to Redis when Gopher is enabled and send it
+# a string like "/foo", if there is a key named "/foo" it is served via the
+# Gopher protocol.
+#
+# In order to create a real Gopher "hole" (the name of a Gopher site in Gopher
+# talking), you likely need a script like the following:
+#
+#   https://github.com/antirez/gopher2redis
+#
+# --- SECURITY WARNING ---
+#
+# If you plan to put Redis on the internet in a publicly accessible address
+# to server Gopher pages MAKE SURE TO SET A PASSWORD to the instance.
+# Once a password is set:
+#
+#   1. The Gopher server (when enabled, not by default) will kill serve
+#      content via Gopher.
+#   2. However other commands cannot be called before the client will
+#      authenticate.
+#
+# So use the 'requirepass' option to protect your instance.
+#
+# To enable Gopher support uncomment the following line and set
+# the option from no (the default) to yes.
+#
+# gopher-enabled no
+
 ############################### ADVANCED CONFIG ###############################
 
 # Hashes are encoded using a memory efficient data structure when they have a