Skip to content

Gopher selectors with // or /../ or /./ are mangled #790

Description

@abbrev

I have selectors on my Gopher site that contain // or /../ or /./. Lagrange collapses these according to URL path syntax rules, but a selector is not a URL and should not be modified—it should be sent to the server exactly as it's presented to the client in a Gopher menu.

I made a client test for this issue at gopher://asciz.com/1/client-tests/menu ("Query string test 3"). The selector is /client-tests/query3/?http://example.com/a/../b/./c/, but Lagrange modifies it:

Expected request:

    "/client-tests/query3/?http://example.com/a/../b/./c/"

Actual request:

    "/client-tests/query3/?http:/example.com/b/c/"

(Lynx mangles it the same way.)

Here's a proposed fix (in the_Foundation) when converting a selector to a URL path, though I haven't tested it, and it might break some Gemini or other URLs outside of Gopher menus (especially ones that contain .. or . path segments):

diff --git a/src/string.c b/src/string.c
index 9644dea6b..a9f1eafa2 100644
--- a/src/string.c
+++ b/src/string.c
@@ -591,11 +591,22 @@ iString *maybeUrlEncodeExclude_String(const iString *d, const char *excluded) {
     /* TODO: Return NULL if nothing to encode. */
     iString *encoded = new_String();
     /* Note: Any UTF-8 code points are encoded as multiple %NN sequences. */
+    char prevch = '\0';
     for (const char *i = constBegin_String(d), *end = constEnd_String(d); i != end; ++i) {
         char ch = *i;
         if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') ||
             ch == '-' || ch == '_' || ch == '.' || ch == '~' || strchr(excluded, ch)) {
-            appendData_Block(&encoded->chars, i, 1);
+            if (prevch == '/' && ch == '/') {
+                /* Escape doubled slash to prevent collapsing into one slash. */
+                appendCStrN_String(encoded, "%2F", 3);
+            }
+            else if (prevch == '/' && ch == '.') {
+                /* Escape dot after slash to prevent collapsing /../ and /./ segments. */
+                appendCStrN_String(encoded, "%2E", 3);
+            }
+            else {
+                appendData_Block(&encoded->chars, i, 1);
+            }
         }
         else {
             static const char hex[16] = {
@@ -603,6 +614,7 @@ iString *maybeUrlEncodeExclude_String(const iString *d, const char *excluded) {
             char escaped[3] = {'%', hex[(ch >> 4) & 0xf], hex[ch & 0xf]};
             appendCStrN_String(encoded, escaped, 3);
         }
+        prevch = ch;
     }
     return encoded;
 }

Here's an alternative proposed fix which should work in all cases, though it's a bit overkill since it URL-encodes all slashes in a Gopher selector, which is unnecessary most of the time:

diff --git a/src/gopher.c b/src/gopher.c
index 4abad2f4c..300383022 100644
--- a/src/gopher.c
+++ b/src/gopher.c
@@ -171,7 +171,7 @@ static iBool convertSource_Gopher_(iGopher *d) {
                                   isEmpty_Range(&port) ? "70" : cstr_Rangecc(port),
                                   lineType,
                                   cstrCollect_String(
-                                      urlEncodeExclude_String(collectNewRange_String(selector), "/")),
+                                      urlEncode_String(collectNewRange_String(selector))),
                                   cstr_Rangecc(text));
                     append_Block(d->output, utf8_String(buf));
                     iEndCollect();

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions