Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebServer: Allow client to send many requests on the same connection #7414

Merged
merged 5 commits into from
Jul 16, 2020

Conversation

ZakCodes
Copy link
Contributor

Closes #7412.

Solution

Instead of telling the client to close the connection and waiting 2s for the client to do it before accepting another one, this allows the client to send another request within the 2s if there were no other clients waiting to connect to the server when the response was sent.

By default, this isn't activated. To activate it, the programmer must call ESP8266WebServer::keepAlive(true) in the handler during the request.

As mentioned before, if there are any other clients waiting to send a request to the server when the response is sent, the server won't accept any other request from the client. This is important because the server doesn't support having multiple concurrent connections, therefore, without this, a client could stay connected to the server forever and block all the other clients.

Testing code

Arduino HTTPS server code (before patch)

Here's the code to upload on your ESP8266 before applying the patch in this pull request.

#include <Arduino.h>
#include <ESP8266WebServerSecure.h>
#include <ESP8266WiFi.h>

static const uint8_t PRIVATE_KEY[] = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAMaLGUw9UMkni86+fipZS3zoJwza4/nDJkOQeC8M31yb35fISva6
4d2K1HLMIBl4ViaNSd1RElzRJifSy2bIdcMCAwEAAQJBAIaD44Xl3QAMTQqrwWsL
yLs9xodNHjwv3ZLVJLgr7oEc3yUeCv4q28AwlYDOO04OoT53GAS3m1qYv4FG7jox
VaECIQD6/9e2s4WqUpOrmFdRz3AMZx5LbzMrfwYO/yEztNoLCQIhAMp/t8DxqVK9
Cu/h03x/gIal9alpv6uD3MRU4MfC6FFrAiEA9/01NQcUJl8mFaETjPoF+8saTG+W
v//ljYWXWU3zLHkCIHe3RBJsjHcezg19i8Npublg+jhbDXa/8U+dAnr27tPbAiEA
uysakbOSKKk6NyF7zujFEu4yhgnoqrVwYb78RunsVSM=
-----END RSA PRIVATE KEY-----
)EOF";

static const uint8_t CERTIFICATE[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIICBTCCAa+gAwIBAgIUPtU58ibKekgnRomdBcIJIdYUSZ4wDQYJKoZIhvcNAQEF
BQAwVzELMAkGA1UEBhMCQ0ExEjAQBgNVBAgMCVF1w4PCqWJlYzERMA8GA1UEBwwI
R2F0aW5lYXUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0x
OTA4MjMwMjMzNDhaFw00OTA4MTUwMjMzNDhaMFcxCzAJBgNVBAYTAkNBMRIwEAYD
VQQIDAlRdcODwqliZWMxETAPBgNVBAcMCEdhdGluZWF1MSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAxosZ
TD1QySeLzr5+KllLfOgnDNrj+cMmQ5B4LwzfXJvfl8hK9rrh3YrUcswgGXhWJo1J
3VESXNEmJ9LLZsh1wwIDAQABo1MwUTAdBgNVHQ4EFgQUTNUSlxtCEYth3de5ciL6
qiDdpZcwHwYDVR0jBBgwFoAUTNUSlxtCEYth3de5ciL6qiDdpZcwDwYDVR0TAQH/
BAUwAwEB/zANBgkqhkiG9w0BAQUFAANBAEqW+lzyWOT8cA+dVRwW+BkHguxR1ev6
zYHQwup2cIEwXeArBptlX0wkdjb4bGtwWM1NiqtCHCeCXyQhuPdMCLE=
-----END CERTIFICATE-----
)EOF";

#define WIFI_SSID "your-ssid"
#define WIFI_PASSWORD "your-password"

ESP8266WebServerSecure server(443);

