From 3de9ccf190e22f46d6ba7b7167d32ee5c8505bd6 Mon Sep 17 00:00:00 2001 From: antirez 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 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 + * 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: + * + * 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 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 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 "." with "," 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 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 "." with "," 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 + * "." (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 . 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 "." 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 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