From daf1ced88de6c78aa630b942afafbc2deb542235 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=A8=D0=B8=D0=BF=D0=B8=D1=86=D1=8B=D0=BD=20=D0=90=D0=BD?=
 =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9?= <avsh@get-net.ru>
Date: Tue, 9 Apr 2019 15:51:46 +0500
Subject: [PATCH] httpc: allow to don't auto-follow redirects

While we are here also added forgotten option descriptions to httpc.lua.

@TarantoolBot document
Title: httpc: new 'follow_location' option

When the option is set to `true` (which is default) and a response has
3xx code the http client will automatically issue another request to a
location that a server sends in 'Location' header. If the new response
is 3xx again, the http.client will issue a next request and so on in a
loop until a non-3xx response will be received. This last response will
be returned as a result.

Setting this option to `false` allows to disable this behaviour. In this
case the http client will return a 3xx response itself.

See https://curl.haxx.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html
---
 src/httpc.c                       |  7 +++++++
 src/httpc.h                       | 10 ++++++++++
 src/lua/httpc.c                   |  5 +++++
 src/lua/httpc.lua                 | 16 +++++++++++++---
 test/app-tap/http_client.test.lua | 26 +++++++++++++++++++++++++-
 test/app-tap/httpd.py             |  8 ++++++++
 6 files changed, 68 insertions(+), 4 deletions(-)

diff --git a/src/httpc.c b/src/httpc.c
index b673ec3e8b..65eeaa7439 100644
--- a/src/httpc.c
+++ b/src/httpc.c
@@ -326,6 +326,13 @@ httpc_set_interface(struct httpc_request *req, const char *interface)
 	curl_easy_setopt(req->curl_request.easy, CURLOPT_INTERFACE, interface);
 }
 
+void
+httpc_set_follow_location(struct httpc_request *req, long follow)
+{
+	curl_easy_setopt(req->curl_request.easy, CURLOPT_FOLLOWLOCATION,
+			 follow);
+}
+
 int
 httpc_execute(struct httpc_request *req, double timeout)
 {
diff --git a/src/httpc.h b/src/httpc.h
index 821c739550..7463ff7bad 100644
--- a/src/httpc.h
+++ b/src/httpc.h
@@ -302,6 +302,16 @@ httpc_set_ssl_cert(struct httpc_request *req, const char *ssl_cert);
 void
 httpc_set_interface(struct httpc_request *req, const char *interface);
 
+/**
+ * Specify whether the client will follow 'Location' header that
+ * a server sends as part of an 3xx response.
+ * @param req request
+ * @param follow flag
+ * @see https://curl.haxx.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html
+ */
+void
+httpc_set_follow_location(struct httpc_request *req, long follow);
+
 /**
  * This function does async HTTP request
  * @param request - reference to request object with filled fields
diff --git a/src/lua/httpc.c b/src/lua/httpc.c
index 706b9d90be..c39a2f0dec 100644
--- a/src/lua/httpc.c
+++ b/src/lua/httpc.c
@@ -287,6 +287,11 @@ luaT_httpc_request(lua_State *L)
 		httpc_set_interface(req, lua_tostring(L, -1));
 	lua_pop(L, 1);
 
+	lua_getfield(L, 5, "follow_location");
+	if (!lua_isnil(L, -1) && lua_isboolean(L, -1))
+		httpc_set_follow_location(req, lua_toboolean(L, -1));
+	lua_pop(L, 1);
+
 	if (httpc_execute(req, timeout) != 0) {
 		httpc_request_delete(req);
 		return luaT_error(L);
diff --git a/src/lua/httpc.lua b/src/lua/httpc.lua
index a5d6af3a60..aa76328c92 100644
--- a/src/lua/httpc.lua
+++ b/src/lua/httpc.lua
@@ -237,13 +237,15 @@ end
 --  method  - HTTP method, like GET, POST, PUT and so on
 --  url     - HTTP url, like https://tarantool.org/doc
 --  body    - this parameter is optional, you may use it for passing
+--            data to a server. Like 'My text string!'
 --  options - this is a table of options.
---       data to a server. Like 'My text string!'
 --
 --      ca_path - a path to ssl certificate dir;
 --
 --      ca_file - a path to ssl certificate file;
 --
+--      unix_socket - a path to Unix domain socket;
+--
 --      verify_host - set on/off verification of the certificate's name (CN)
 --          against host;
 --
@@ -269,11 +271,19 @@ end
 --          it is less than 2000 bytes/sec
 --          during 20 seconds;
 --
---      timeout - Time-out the read operation and
+--      timeout - time-out the read operation and
 --          waiting for the curl api request
 --          after this amount of seconds;
 --
---      verbose - set on/off verbose mode
+--      max_header_name_length - maximum length of a response header;
+--
+--      verbose - set on/off verbose mode;
+--
+--      interface - source interface for outgoing traffic;
+--
+--      follow_location - whether the client will follow
+--          'Location' header that a server sends as part of an
+--          3xx response;
 --
 --  Returns:
 --      {
diff --git a/test/app-tap/http_client.test.lua b/test/app-tap/http_client.test.lua
index 12c93399c9..413fc3400a 100755
--- a/test/app-tap/http_client.test.lua
+++ b/test/app-tap/http_client.test.lua
@@ -62,7 +62,7 @@ local function stop_server(test, server)
 end
 
 local function test_http_client(test, url, opts)
-    test:plan(11)
+    test:plan(12)
 
     -- gh-4136: confusing httpc usage error message
     local ok, err = pcall(client.request, client)
@@ -84,6 +84,30 @@ local function test_http_client(test, url, opts)
 
     local r = client.request('GET', url, nil, opts)
     test:is(r.status, 200, 'request')
+
+    -- gh-4119: specify whether to follow 'Location' header
+    test:test('gh-4119: follow location', function(test)
+        test:plan(7)
+        local endpoint = 'redirect'
+
+        -- Verify that the default behaviour is to follow location.
+        local r = client.request('GET', url .. endpoint, nil, opts)
+        test:is(r.status, 200, 'default: status')
+        test:is(r.body, 'hello world', 'default: body')
+
+        -- Verify {follow_location = true} behaviour.
+        local r = client.request('GET', url .. endpoint, nil, merge(opts, {
+                                 follow_location = true}))
+        test:is(r.status, 200, 'follow location: status')
+        test:is(r.body, 'hello world', 'follow location: body')
+
+        -- Verify {follow_location = false} behaviour.
+        local r = client.request('GET', url .. endpoint, nil, merge(opts, {
+                                 follow_location = false}))
+        test:is(r.status, 302, 'do not follow location: status')
+        test:is(r.body, 'redirecting', 'do not follow location: body')
+        test:is(r.headers['location'], '/', 'do not follow location: header')
+    end)
 end
 
 --
diff --git a/test/app-tap/httpd.py b/test/app-tap/httpd.py
index 25d64c093f..dbfddfdd85 100755
--- a/test/app-tap/httpd.py
+++ b/test/app-tap/httpd.py
@@ -15,6 +15,7 @@ def hello():
     body = ["hello world"]
     headers = [('Content-Type', 'application/json')]
     return code, body, headers
+
 def hello1():
     code = "200 OK"
     body = [b"abc"]
@@ -44,12 +45,19 @@ def long_query():
     headers = [('Content-Type', 'application/json')]
     return code, body, headers
 
+def redirect():
+    code = "302 Found"
+    body = "redirecting"
+    headers = [('Location', '/')]
+    return code, body, headers
+
 paths = {
         "/": hello,
         "/abc": hello1,
         "/absent": absent,
         "/headers": headers,
         "/long_query": long_query,
+        "/redirect": redirect,
         }
 
 def read_handle(env, response):
-- 
GitLab