void setup() {
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    Serial.println();

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    uint8_t i = 0;
    while (WiFi.status() != WL_CONNECTED) {
        i++;
        if (i == 21) {
            WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
            Serial.println("Could not connect to " WIFI_SSID);
        }
        delay(500);
    }
    Serial.print("Connected! IP address: ");
    Serial.println(WiFi.localIP());

    server.getServer().setServerKeyAndCert(PRIVATE_KEY, sizeof(PRIVATE_KEY),
                                           CERTIFICATE, sizeof(CERTIFICATE));
    server.on("/", []() {
        server.send(200, "text/plain", "Hey!\r\n");
    });
    server.begin();
}

void loop() {
    server.handleClient();
}

Arduino HTTPS server code (after the patch)

Here's the code that requires the patch in this pull request to compile.

#include <Arduino.h>
#include <ESP8266WebServerSecure.h>
#include <ESP8266WiFi.h>

static const uint8_t PRIVATE_KEY[] = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAMaLGUw9UMkni86+fipZS3zoJwza4/nDJkOQeC8M31yb35fISva6
4d2K1HLMIBl4ViaNSd1RElzRJifSy2bIdcMCAwEAAQJBAIaD44Xl3QAMTQqrwWsL
yLs9xodNHjwv3ZLVJLgr7oEc3yUeCv4q28AwlYDOO04OoT53GAS3m1qYv4FG7jox
VaECIQD6/9e2s4WqUpOrmFdRz3AMZx5LbzMrfwYO/yEztNoLCQIhAMp/t8DxqVK9
Cu/h03x/gIal9alpv6uD3MRU4MfC6FFrAiEA9/01NQcUJl8mFaETjPoF+8saTG+W
v//ljYWXWU3zLHkCIHe3RBJsjHcezg19i8Npublg+jhbDXa/8U+dAnr27tPbAiEA
uysakbOSKKk6NyF7zujFEu4yhgnoqrVwYb78RunsVSM=
-----END RSA PRIVATE KEY-----
)EOF";

static const uint8_t CERTIFICATE[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIICBTCCAa+gAwIBAgIUPtU58ibKekgnRomdBcIJIdYUSZ4wDQYJKoZIhvcNAQEF
BQAwVzELMAkGA1UEBhMCQ0ExEjAQBgNVBAgMCVF1w4PCqWJlYzERMA8GA1UEBwwI
R2F0aW5lYXUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0x
OTA4MjMwMjMzNDhaFw00OTA4MTUwMjMzNDhaMFcxCzAJBgNVBAYTAkNBMRIwEAYD
VQQIDAlRdcODwqliZWMxETAPBgNVBAcMCEdhdGluZWF1MSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAxosZ
TD1QySeLzr5+KllLfOgnDNrj+cMmQ5B4LwzfXJvfl8hK9rrh3YrUcswgGXhWJo1J
3VESXNEmJ9LLZsh1wwIDAQABo1MwUTAdBgNVHQ4EFgQUTNUSlxtCEYth3de5ciL6
qiDdpZcwHwYDVR0jBBgwFoAUTNUSlxtCEYth3de5ciL6qiDdpZcwDwYDVR0TAQH/
BAUwAwEB/zANBgkqhkiG9w0BAQUFAANBAEqW+lzyWOT8cA+dVRwW+BkHguxR1ev6
zYHQwup2cIEwXeArBptlX0wkdjb4bGtwWM1NiqtCHCeCXyQhuPdMCLE=
-----END CERTIFICATE-----
)EOF";

#define WIFI_SSID "your-ssid"
#define WIFI_PASSWORD "your-password"

ESP8266WebServerSecure server(443);

void setup() {
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    Serial.println();

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    uint8_t i = 0;
    while (WiFi.status() != WL_CONNECTED) {
        i++;
        if (i == 21) {
            WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
            Serial.println("Could not connect to " WIFI_SSID);
        }
        delay(500);
    }
    Serial.print("Connected! IP address: ");
    Serial.println(WiFi.localIP());

    server.getServer().setServerKeyAndCert(PRIVATE_KEY, sizeof(PRIVATE_KEY),
                                           CERTIFICATE, sizeof(CERTIFICATE));
    server.on("/", []() {
        server.keepAlive(true);
        server.send(200, "text/plain", "Hey!\r\n");
    });
    server.begin();
}

