diff --git a/ArduinoHttpClient.h b/ArduinoHttpClient.h index 2064028..578733f 100644 --- a/ArduinoHttpClient.h +++ b/ArduinoHttpClient.h @@ -6,5 +6,6 @@ #define ArduinoHttpClient_h #include "HttpClient.h" +#include "WebSocketClient.h" #endif diff --git a/HttpClient.cpp b/HttpClient.cpp index 4159b9f..287e8c7 100644 --- a/HttpClient.cpp +++ b/HttpClient.cpp @@ -439,7 +439,7 @@ int HttpClient::responseStatusCode() delay(kHttpWaitForDataDelay); } } - if ( (c == '\n') && (iStatusCode < 200) ) + if ( (c == '\n') && (iStatusCode < 200 && iStatusCode != 101) ) { // We've reached the end of an informational status line c = '\0'; // Clear c so we'll go back into the data reading loop @@ -447,7 +447,7 @@ int HttpClient::responseStatusCode() } // If we've read a status code successfully but it's informational (1xx) // loop back to the start - while ( (iState == eStatusCodeRead) && (iStatusCode < 200) ); + while ( (iState == eStatusCodeRead) && (iStatusCode < 200 && iStatusCode != 101) ); if ( (c == '\n') && (iState == eStatusCodeRead) ) { diff --git a/WebSocketClient.cpp b/WebSocketClient.cpp new file mode 100644 index 0000000..1237b04 --- /dev/null +++ b/WebSocketClient.cpp @@ -0,0 +1,371 @@ +// (c) Copyright Arduino. 2016 +// Released under Apache License, version 2.0 + +#include "b64.h" + +#include "WebSocketClient.h" + +WebSocketClient::WebSocketClient(Client& aClient, const char* aServerName, uint16_t aServerPort) + : HttpClient(aClient, aServerName, aServerPort), + iTxStarted(false), + iRxSize(0) +{ +} + +WebSocketClient::WebSocketClient(Client& aClient, const String& aServerName, uint16_t aServerPort) + : HttpClient(aClient, aServerName, aServerPort), + iTxStarted(false), + iRxSize(0) +{ +} + +WebSocketClient::WebSocketClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort) + : HttpClient(aClient, aServerAddress, aServerPort), + iTxStarted(false), + iRxSize(0) +{ +} + +int WebSocketClient::begin(const char* aPath) +{ + // start the GET request + beginRequest(); + connectionKeepAlive(); + int status = get(aPath); + + if (status == 0) + { + uint8_t randomKey[13]; + char base64RandomKey[21]; + + // create a random key for the connection upgrade + for (int i = 0; i < (int)sizeof(randomKey); i++) + { + randomKey[i] = random(0x01, 0xff); + } + memset(base64RandomKey, 0x00, sizeof(base64RandomKey)); + b64_encode(randomKey, sizeof(randomKey), (unsigned char*)base64RandomKey, sizeof(base64RandomKey)); + + // start the connection upgrade sequence + sendHeader("Upgrade", "websocket"); + sendHeader("Connection", "Upgrade"); + sendHeader("Sec-WebSocket-Key", base64RandomKey); + sendHeader("Sec-WebSocket-Version", "13"); + endRequest(); + + status = responseStatusCode(); + + if (status > 0) + { + skipResponseHeaders(); + } + } + + iRxSize = 0; + + // status code of 101 means success + return (status == 101) ? 0 : status; +} + +int WebSocketClient::begin(const String& aPath) +{ + return begin(aPath.c_str()); +} + +int WebSocketClient::beginMessage(int aType) +{ + if (iTxStarted) + { + // fail TX already started + return 1; + } + + iTxStarted = true; + iTxMessageType = (aType & 0xf); + iTxSize = 0; + + return 0; +} + +int WebSocketClient::endMessage() +{ + if (!iTxStarted) + { + // fail TX not started + return 1; + } + + // send FIN + the message type (opcode) + HttpClient::write(0x80 | iTxMessageType); + + // the message is masked (0x80) + // send the length + if (iTxSize < 126) + { + HttpClient::write(0x80 | (uint8_t)iTxSize); + } + else if (iTxSize < 0xffff) + { + HttpClient::write(0x80 | 126); + HttpClient::write((iTxSize >> 8) & 0xff); + HttpClient::write((iTxSize >> 0) & 0xff); + } + else + { + HttpClient::write(0x80 | 127); + HttpClient::write((iTxSize >> 56) & 0xff); + HttpClient::write((iTxSize >> 48) & 0xff); + HttpClient::write((iTxSize >> 40) & 0xff); + HttpClient::write((iTxSize >> 32) & 0xff); + HttpClient::write((iTxSize >> 24) & 0xff); + HttpClient::write((iTxSize >> 16) & 0xff); + HttpClient::write((iTxSize >> 8) & 0xff); + HttpClient::write((iTxSize >> 0) & 0xff); + } + + uint8_t maskKey[4]; + + // create a random mask for the data and send + for (int i = 0; i < (int)sizeof(maskKey); i++) + { + maskKey[i] = random(0xff); + } + HttpClient::write(maskKey, sizeof(maskKey)); + + // mask the data and send + for (int i = 0; i < (int)iTxSize; i++) { + iTxBuffer[i] ^= maskKey[i % sizeof(maskKey)]; + } + + size_t txSize = iTxSize; + + iTxStarted = false; + iTxSize = 0; + + return (HttpClient::write(iTxBuffer, txSize) == txSize) ? 0 : 1; +} + +size_t WebSocketClient::write(uint8_t aByte) +{ + return write(&aByte, sizeof(aByte)); +} + +size_t WebSocketClient::write(const uint8_t *aBuffer, size_t aSize) +{ + if (iState < eReadingBody) + { + // have not upgraded the connection yet + return HttpClient::write(aBuffer, aSize); + } + + if (!iTxStarted) + { + // fail TX not started + return 0; + } + + // check if the write size, fits in the buffer + if ((iTxSize + aSize) > sizeof(iTxBuffer)) + { + aSize = sizeof(iTxSize) - iTxSize; + } + + // copy data into the buffer + memcpy(iTxBuffer + iTxSize, aBuffer, aSize); + + iTxSize += aSize; + + return aSize; +} + +int WebSocketClient::parseMessage() +{ + flushRx(); + + // make sure 2 bytes (opcode + length) + // are available + if (HttpClient::available() < 2) + { + return 0; + } + + // read open code and length + uint8_t opcode = HttpClient::read(); + int length = HttpClient::read(); + + if ((opcode & 0x0f) == 0) + { + // continuation, use previous opcode and update flags + iRxOpCode |= opcode; + } + else + { + iRxOpCode = opcode; + } + + iRxMasked = (length & 0x80); + length &= 0x7f; + + // read the RX size + if (length < 126) + { + iRxSize = length; + } + else if (length == 126) + { + iRxSize = (HttpClient::read() << 8) | HttpClient::read(); + } + else + { + iRxSize = ((uint64_t)HttpClient::read() << 56) | + ((uint64_t)HttpClient::read() << 48) | + ((uint64_t)HttpClient::read() << 40) | + ((uint64_t)HttpClient::read() << 32) | + ((uint64_t)HttpClient::read() << 24) | + ((uint64_t)HttpClient::read() << 16) | + ((uint64_t)HttpClient::read() << 8) | + (uint64_t)HttpClient::read(); + } + + // read in the mask, if present + if (iRxMasked) + { + for (int i = 0; i < (int)sizeof(iRxMaskKey); i++) + { + iRxMaskKey[i] = HttpClient::read(); + } + } + + iRxMaskIndex = 0; + + if (TYPE_CONNECTION_CLOSE == messageType()) + { + flushRx(); + stop(); + iRxSize = 0; + } + else if (TYPE_PING == messageType()) + { + beginMessage(TYPE_PONG); + while(available()) + { + write(read()); + } + endMessage(); + + iRxSize = 0; + } + else if (TYPE_PONG == messageType()) + { + flushRx(); + iRxSize = 0; + } + + return iRxSize; +} + +int WebSocketClient::messageType() +{ + return (iRxOpCode & 0x0f); +} + +bool WebSocketClient::isFinal() +{ + return ((iRxOpCode & 0x80) != 0); +} + +String WebSocketClient::readString() +{ + int avail = available(); + String s; + + if (avail > 0) + { + s.reserve(avail); + + for (int i = 0; i < avail; i++) + { + s += (char)read(); + } + } + + return s; +} + +int WebSocketClient::ping() +{ + uint8_t pingData[16]; + + // create random data for the ping + for (int i = 0; i < (int)sizeof(pingData); i++) + { + pingData[i] = random(0xff); + } + + beginMessage(TYPE_PING); + write(pingData, sizeof(pingData)); + return endMessage(); +} + +int WebSocketClient::available() +{ + if (iState < eReadingBody) + { + return HttpClient::available(); + } + + return iRxSize; +} + +int WebSocketClient::read() +{ + byte b; + + if (read(&b, sizeof(b))) + { + return b; + } + + return -1; +} + +int WebSocketClient::read(uint8_t *aBuffer, size_t aSize) +{ + int readCount = HttpClient::read(aBuffer, aSize); + + if (readCount > 0) + { + iRxSize -= readCount; + + // unmask the RX data if needed + if (iRxMasked) + { + for (int i = 0; i < (int)aSize; i++, iRxMaskIndex++) { + aBuffer[i] ^= iRxMaskKey[iRxMaskIndex % sizeof(iRxMaskKey)]; + } + } + } + + return readCount; +} + +int WebSocketClient::peek() +{ + int p = HttpClient::peek(); + + if (p != -1 && iRxMasked) + { + // unmask the RX data if needed + p = (uint8_t)p ^ iRxMaskKey[iRxMaskIndex % sizeof(iRxMaskKey)]; + } + + return p; +} + +void WebSocketClient::flushRx() +{ + while(available()) + { + read(); + } +} diff --git a/WebSocketClient.h b/WebSocketClient.h new file mode 100644 index 0000000..4b009e6 --- /dev/null +++ b/WebSocketClient.h @@ -0,0 +1,99 @@ +// (c) Copyright Arduino. 2016 +// Released under Apache License, version 2.0 + +#ifndef WebSocketClient_h +#define WebSocketClient_h + +#include + +#include "HttpClient.h" + +static const int TYPE_CONTINUATION = 0x0; +static const int TYPE_TEXT = 0x1; +static const int TYPE_BINARY = 0x2; +static const int TYPE_CONNECTION_CLOSE = 0x8; +static const int TYPE_PING = 0x9; +static const int TYPE_PONG = 0xa; + +class WebSocketClient : public HttpClient +{ +public: + WebSocketClient(Client& aClient, const char* aServerName, uint16_t aServerPort = HttpClient::kHttpPort); + WebSocketClient(Client& aClient, const String& aServerName, uint16_t aServerPort = HttpClient::kHttpPort); + WebSocketClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort = HttpClient::kHttpPort); + + /** Start the Web Socket connection to the specified path + @param aURLPath Path to use in request (optional, "/" is used by default) + @return 0 if successful, else error + */ + int begin(const char* aPath = "/"); + int begin(const String& aPath); + + /** Begin to send a message of type (TYPE_TEXT or TYPE_BINARY) + Use the write or Stream API's to set message content, followed by endMessage + to complete the message. + @param aURLPath Path to use in request + @return 0 if successful, else error + */ + int beginMessage(int aType); + + /** Completes sending of a message started by beginMessage + @return 0 if successful, else error + */ + int endMessage(); + + /** Try to parse an incoming messages + @return 0 if no message available, else size of parsed message + */ + int parseMessage(); + + /** Returns type of current parsed message + @return type of current parsedMessage (TYPE_TEXT or TYPE_BINARY) + */ + int messageType(); + + /** Returns if the current message is the final chunk of a split + message + @return true for final message, false otherwise + */ + bool isFinal(); + + /** Read the current messages as a string + @return current message as a string + */ + String readString(); + + /** Send a ping + @return 0 if successful, else error + */ + int ping(); + + // Inherited from Print + virtual size_t write(uint8_t aByte); + virtual size_t write(const uint8_t *aBuffer, size_t aSize); + // Inherited from Stream + virtual int available(); + /** Read the next byte from the server. + @return Byte read or -1 if there are no bytes available. + */ + virtual int read(); + virtual int read(uint8_t *buf, size_t size); + virtual int peek(); + +private: + void flushRx(); + +private: + bool iTxStarted; + uint8_t iTxMessageType; + uint8_t iTxBuffer[128]; + uint64_t iTxSize; + + uint8_t iRxOpCode; + uint64_t iRxSize; + bool iRxMasked; + int iRxMaskIndex; + uint8_t iRxMaskKey[4]; +}; + +#endif diff --git a/examples/SimpleWebSocket/SimpleWebSocket.ino b/examples/SimpleWebSocket/SimpleWebSocket.ino new file mode 100644 index 0000000..ccd97b7 --- /dev/null +++ b/examples/SimpleWebSocket/SimpleWebSocket.ino @@ -0,0 +1,80 @@ +/* + Simple WebSocket client for ArduinoHttpClient library + Connects to the WebSocket server, and sends a hello + message every 5 seconds + + note: WiFi SSID and password are stored in config.h file. + If it is not present, add a new tab, call it "config.h" + and add the following variables: + char ssid[] = "ssid"; // your network SSID (name) + char pass[] = "password"; // your network password + + created 28 Jun 2016 + by Sandeep Mistry + + this example is in the public domain +*/ +#include +#include +#include "config.h" + +char serverAddress[] = "echo.websocket.org"; // server address +int port = 80; + +WiFiClient wifi; +WebSocketClient client = WebSocketClient(wifi, serverAddress, port); +int status = WL_IDLE_STATUS; +int count = 0; + +void setup() { + Serial.begin(9600); + while ( status != WL_CONNECTED) { + Serial.print("Attempting to connect to Network named: "); + Serial.println(ssid); // print the network name (SSID); + + // Connect to WPA/WPA2 network: + status = WiFi.begin(ssid, pass); + } + + // print the SSID of the network you're attached to: + Serial.print("SSID: "); + Serial.println(WiFi.SSID()); + + // print your WiFi shield's IP address: + IPAddress ip = WiFi.localIP(); + Serial.print("IP Address: "); + Serial.println(ip); +} + +void loop() { + Serial.println("starting WebSocket client"); + client.begin(); + + while (client.connected()) { + Serial.print("Sending hello "); + Serial.println(count); + + // send a hello # + client.beginMessage(TYPE_TEXT); + client.print("hello "); + client.print(count); + client.endMessage(); + + // increment count for next message + count++; + + // check if a message is available to be received + int messageSize = client.parseMessage(); + + if (messageSize > 0) { + Serial.println("Received a message:"); + Serial.println(client.readString()); + } + + // wait 5 seconds + delay(5000); + } + + Serial.println("disconnected"); +} + diff --git a/examples/DweetGet/config.h b/examples/SimpleWebSocket/config.h similarity index 99% rename from examples/DweetGet/config.h rename to examples/SimpleWebSocket/config.h index c263766..7765359 100644 --- a/examples/DweetGet/config.h +++ b/examples/SimpleWebSocket/config.h @@ -1,2 +1,3 @@ char ssid[] = "ssid"; // your network SSID (name) char pass[] = "password"; // your network password + diff --git a/examples/node_test_server/getPostPutDelete.js b/examples/node_test_server/getPostPutDelete.js index ec58f82..e055b65 100644 --- a/examples/node_test_server/getPostPutDelete.js +++ b/examples/node_test_server/getPostPutDelete.js @@ -8,16 +8,17 @@ */ var express = require('express'); // include express.js -var app = express(); // a local instance of it +var app = express(); // a local instance of it var bodyParser = require('body-parser'); // include body-parser +var WebSocketServer = require('ws').Server // include Web Socket server // you need a body parser: app.use(bodyParser.urlencoded({extended: false})); // for application/x-www-form-urlencoded // this runs after the server successfully starts: function serverStart() { - var port = server.address().port; - console.log('Server listening on port '+ port); + var port = server.address().port; + console.log('Server listening on port '+ port); } // this is the POST handler: @@ -26,12 +27,12 @@ app.all('/*', function (request, response) { // the parameters of a GET request are passed in // request.body. Pass that to formatResponse() // for formatting: - console.log(request.headers); - if (request.method == 'GET') { - console.log(request.query); - } else { - console.log(request.body); - } + console.log(request.headers); + if (request.method == 'GET') { + console.log(request.query); + } else { + console.log(request.body); + } // send the response: response.send('OK'); @@ -40,3 +41,17 @@ app.all('/*', function (request, response) { // start the server: var server = app.listen(8080, serverStart); + +// create a WebSocket server and attach it to the server +var wss = new WebSocketServer({server: server}); + +wss.on('connection', function connection(ws) { + // new connection, add message listener + ws.on('message', function incoming(message) { + // received a message + console.log('received: %s', message); + + // echo it back + ws.send(message); + }); +}); diff --git a/examples/node_test_server/package.json b/examples/node_test_server/package.json index d6fb7cc..25fa25b 100644 --- a/examples/node_test_server/package.json +++ b/examples/node_test_server/package.json @@ -2,12 +2,13 @@ "name": "node_test_server", "version": "0.0.1", "author": { - "name":"Tom Igoe" - }, + "name": "Tom Igoe" + }, "dependencies": { + "body-parser": ">=1.11.0", "express": ">=4.0.0", - "body-parser" : ">=1.11.0", - "multer" : "*" + "multer": "*", + "ws": "^1.1.1" }, "engines": { "node": "0.10.x", diff --git a/keywords.txt b/keywords.txt index 00bf130..f62ba70 100644 --- a/keywords.txt +++ b/keywords.txt @@ -8,6 +8,7 @@ ArduinoHttpClient KEYWORD1 HttpClient KEYWORD1 +WebSocketClient KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -35,12 +36,26 @@ readHeaderName KEYWORD2 readHeaderValue KEYWORD2 responseBody KEYWORD2 +beginMessage KEYWORD2 +endMessage KEYWORD2 +parseMessage KEYWORD2 +messageType KEYWORD2 +isFinal KEYWORD2 +readString KEYWORD2 +ping KEYWORD2 + ####################################### # Constants (LITERAL1) ####################################### -HTTP_SUCCESS LITERAL1 -HTTP_ERROR_CONNECTION_FAILED LITERAL1 -HTTP_ERROR_API LITERAL1 -HTTP_ERROR_TIMED_OUT LITERAL1 -HTTP_ERROR_INVALID_RESPONSE LITERAL1 +HTTP_SUCCESS LITERAL1 +HTTP_ERROR_CONNECTION_FAILED LITERAL1 +HTTP_ERROR_API LITERAL1 +HTTP_ERROR_TIMED_OUT LITERAL1 +HTTP_ERROR_INVALID_RESPONSE LITERAL1 +TYPE_CONTINUATION LITERAL1 +TYPE_TEXT LITERAL1 +TYPE_BINARY LITERAL1 +TYPE_CONNECTION_CLOSE LITERAL1 +TYPE_PING LITERAL1 +TYPE_PONG LITERAL1