void loop() {
    server.handleClient();
}

Ruby client benchmark code

require 'net/http'
require 'benchmark'

DOMAIN = "<your controller's IP>"
TIMES = 100

uri = URI("https://#{DOMAIN}/")

request = Net::HTTP::Get.new(uri)

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# Allow self signed certificates
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

Benchmark.bm(12) do |bm|
  request["Connection"] = "keep-alive"
  bm.report("keep-alive:") {
    http.start do |http|
      TIMES.times do |_|
        response = http.request(request)
      end
    end
  }

  request["Connection"] = "close"
  bm.report("close:") {
    TIMES.times do |_|
      http.start do |http|
        http.get(uri.path)
      end
    end
  }
end

Bash curl clients

while true; do curl -k https://$YOUR_IP/; done

Testing procedure and result

Before the patch

These tests are to be done on the version of this repository before applying the patch in this pull request.

Single client test

Process

  • Upload the Arduino sketch for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute the ruby benchmark script in a terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.239742   0.094293   0.334035 ( 29.232077)
close:         0.280106   0.082856   0.362962 ( 28.905040)

Multiple clients test

Process

  • Upload the Arduino sketch for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute while true; do curl -k https://$YOUR_IP/; done in a terminal with the IP of your ESP8266.
  • Execute the ruby benchmark script in another terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.255995   0.079367   0.335362 ( 57.122717)
close:         0.287268   0.053903   0.341171 ( 56.539017)

After the patch

These tests are to be done on the version of this repository after applying the patch in this pull request.

Single client test with the pre-patch sketch

Process

  • Upload the Arduino sketch for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute the ruby benchmark script in a terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.323017   0.090638   0.413655 ( 29.205106)
close:         0.274748   0.070732   0.345480 ( 28.794096)

Single client test with the post-patch sketch

Process

  • Upload the Arduino sketch for the version after the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute the ruby benchmark script in a terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.055521   0.021891   0.077412 (  5.508462)
close:         0.273024   0.058076   0.331100 ( 28.774168)

Multiple clients test with the pre-patch sketch

Process

  • Upload the Arduino for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute while true; do curl -k https://$YOUR_IP/; done in a terminal with the IP of your ESP8266.
  • Execute the ruby benchmark script in another terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.270916   0.083906   0.354822 ( 57.148734)
close:         0.298795   0.058366   0.357161 ( 56.935414)

Multiple clients test with the post-patch sketch

Process

  • Upload the Arduino for the version after the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute while true; do curl -k https://$YOUR_IP/; done in a terminal with the IP of your ESP8266.
  • Execute the ruby benchmark script in another terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.257426   0.094239   0.351665 ( 57.534744)
close:         0.284825   0.064359   0.349184 ( 57.068869)

Conclusion

The results do not show any negative consequences to this pull request. In the worst case scenario, the performance will be the same. In the best case scenario, where a single client can make as many requests as it wants, we see a 5x performance improvement.

This was achieved with a ESP8266 at 160MHz connected to a WiFi network with excellent connection. The client is a desktop computer connected to the WiFi router via an Ethernet cable. There is very little latency in my setup. If we increased the latency, the performance improvement will increase further and there still shouldn't be any negative impact.

@d-a-v
Copy link
Collaborator

d-a-v commented Jun 29, 2020

Thanks!

In ::_prepareHeader() should keepalive be assumed to be false when http version is 1.0 ?
I wonder why you reset it to false at every connection, I would think it is safe to leave it to true once for all.
It is false by default, that's OK if we merge this for core-v2.7.2, but I would set it to true by default in core-v3.

@ZakCodes
Copy link
Contributor Author

Just like the headers for a response are reset for each connection, I thought that it would be better to decide whether to keep a connection alive for each connection. Also, it's important to set it to false if there's another incoming connection, so we can't let the user assume that they can set keepAlive to true once when the server is being setup and that it will be true forever.

However, it would be a good idea to set it to true by default in core-v3.0.0 just like you said. Do you want me to do another pull request for that?

@d-a-v
Copy link
Collaborator

d-a-v commented Jul 10, 2020

I thought that it would be better to decide whether to keep a connection alive for each connection.

That's true but I don't understand what prevents it to be true by default.
You are always checking for a new client, then you disable keep-alive when there's one.
Isn't that enough ?

However, it would be a good idea to set it to true by default in core-v3.0.0 just like you said. Do you want me to do another pull request for that?

You can update this PR with the new default value. 2.7.2 is released and we are now in 3.0.0-dev.

(you need only to commit and push your changes, maybe after pulling this PR to your local-repo since it was updated by GH)

@ZakCodes
Copy link
Contributor Author

I've made changes to keep the connection alive by default. Does this look good to you?

@d-a-v
Copy link
Collaborator

d-a-v commented Jul 10, 2020

Does this look good to you?

The "Default to false" comment needs change.

I wonder if this test

    if (_keepAlive && _server.hasClient()) { // Disable keep alive if another client is waiting.
      _keepAlive = false;
    }

should also check _currentVersion (0 for http/1.0, 1 for http/1.1)

    if (_currentVersion == 0 || _server.hasClient()) { // Disable keep alive if HTTP/1.0 or another client is waiting.
      _keepAlive = false;
    }

Apart from that it looks good !

@ZakCodes
Copy link
Contributor Author

I get what you mean.

I changed the place where _keepAlive's value is initialized for each request. When the request is parsed, _keepAlive will be set to true if the HTTP version is above 1.0 and it will be set to false otherwise. Then, the received headers will be checked to see whether the Connection header has been received from the client. If the client has sent Connection: keep-alive, _keepAlive is set to true, if Connection: {anything other than keep-alive} is received, _keepAlive is set to false.

Copy link
Collaborator

@earlephilhower earlephilhower left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is awesome, @ZakCodes . Very good test procedure and writeup!

Unfortunately, we don't have documentation on the WebServer class so users need to look at the examples to figure it out.

Could you please modify one or more of the HTTPS examples to include this call with a little comment about it?

That would make it more widely used (since many people begin w/an example) and easier to grok.

@ZakCodes
Copy link
Contributor Author

As mentioned in my last comment, the keep-alive feature that I've implemented is now automatically activated if the HTTP 1.1 or above or if the Connection: Keep-Alive header is in the request, so everyone will see the benefits of this PR when they update to the new version of this library without having to do anything else.

However, we could show an example of disabling it. Disabling it is useful when a request couldn't be authenticated. By disabling it, you force the attacker to make another full TLS handshake to try another password which would then require more time for the attacker to crack the password.

Therefore, we could disable keep-alive when the authentication is failed in the SimpleAuthentication example.
We could also disable keep-alive in ESP8266WebServerTemplate::requestAuthentication.

@earlephilhower
Copy link
Collaborator

Thanks for the clarification, @ZakCodes . I don't think we need to show disabling because in general it's not a good thing to do performance-wise and there's not really a downside I can see now.

@Legion2
Copy link

Legion2 commented Jul 16, 2020

@ZakCodes Passwords should not be that simple that they can be cracked by a brute-force attack. Then you should use better passwords instead.

@earlephilhower earlephilhower merged commit 3c1bd65 into esp8266:master Jul 16, 2020
@TD-er
Copy link
Contributor

TD-er commented Jul 16, 2020

"Brute force attack" on an ESP node also sounds like a good way to get its service down.
Especially with TLS enabled, you don't need much 'brute' force.

@ZakCodes
Copy link
Contributor Author

@ZakCodes Passwords should not be that simple that they can be cracked by a brute-force attack. Then you should use better passwords instead.

Good point

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow HTTP client to send other requests if no other clients are waiting
5 participants