mirror of
https://github.com/espruino/Espruino.git
synced 2025-12-08 19:06:15 +00:00
906 lines
34 KiB
C
906 lines
34 KiB
C
/*
|
|
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
|
|
*
|
|
* Copyright (C) 2013 Gordon Williams <gw@pur3.co.uk>
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* ----------------------------------------------------------------------------
|
|
* Contains HTTP client and server
|
|
* ----------------------------------------------------------------------------
|
|
*/
|
|
#include "socketserver.h"
|
|
#include "socketerrors.h"
|
|
#include "jsparse.h"
|
|
#include "jsinteractive.h"
|
|
#include "jshardware.h"
|
|
#include "jswrap_stream.h"
|
|
|
|
#define HTTP_NAME_SOCKETTYPE "type" // normal socket or HTTP
|
|
#define HTTP_NAME_PORT "port"
|
|
#define HTTP_NAME_SOCKET "sckt"
|
|
#define HTTP_NAME_HAD_HEADERS "hdrs"
|
|
#define HTTP_NAME_RECEIVE_DATA "dRcv"
|
|
#define HTTP_NAME_SEND_DATA "dSnd"
|
|
#define HTTP_NAME_RESPONSE_VAR "res"
|
|
#define HTTP_NAME_OPTIONS_VAR "opt"
|
|
#define HTTP_NAME_SERVER_VAR "svr"
|
|
#define HTTP_NAME_CHUNKED "chunked"
|
|
#define HTTP_NAME_CLOSENOW "closeNow" // boolean: gotta close
|
|
#define HTTP_NAME_CONNECTED "conn" // boolean: we are connected
|
|
#define HTTP_NAME_CLOSE "close" // close after sending
|
|
#define HTTP_NAME_ON_CONNECT JS_EVENT_PREFIX"connect"
|
|
#define HTTP_NAME_ON_CLOSE JS_EVENT_PREFIX"close"
|
|
#define HTTP_NAME_ON_END JS_EVENT_PREFIX"end"
|
|
#define HTTP_NAME_ON_DRAIN JS_EVENT_PREFIX"drain"
|
|
#define HTTP_NAME_ON_ERROR JS_EVENT_PREFIX"error"
|
|
|
|
#define HTTP_ARRAY_HTTP_CLIENT_CONNECTIONS "HttpCC"
|
|
#define HTTP_ARRAY_HTTP_SERVERS "HttpS"
|
|
#define HTTP_ARRAY_HTTP_SERVER_CONNECTIONS "HttpSC"
|
|
|
|
// Define the size of buffers/chunks that are transmitted or received
|
|
#ifdef ESP8266
|
|
// The TCP MSS is 536, we use half that 'cause otherwise we easily run out of JSvars memory
|
|
#define CHUNK (536/2)
|
|
// esp8266 debugging, need to remove this eventually
|
|
extern int os_printf_plus(const char *format, ...) __attribute__((format(printf, 1, 2)));
|
|
#define printf os_printf_plus
|
|
#else
|
|
#define CHUNK 64
|
|
#endif
|
|
|
|
|
|
// -----------------------------
|
|
|
|
static void httpAppendHeaders(JsVar *string, JsVar *headerObject) {
|
|
// append headers
|
|
JsvObjectIterator it;
|
|
jsvObjectIteratorNew(&it, headerObject);
|
|
while (jsvObjectIteratorHasValue(&it)) {
|
|
JsVar *k = jsvAsString(jsvObjectIteratorGetKey(&it), true);
|
|
JsVar *v = jsvAsString(jsvObjectIteratorGetValue(&it), true);
|
|
jsvAppendStringVarComplete(string, k);
|
|
jsvAppendString(string, ": ");
|
|
jsvAppendStringVarComplete(string, v);
|
|
jsvAppendString(string, "\r\n");
|
|
jsvUnLock2(k, v);
|
|
jsvObjectIteratorNext(&it);
|
|
}
|
|
jsvObjectIteratorFree(&it);
|
|
|
|
// free headers
|
|
}
|
|
|
|
// httpParseHeaders(&receiveData, reqVar, true) // server
|
|
// httpParseHeaders(&receiveData, resVar, false) // client
|
|
bool httpParseHeaders(JsVar **receiveData, JsVar *objectForData, bool isServer) {
|
|
// find /r/n/r/n
|
|
int newlineIdx = 0;
|
|
int strIdx = 0;
|
|
int headerEnd = -1;
|
|
JsvStringIterator it;
|
|
jsvStringIteratorNew(&it, *receiveData, 0);
|
|
while (jsvStringIteratorHasChar(&it)) {
|
|
char ch = jsvStringIteratorGetChar(&it);
|
|
if (ch == '\r') {
|
|
if (newlineIdx==0) newlineIdx=1;
|
|
else if (newlineIdx==2) newlineIdx=3;
|
|
} else if (ch == '\n') {
|
|
if (newlineIdx==1) newlineIdx=2;
|
|
else if (newlineIdx==3) {
|
|
headerEnd = strIdx+1;
|
|
}
|
|
} else newlineIdx=0;
|
|
jsvStringIteratorNext(&it);
|
|
strIdx++;
|
|
}
|
|
jsvStringIteratorFree(&it);
|
|
// skip if we have no header
|
|
if (headerEnd<0) return false;
|
|
// Now parse the header
|
|
JsVar *vHeaders = jsvNewWithFlags(JSV_OBJECT);
|
|
if (!vHeaders) return true;
|
|
jsvUnLock(jsvAddNamedChild(objectForData, vHeaders, "headers"));
|
|
strIdx = 0;
|
|
int firstSpace = -1;
|
|
int secondSpace = -1;
|
|
int firstEOL = -1;
|
|
int lineNumber = 0;
|
|
int lastLineStart = 0;
|
|
int colonPos = 0;
|
|
//jsiConsolePrintStringVar(receiveData);
|
|
jsvStringIteratorNew(&it, *receiveData, 0);
|
|
while (jsvStringIteratorHasChar(&it)) {
|
|
char ch = jsvStringIteratorGetChar(&it);
|
|
if (ch==' ' || ch=='\r') {
|
|
if (firstSpace<0) firstSpace = strIdx;
|
|
else if (secondSpace<0) secondSpace = strIdx;
|
|
}
|
|
if (ch == ':' && colonPos<0) colonPos = strIdx;
|
|
if (ch == '\r') {
|
|
if (firstEOL<0) firstEOL=strIdx;
|
|
if (lineNumber>0 && colonPos>lastLineStart && lastLineStart<strIdx) {
|
|
JsVar *hVal = jsvNewFromEmptyString();
|
|
if (hVal)
|
|
jsvAppendStringVar(hVal, *receiveData, (size_t)colonPos+2, (size_t)(strIdx-(colonPos+2)));
|
|
JsVar *hKey = jsvNewFromEmptyString();
|
|
if (hKey) {
|
|
jsvMakeIntoVariableName(hKey, hVal);
|
|
jsvAppendStringVar(hKey, *receiveData, (size_t)lastLineStart, (size_t)(colonPos-lastLineStart));
|
|
jsvAddName(vHeaders, hKey);
|
|
jsvUnLock(hKey);
|
|
}
|
|
jsvUnLock(hVal);
|
|
}
|
|
lineNumber++;
|
|
colonPos=-1;
|
|
}
|
|
if (ch == '\r' || ch == '\n') {
|
|
lastLineStart = strIdx+1;
|
|
}
|
|
|
|
jsvStringIteratorNext(&it);
|
|
strIdx++;
|
|
}
|
|
jsvStringIteratorFree(&it);
|
|
jsvUnLock(vHeaders);
|
|
// try and pull out methods/etc
|
|
if (isServer) {
|
|
jsvObjectSetChildAndUnLock(objectForData, "method", jsvNewFromStringVar(*receiveData, 0, (size_t)firstSpace));
|
|
jsvObjectSetChildAndUnLock(objectForData, "url", jsvNewFromStringVar(*receiveData, (size_t)(firstSpace+1), (size_t)(secondSpace-(firstSpace+1))));
|
|
} else {
|
|
jsvObjectSetChildAndUnLock(objectForData, "httpVersion", jsvNewFromStringVar(*receiveData, 5, (size_t)firstSpace-5));
|
|
jsvObjectSetChildAndUnLock(objectForData, "statusCode", jsvNewFromStringVar(*receiveData, (size_t)(firstSpace+1), (size_t)(secondSpace-(firstSpace+1))));
|
|
jsvObjectSetChildAndUnLock(objectForData, "statusMessage", jsvNewFromStringVar(*receiveData, (size_t)(secondSpace+1), (size_t)(firstEOL-(secondSpace+1))));
|
|
}
|
|
// strip out the header
|
|
JsVar *afterHeaders = jsvNewFromStringVar(*receiveData, (size_t)headerEnd, JSVAPPENDSTRINGVAR_MAXLENGTH);
|
|
jsvUnLock(*receiveData);
|
|
*receiveData = afterHeaders;
|
|
return true;
|
|
}
|
|
|
|
size_t httpStringGet(JsVar *v, char *str, size_t len) {
|
|
size_t l = len;
|
|
JsvStringIterator it;
|
|
jsvStringIteratorNew(&it, v, 0);
|
|
while (jsvStringIteratorHasChar(&it)) {
|
|
if (l--==0) {
|
|
jsvStringIteratorFree(&it);
|
|
return len;
|
|
}
|
|
*(str++) = jsvStringIteratorGetChar(&it);
|
|
jsvStringIteratorNext(&it);
|
|
}
|
|
jsvStringIteratorFree(&it);
|
|
return len-l;
|
|
}
|
|
|
|
// -----------------------------
|
|
|
|
static JsVar *socketGetArray(const char *name, bool create) {
|
|
return jsvObjectGetChild(execInfo.hiddenRoot, name, create?JSV_ARRAY:0);
|
|
}
|
|
|
|
static NO_INLINE SocketType socketGetType(JsVar *var) {
|
|
return jsvGetIntegerAndUnLock(jsvObjectGetChild(var, HTTP_NAME_SOCKETTYPE, 0));
|
|
}
|
|
|
|
static NO_INLINE void socketSetType(JsVar *var, SocketType socketType) {
|
|
jsvObjectSetChildAndUnLock(var, HTTP_NAME_SOCKETTYPE, jsvNewFromInteger((JsVarInt)socketType));
|
|
}
|
|
|
|
void _socketConnectionKill(JsNetwork *net, JsVar *connection) {
|
|
if (!net || networkState != NETWORKSTATE_ONLINE) return;
|
|
int sckt = (int)jsvGetIntegerAndUnLock(jsvObjectGetChild(connection,HTTP_NAME_SOCKET,0))-1; // so -1 if undefined
|
|
if (sckt>=0) {
|
|
netCloseSocket(net, sckt);
|
|
jsvObjectSetChild(connection,HTTP_NAME_SOCKET,0);
|
|
}
|
|
}
|
|
|
|
// -----------------------------
|
|
|
|
NO_INLINE static void _socketCloseAllConnectionsFor(JsNetwork *net, char *name) {
|
|
JsVar *arr = socketGetArray(name, false);
|
|
if (!arr) return;
|
|
JsvObjectIterator it;
|
|
jsvObjectIteratorNew(&it, arr);
|
|
while (jsvObjectIteratorHasValue(&it)) {
|
|
JsVar *connection = jsvObjectIteratorGetValue(&it);
|
|
_socketConnectionKill(net, connection);
|
|
jsvUnLock(connection);
|
|
jsvObjectIteratorNext(&it);
|
|
}
|
|
jsvObjectIteratorFree(&it);
|
|
jsvRemoveAllChildren(arr);
|
|
jsvUnLock(arr);
|
|
}
|
|
|
|
NO_INLINE static void _socketCloseAllConnections(JsNetwork *net) {
|
|
// shut down connections
|
|
_socketCloseAllConnectionsFor(net, HTTP_ARRAY_HTTP_SERVER_CONNECTIONS);
|
|
_socketCloseAllConnectionsFor(net, HTTP_ARRAY_HTTP_CLIENT_CONNECTIONS);
|
|
_socketCloseAllConnectionsFor(net, HTTP_ARRAY_HTTP_SERVERS);
|
|
}
|
|
|
|
// returns 0 on success and a (negative) error number on failure
|
|
int socketSendData(JsNetwork *net, JsVar *connection, int sckt, JsVar **sendData) {
|
|
char buf[CHUNK];
|
|
|
|
if (!jsvIsEmptyString(*sendData)) {
|
|
size_t bufLen = httpStringGet(*sendData, buf, sizeof(buf));
|
|
int num = netSend(net, sckt, buf, bufLen);
|
|
if (num < 0) return num; // an error occurred
|
|
// Now cut what we managed to send off the beginning of sendData
|
|
if (num > 0) {
|
|
JsVar *newSendData = 0;
|
|
if (num < (int)jsvGetStringLength(*sendData)) {
|
|
// we didn't send all of it... cut out what we did send
|
|
newSendData = jsvNewFromStringVar(*sendData, (size_t)num, JSVAPPENDSTRINGVAR_MAXLENGTH);
|
|
} else {
|
|
// we sent all of it! Issue a drain event
|
|
jsiQueueObjectCallbacks(connection, HTTP_NAME_ON_DRAIN, &connection, 1);
|
|
}
|
|
jsvUnLock(*sendData);
|
|
*sendData = newSendData;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// -----------------------------
|
|
|
|
void socketInit() {
|
|
#ifdef WIN32
|
|
// Init winsock 1.1
|
|
WORD sockVersion;
|
|
WSADATA wsaData;
|
|
sockVersion = MAKEWORD(1, 1);
|
|
WSAStartup(sockVersion, &wsaData);
|
|
#endif
|
|
}
|
|
|
|
void socketKill(JsNetwork *net) {
|
|
_socketCloseAllConnections(net);
|
|
#ifdef WIN32
|
|
// Shutdown Winsock
|
|
WSACleanup();
|
|
#endif
|
|
}
|
|
|
|
// Fire error events on up to two objects if there is an error, returns true if there is an error
|
|
// The error events have a code field and a message field.
|
|
static bool fireErrorEvent(int error, JsVar *obj1, JsVar *obj2) {
|
|
bool hadError = error < 0 && error != SOCKET_ERR_CLOSED;
|
|
JsVar *params[1];
|
|
if (hadError) {
|
|
params[0] = jsvNewWithFlags(JSV_OBJECT);
|
|
jsvObjectSetChildAndUnLock(params[0], "code", jsvNewFromInteger(error));
|
|
jsvObjectSetChildAndUnLock(params[0], "message",
|
|
jsvNewFromString(socketErrorString(error)));
|
|
if (obj1 != NULL)
|
|
jsiQueueObjectCallbacks(obj1, HTTP_NAME_ON_ERROR, params, 1);
|
|
if (obj2 != NULL)
|
|
jsiQueueObjectCallbacks(obj2, HTTP_NAME_ON_ERROR, params, 1);
|
|
jsvUnLock(params[0]);
|
|
}
|
|
return hadError;
|
|
}
|
|
|
|
// -----------------------------
|
|
|
|
bool socketServerConnectionsIdle(JsNetwork *net) {
|
|
char buf[CHUNK];
|
|
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_SERVER_CONNECTIONS,false);
|
|
if (!arr) return false;
|
|
|
|
bool hadSockets = false;
|
|
JsvObjectIterator it;
|
|
jsvObjectIteratorNew(&it, arr);
|
|
while (jsvObjectIteratorHasValue(&it)) {
|
|
hadSockets = true;
|
|
// Get connection, socket, and socket type
|
|
// For normal sockets, socket==connection, but for HTTP we split it into a request and a response
|
|
JsVar *connection = jsvObjectIteratorGetValue(&it);
|
|
SocketType socketType = socketGetType(connection);
|
|
JsVar *socket = ((socketType&ST_TYPE_MASK)==ST_HTTP) ? jsvObjectGetChild(connection,HTTP_NAME_RESPONSE_VAR,0) : jsvLockAgain(connection);
|
|
|
|
int sckt = (int)jsvGetIntegerAndUnLock(jsvObjectGetChild(connection,HTTP_NAME_SOCKET,0))-1; // so -1 if undefined
|
|
bool closeConnectionNow = jsvGetBoolAndUnLock(jsvObjectGetChild(connection, HTTP_NAME_CLOSENOW, false));
|
|
int error = 0;
|
|
|
|
if (!closeConnectionNow) {
|
|
int num = netRecv(net, sckt, buf,sizeof(buf));
|
|
if (num<0) {
|
|
// we probably disconnected so just get rid of this
|
|
closeConnectionNow = true;
|
|
error = num;
|
|
} else {
|
|
// add it to our request string
|
|
if (num>0) {
|
|
JsVar *receiveData = jsvObjectGetChild(connection,HTTP_NAME_RECEIVE_DATA,0);
|
|
JsVar *oldReceiveData = receiveData;
|
|
if (!receiveData) receiveData = jsvNewFromEmptyString();
|
|
if (receiveData) {
|
|
jsvAppendStringBuf(receiveData, buf, (size_t)num);
|
|
bool hadHeaders = jsvGetBoolAndUnLock(jsvObjectGetChild(connection,HTTP_NAME_HAD_HEADERS,0));
|
|
if (!hadHeaders && httpParseHeaders(&receiveData, connection, true)) {
|
|
hadHeaders = true;
|
|
jsvObjectSetChildAndUnLock(connection, HTTP_NAME_HAD_HEADERS, jsvNewFromBool(hadHeaders));
|
|
JsVar *server = jsvObjectGetChild(connection,HTTP_NAME_SERVER_VAR,0);
|
|
JsVar *args[2] = { connection, socket };
|
|
jsiQueueObjectCallbacks(server, HTTP_NAME_ON_CONNECT, args, ((socketType&ST_TYPE_MASK)==ST_HTTP) ? 2 : 1);
|
|
jsvUnLock(server);
|
|
}
|
|
if (hadHeaders && !jsvIsEmptyString(receiveData)) {
|
|
// execute 'data' callback or save data
|
|
if (jswrap_stream_pushData(connection, receiveData, false)) {
|
|
// clear received data
|
|
jsvUnLock(receiveData);
|
|
receiveData = 0;
|
|
}
|
|
}
|
|
// if received data changed, update it
|
|
if (receiveData != oldReceiveData)
|
|
jsvObjectSetChild(connection,HTTP_NAME_RECEIVE_DATA,receiveData);
|
|
jsvUnLock(receiveData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// send data if possible
|
|
JsVar *sendData = jsvObjectGetChild(socket,HTTP_NAME_SEND_DATA,0);
|
|
if (sendData) {
|
|
int sent = socketSendData(net, socket, sckt, &sendData);
|
|
// FIXME? checking for errors is a bit iffy. With the esp8266 network that returns
|
|
// varied error codes we'd want to skip SOCKET_ERR_CLOSED and let the recv side deal
|
|
// with normal closing so we don't miss the tail of what's received, but other drivers
|
|
// return -1 (which is the same value) for all errors. So we rely on the check ~12 lines
|
|
// down if(num>0)closeConnectionNow=false instead.
|
|
if (sent < 0) {
|
|
closeConnectionNow = true;
|
|
error = sent;
|
|
}
|
|
jsvObjectSetChild(socket, HTTP_NAME_SEND_DATA, sendData); // socketSendData prob updated sendData
|
|
}
|
|
// only close if we want to close, have no data to send, and aren't receiving data
|
|
if (jsvGetBoolAndUnLock(jsvObjectGetChild(socket,HTTP_NAME_CLOSE,0)) && !sendData && num<=0)
|
|
closeConnectionNow = true;
|
|
else if (num > 0)
|
|
closeConnectionNow = false; // guarantee that anything received is processed
|
|
jsvUnLock(sendData);
|
|
}
|
|
if (closeConnectionNow) {
|
|
// send out any data that we were POSTed
|
|
JsVar *receiveData = jsvObjectGetChild(connection,HTTP_NAME_RECEIVE_DATA,0);
|
|
bool hadHeaders = jsvGetBoolAndUnLock(jsvObjectGetChild(connection,HTTP_NAME_HAD_HEADERS,0));
|
|
if (hadHeaders && !jsvIsEmptyString(receiveData)) {
|
|
// execute 'data' callback or save data
|
|
jswrap_stream_pushData(connection, receiveData, true);
|
|
}
|
|
jsvUnLock(receiveData);
|
|
|
|
// fire error events
|
|
bool hadError = fireErrorEvent(error, connection, socket);
|
|
|
|
// fire the close listeners
|
|
JsVar *params[1] = { jsvNewFromBool(hadError) };
|
|
jsiQueueObjectCallbacks(connection, HTTP_NAME_ON_CLOSE, params, 1);
|
|
jsiQueueObjectCallbacks(socket, HTTP_NAME_ON_CLOSE, params, 1);
|
|
jsvUnLock(params[0]);
|
|
|
|
_socketConnectionKill(net, connection);
|
|
JsVar *connectionName = jsvObjectIteratorGetKey(&it);
|
|
jsvObjectIteratorNext(&it);
|
|
jsvRemoveChild(arr, connectionName);
|
|
jsvUnLock(connectionName);
|
|
} else
|
|
jsvObjectIteratorNext(&it);
|
|
jsvUnLock2(connection, socket);
|
|
}
|
|
jsvObjectIteratorFree(&it);
|
|
jsvUnLock(arr);
|
|
|
|
return hadSockets;
|
|
}
|
|
|
|
|
|
void socketClientPushReceiveData(JsVar *connection, JsVar *socket, JsVar **receiveData) {
|
|
if (*receiveData) {
|
|
if (jsvIsEmptyString(*receiveData) ||
|
|
jswrap_stream_pushData(socket, *receiveData, false)) {
|
|
// clear - because we have issued a callback
|
|
jsvObjectSetChild(connection,HTTP_NAME_RECEIVE_DATA,0);
|
|
jsvUnLock(*receiveData);
|
|
*receiveData = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool socketClientConnectionsIdle(JsNetwork *net) {
|
|
char buf[CHUNK];
|
|
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_CLIENT_CONNECTIONS,false);
|
|
if (!arr) return false;
|
|
|
|
bool hadSockets = false;
|
|
JsvObjectIterator it;
|
|
jsvObjectIteratorNew(&it, arr);
|
|
while (jsvObjectIteratorHasValue(&it)) {
|
|
hadSockets = true;
|
|
// Get connection, socket, and socket type
|
|
// For normal sockets, socket==connection, but for HTTP we split it into a request and a response
|
|
JsVar *connection = jsvObjectIteratorGetValue(&it);
|
|
SocketType socketType = socketGetType(connection);
|
|
JsVar *socket = ((socketType&ST_TYPE_MASK)==ST_HTTP) ? jsvObjectGetChild(connection,HTTP_NAME_RESPONSE_VAR,0) : jsvLockAgain(connection);
|
|
bool socketClosed = false;
|
|
JsVar *receiveData = 0;
|
|
|
|
bool hadHeaders = false;
|
|
int error = 0; // error code received from netXxxx functions
|
|
bool isHttp = (socketType&ST_TYPE_MASK) == ST_HTTP;
|
|
bool closeConnectionNow = jsvGetBoolAndUnLock(jsvObjectGetChild(connection, HTTP_NAME_CLOSENOW, false));
|
|
bool alreadyConnected = jsvGetBoolAndUnLock(jsvObjectGetChild(connection, HTTP_NAME_CONNECTED, false));
|
|
int sckt = (int)jsvGetIntegerAndUnLock(jsvObjectGetChild(connection,HTTP_NAME_SOCKET,0))-1; // so -1 if undefined
|
|
if (sckt>=0) {
|
|
if (isHttp)
|
|
hadHeaders = jsvGetBoolAndUnLock(jsvObjectGetChild(connection,HTTP_NAME_HAD_HEADERS,0));
|
|
else
|
|
hadHeaders = true;
|
|
receiveData = jsvObjectGetChild(connection,HTTP_NAME_RECEIVE_DATA,0);
|
|
|
|
/* We do this up here because we want to wait until we have been once
|
|
* around the idle loop (=callbacks have been executed) before we run this */
|
|
if (hadHeaders)
|
|
socketClientPushReceiveData(connection, socket, &receiveData);
|
|
|
|
JsVar *sendData = jsvObjectGetChild(connection,HTTP_NAME_SEND_DATA,0);
|
|
if (!closeConnectionNow) {
|
|
// send data if possible
|
|
if (sendData) {
|
|
// don't try to send if we're already in error state
|
|
int num = 0;
|
|
if (error == 0) num = socketSendData(net, connection, sckt, &sendData);
|
|
//if (num != 0) printf("send returned %d\r\n", num);
|
|
if (num > 0 && !alreadyConnected && !isHttp) { // whoa, we sent something, must be connected!
|
|
jsiQueueObjectCallbacks(connection, HTTP_NAME_ON_CONNECT, &connection, 1);
|
|
jsvObjectSetChildAndUnLock(connection, HTTP_NAME_CONNECTED, jsvNewFromBool(true));
|
|
alreadyConnected = true;
|
|
}
|
|
if (num < 0) {
|
|
closeConnectionNow = true;
|
|
error = num;
|
|
}
|
|
jsvObjectSetChild(connection, HTTP_NAME_SEND_DATA, sendData); // _http_send prob updated sendData
|
|
} else {
|
|
// no data to send, do we want to close? do so.
|
|
if (jsvGetBoolAndUnLock(jsvObjectGetChild(connection, HTTP_NAME_CLOSE, false)))
|
|
closeConnectionNow = true;
|
|
}
|
|
// Now read data if possible (and we have space for it)
|
|
if (!receiveData || !hadHeaders) {
|
|
int num = netRecv(net, sckt, buf, sizeof(buf));
|
|
//if (num != 0) printf("recv returned %d\r\n", num);
|
|
if (!alreadyConnected && num == SOCKET_ERR_NO_CONN) {
|
|
; // ignore... it's just telling us we're not connected yet
|
|
} else if (num < 0) {
|
|
closeConnectionNow = true;
|
|
error = num;
|
|
// disconnected without headers? error.
|
|
if (!hadHeaders && error == SOCKET_ERR_CLOSED) error = SOCKET_ERR_NO_RESP;
|
|
} else {
|
|
// did we just get connected?
|
|
if (!alreadyConnected && !isHttp) {
|
|
jsiQueueObjectCallbacks(connection, HTTP_NAME_ON_CONNECT, &connection, 1);
|
|
jsvObjectSetChildAndUnLock(connection, HTTP_NAME_CONNECTED, jsvNewFromBool(true));
|
|
alreadyConnected = true;
|
|
// if we do not have any data to send, issue a drain event
|
|
if (!sendData || (int)jsvGetStringLength(sendData) == 0)
|
|
jsiQueueObjectCallbacks(connection, HTTP_NAME_ON_DRAIN, &connection, 1);
|
|
}
|
|
// got data add it to our receive buffer
|
|
if (num > 0) {
|
|
if (!receiveData) {
|
|
receiveData = jsvNewFromEmptyString();
|
|
jsvObjectSetChild(connection, HTTP_NAME_RECEIVE_DATA, receiveData);
|
|
}
|
|
if (receiveData) { // could be out of memory
|
|
jsvAppendStringBuf(receiveData, buf, (size_t)num);
|
|
if ((socketType&ST_TYPE_MASK)==ST_HTTP && !hadHeaders) {
|
|
// for HTTP see whether we now have full response headers
|
|
JsVar *resVar = jsvObjectGetChild(connection,HTTP_NAME_RESPONSE_VAR,0);
|
|
if (httpParseHeaders(&receiveData, resVar, false)) {
|
|
hadHeaders = true;
|
|
jsvObjectSetChildAndUnLock(connection, HTTP_NAME_HAD_HEADERS, jsvNewFromBool(hadHeaders));
|
|
jsiQueueObjectCallbacks(connection, HTTP_NAME_ON_CONNECT, &resVar, 1);
|
|
}
|
|
jsvUnLock(resVar);
|
|
jsvObjectSetChild(connection, HTTP_NAME_RECEIVE_DATA, receiveData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
jsvUnLock(sendData);
|
|
}
|
|
}
|
|
|
|
if (closeConnectionNow) {
|
|
socketClientPushReceiveData(connection, socket, &receiveData);
|
|
if (!receiveData) {
|
|
//printf("closing now error=%d\r\n", error);
|
|
if ((socketType&ST_TYPE_MASK) != ST_HTTP)
|
|
jsiQueueObjectCallbacks(socket, HTTP_NAME_ON_END, &socket, 1);
|
|
|
|
// If we had data to send but the socket closed, this is an error
|
|
JsVar *sendData = jsvObjectGetChild(connection,HTTP_NAME_SEND_DATA,0);
|
|
if (sendData && error == SOCKET_ERR_CLOSED) error = SOCKET_ERR_UNSENT_DATA;
|
|
jsvUnLock(sendData);
|
|
|
|
_socketConnectionKill(net, connection);
|
|
JsVar *connectionName = jsvObjectIteratorGetKey(&it);
|
|
jsvObjectIteratorNext(&it);
|
|
jsvRemoveChild(arr, connectionName);
|
|
jsvUnLock(connectionName);
|
|
socketClosed = true;
|
|
|
|
// fire error event, if ther eis an error
|
|
bool hadError = fireErrorEvent(error, connection, NULL);
|
|
|
|
// close callback must happen after error callback
|
|
JsVar *params[1] = { jsvNewFromBool(hadError) };
|
|
jsiQueueObjectCallbacks(socket, HTTP_NAME_ON_CLOSE, params, 1);
|
|
jsvUnLock(params[0]);
|
|
}
|
|
}
|
|
|
|
|
|
if (!socketClosed) {
|
|
jsvObjectIteratorNext(&it);
|
|
}
|
|
|
|
jsvUnLock3(receiveData, connection, socket);
|
|
}
|
|
jsvUnLock(arr);
|
|
|
|
return hadSockets;
|
|
}
|
|
|
|
|
|
bool socketIdle(JsNetwork *net) {
|
|
if (networkState != NETWORKSTATE_ONLINE) {
|
|
// clear all clients and servers
|
|
_socketCloseAllConnections(net);
|
|
return false;
|
|
}
|
|
bool hadSockets = false;
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_SERVERS,false);
|
|
if (arr) {
|
|
JsvObjectIterator it;
|
|
jsvObjectIteratorNew(&it, arr);
|
|
while (jsvObjectIteratorHasValue(&it)) {
|
|
hadSockets = true;
|
|
|
|
JsVar *server = jsvObjectIteratorGetValue(&it);
|
|
int sckt = (int)jsvGetIntegerAndUnLock(jsvObjectGetChild(server,HTTP_NAME_SOCKET,0))-1; // so -1 if undefined
|
|
|
|
int theClient = netAccept(net, sckt);
|
|
if (theClient >= 0) {
|
|
SocketType socketType = socketGetType(server);
|
|
if ((socketType&ST_TYPE_MASK) == ST_HTTP) {
|
|
JsVar *req = jspNewObject(0, "httpSRq");
|
|
JsVar *res = jspNewObject(0, "httpSRs");
|
|
if (res && req) { // out of memory?
|
|
socketSetType(req, ST_HTTP);
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_SERVER_CONNECTIONS, true);
|
|
if (arr) {
|
|
jsvArrayPush(arr, req);
|
|
jsvUnLock(arr);
|
|
}
|
|
jsvObjectSetChild(req, HTTP_NAME_RESPONSE_VAR, res);
|
|
jsvObjectSetChild(req, HTTP_NAME_SERVER_VAR, server);
|
|
jsvObjectSetChildAndUnLock(req, HTTP_NAME_SOCKET, jsvNewFromInteger(theClient+1));
|
|
}
|
|
jsvUnLock2(req, res);
|
|
} else {
|
|
// Normal sockets
|
|
JsVar *sock = jspNewObject(0, "Socket");
|
|
if (sock) { // out of memory?
|
|
socketSetType(sock, ST_NORMAL);
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_CLIENT_CONNECTIONS, true);
|
|
if (arr) {
|
|
jsvArrayPush(arr, sock);
|
|
jsvUnLock(arr);
|
|
}
|
|
jsvObjectSetChildAndUnLock(sock, HTTP_NAME_SOCKET, jsvNewFromInteger(theClient+1));
|
|
jsiQueueObjectCallbacks(server, HTTP_NAME_ON_CONNECT, &sock, 1);
|
|
jsvUnLock(sock);
|
|
}
|
|
}
|
|
}
|
|
|
|
jsvUnLock(server);
|
|
jsvObjectIteratorNext(&it);
|
|
}
|
|
jsvObjectIteratorFree(&it);
|
|
jsvUnLock(arr);
|
|
}
|
|
|
|
if (socketServerConnectionsIdle(net)) hadSockets = true;
|
|
if (socketClientConnectionsIdle(net)) hadSockets = true;
|
|
netCheckError(net);
|
|
return hadSockets;
|
|
}
|
|
|
|
// -----------------------------
|
|
|
|
JsVar *serverNew(SocketType socketType, JsVar *callback) {
|
|
JsVar *server = jspNewObject(0, ((socketType&ST_TYPE_MASK)==ST_HTTP) ? "httpSrv" : "Server");
|
|
if (!server) return 0; // out of memory
|
|
socketSetType(server, socketType);
|
|
jsvObjectSetChild(server, HTTP_NAME_ON_CONNECT, callback); // no unlock needed
|
|
return server;
|
|
}
|
|
|
|
void serverListen(JsNetwork *net, JsVar *server, int port) {
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_SERVERS, true);
|
|
if (!arr) return; // out of memory
|
|
|
|
jsvObjectSetChildAndUnLock(server, HTTP_NAME_PORT, jsvNewFromInteger(port));
|
|
|
|
int sckt = netCreateSocket(net, 0/*server*/, (unsigned short)port, NCF_NORMAL, 0 /*options*/);
|
|
if (sckt<0) {
|
|
jsError("Unable to create socket\n");
|
|
jsvObjectSetChildAndUnLock(server, HTTP_NAME_CLOSENOW, jsvNewFromBool(true));
|
|
} else {
|
|
jsvObjectSetChildAndUnLock(server, HTTP_NAME_SOCKET, jsvNewFromInteger(sckt+1));
|
|
// add to list of servers
|
|
jsvArrayPush(arr, server);
|
|
}
|
|
jsvUnLock(arr);
|
|
}
|
|
|
|
void serverClose(JsNetwork *net, JsVar *server) {
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_SERVERS,false);
|
|
if (arr) {
|
|
// close socket
|
|
_socketConnectionKill(net, server);
|
|
// remove from array
|
|
JsVar *idx = jsvGetArrayIndexOf(arr, server, true);
|
|
if (idx) {
|
|
jsvRemoveChild(arr, idx);
|
|
jsvUnLock(idx);
|
|
} else
|
|
jsWarn("Server not found!");
|
|
jsvUnLock(arr);
|
|
}
|
|
}
|
|
|
|
|
|
JsVar *clientRequestNew(SocketType socketType, JsVar *options, JsVar *callback) {
|
|
JsVar *arr = socketGetArray(HTTP_ARRAY_HTTP_CLIENT_CONNECTIONS,true);
|
|
if (!arr) return 0;
|
|
JsVar *req, *res = 0;
|
|
if ((socketType&ST_TYPE_MASK)==ST_HTTP) {
|
|
res = jspNewObject(0, "httpCRs");
|
|
if (!res) { jsvUnLock(arr); return 0; } // out of memory?
|
|
req = jspNewObject(0, "httpCRq");
|
|
} else {
|
|
req = jspNewObject(0, "Socket");
|
|
}
|
|
if (req) { // out of memory?
|
|
socketSetType(req, socketType);
|
|
if (callback != NULL)
|
|
jsvUnLock(jsvAddNamedChild(req, callback, HTTP_NAME_ON_CONNECT));
|
|
|
|
jsvArrayPush(arr, req);
|
|
if (res)
|
|
jsvObjectSetChild(req, HTTP_NAME_RESPONSE_VAR, res);
|
|
jsvObjectSetChild(req, HTTP_NAME_OPTIONS_VAR, options);
|
|
}
|
|
jsvUnLock2(res, arr);
|
|
return req;
|
|
}
|
|
|
|
void clientRequestWrite(JsNetwork *net, JsVar *httpClientReqVar, JsVar *data) {
|
|
SocketType socketType = socketGetType(httpClientReqVar);
|
|
// Append data to sendData
|
|
JsVar *sendData = jsvObjectGetChild(httpClientReqVar, HTTP_NAME_SEND_DATA, 0);
|
|
if (!sendData) {
|
|
JsVar *options = 0;
|
|
// Only append a header if we're doing HTTP AND we haven't already connected
|
|
if ((socketType&ST_TYPE_MASK) == ST_HTTP)
|
|
if (jsvGetIntegerAndUnLock(jsvObjectGetChild(httpClientReqVar, HTTP_NAME_SOCKET, 0))==0)
|
|
options = jsvObjectGetChild(httpClientReqVar, HTTP_NAME_OPTIONS_VAR, 0);
|
|
if (options) {
|
|
// We're an HTTP client - make a header
|
|
JsVar *method = jsvObjectGetChild(options, "method", 0);
|
|
JsVar *path = jsvObjectGetChild(options, "path", 0);
|
|
sendData = jsvVarPrintf("%v %v HTTP/1.0\r\nUser-Agent: Espruino "JS_VERSION"\r\nConnection: close\r\n", method, path);
|
|
jsvUnLock2(method, path);
|
|
JsVar *headers = jsvObjectGetChild(options, "headers", 0);
|
|
bool hasHostHeader = false;
|
|
if (jsvIsObject(headers)) {
|
|
JsVar *hostHeader = jsvObjectGetChild(headers, "Host", 0);
|
|
hasHostHeader = hostHeader!=0;
|
|
jsvUnLock(hostHeader);
|
|
httpAppendHeaders(sendData, headers);
|
|
// if Transfer-Encoding:chunked was set, subsequent writes need to 'chunk' the data that is sent
|
|
if (jsvIsStringEqualAndUnLock(jsvObjectGetChild(headers, "Transfer-Encoding", 0), "chunked")) {
|
|
jsvObjectSetChildAndUnLock(httpClientReqVar, HTTP_NAME_CHUNKED, jsvNewFromBool(true));
|
|
}
|
|
}
|
|
jsvUnLock(headers);
|
|
if (!hasHostHeader) {
|
|
JsVar *host = jsvObjectGetChild(options, "host", 0);
|
|
int port = (int)jsvGetIntegerAndUnLock(jsvObjectGetChild(options, "port", 0));
|
|
if (port>0 && port!=80)
|
|
jsvAppendPrintf(sendData, "Host: %v:%d\r\n", host, port);
|
|
else
|
|
jsvAppendPrintf(sendData, "Host: %v\r\n", host);
|
|
jsvUnLock(host);
|
|
}
|
|
// finally add ending newline
|
|
jsvAppendString(sendData, "\r\n");
|
|
} else { // !options
|
|
// We're not HTTP (or were already connected), so don't send any header
|
|
sendData = jsvNewFromString("");
|
|
}
|
|
jsvObjectSetChild(httpClientReqVar, HTTP_NAME_SEND_DATA, sendData);
|
|
jsvUnLock(options);
|
|
}
|
|
// We have data and aren't out of memory...
|
|
if (data && sendData) {
|
|
// append the data to what we want to send
|
|
JsVar *s = jsvAsString(data, false);
|
|
if (s) {
|
|
if ((socketType&ST_TYPE_MASK) == ST_HTTP &&
|
|
jsvGetBoolAndUnLock(jsvObjectGetChild(httpClientReqVar, HTTP_NAME_CHUNKED, 0))) {
|
|
// If we asked to send 'chunked' data, we need to wrap it up,
|
|
// prefixed with the length
|
|
jsvAppendPrintf(sendData, "%x\r\n%v\r\n", jsvGetStringLength(s), s);
|
|
} else {
|
|
jsvAppendStringVarComplete(sendData,s);
|
|
}
|
|
jsvUnLock(s);
|
|
}
|
|
}
|
|
jsvUnLock(sendData);
|
|
if ((socketType&ST_TYPE_MASK) == ST_HTTP) {
|
|
// on HTTP we connect after the first write
|
|
clientRequestConnect(net, httpClientReqVar);
|
|
}
|
|
}
|
|
|
|
// Connect this connection/socket
|
|
void clientRequestConnect(JsNetwork *net, JsVar *httpClientReqVar) {
|
|
// Have we already connected? If so, don't go further
|
|
if (jsvGetIntegerAndUnLock(jsvObjectGetChild(httpClientReqVar, HTTP_NAME_SOCKET, 0))>0)
|
|
return;
|
|
|
|
SocketType socketType = socketGetType(httpClientReqVar);
|
|
|
|
JsVar *options = jsvObjectGetChild(httpClientReqVar, HTTP_NAME_OPTIONS_VAR, false);
|
|
unsigned short port = (unsigned short)jsvGetIntegerAndUnLock(jsvObjectGetChild(options, "port", 0));
|
|
|
|
char hostName[128];
|
|
JsVar *hostNameVar = jsvObjectGetChild(options, "host", 0);
|
|
if (jsvIsUndefined(hostNameVar))
|
|
strncpy(hostName, "localhost", sizeof(hostName));
|
|
else
|
|
jsvGetString(hostNameVar, hostName, sizeof(hostName));
|
|
jsvUnLock(hostNameVar);
|
|
|
|
uint32_t host_addr = 0;
|
|
networkGetHostByName(net, hostName, &host_addr);
|
|
|
|
if(!host_addr) {
|
|
jsError("Unable to locate host\n");
|
|
// As this is already in the list of connections, an error will be thrown on idle anyway
|
|
jsvObjectSetChildAndUnLock(httpClientReqVar, HTTP_NAME_CLOSENOW, jsvNewFromBool(true));
|
|
jsvUnLock(options);
|
|
netCheckError(net);
|
|
return;
|
|
}
|
|
|
|
NetCreateFlags flags = NCF_NORMAL;
|
|
#ifdef USE_TLS
|
|
if (socketType & ST_TLS) {
|
|
flags |= NCF_TLS;
|
|
if (port==0) port = 443;
|
|
}
|
|
#endif
|
|
|
|
if (port==0) port = 80;
|
|
|
|
int sckt = netCreateSocket(net, host_addr, port, flags, options);
|
|
if (sckt<0) {
|
|
jsError("Unable to create socket\n");
|
|
// As this is already in the list of connections, an error will be thrown on idle anyway
|
|
jsvObjectSetChildAndUnLock(httpClientReqVar, HTTP_NAME_CLOSENOW, jsvNewFromBool(true));
|
|
} else {
|
|
jsvObjectSetChildAndUnLock(httpClientReqVar, HTTP_NAME_SOCKET, jsvNewFromInteger(sckt+1));
|
|
}
|
|
|
|
jsvUnLock(options);
|
|
|
|
netCheckError(net);
|
|
}
|
|
|
|
// 'end' this connection
|
|
void clientRequestEnd(JsNetwork *net, JsVar *httpClientReqVar) {
|
|
SocketType socketType = socketGetType(httpClientReqVar);
|
|
if ((socketType&ST_TYPE_MASK) == ST_HTTP) {
|
|
JsVar *finalData = 0;
|
|
if (jsvGetBoolAndUnLock(jsvObjectGetChild(httpClientReqVar, HTTP_NAME_CHUNKED, 0))) {
|
|
// If we were asked to send 'chunked' data, we need to finish up
|
|
finalData = jsvNewFromString("");
|
|
}
|
|
// on HTTP, this actually means we connect
|
|
// force sendData to be made
|
|
clientRequestWrite(net, httpClientReqVar, finalData);
|
|
jsvUnLock(finalData);
|
|
} else {
|
|
// on normal sockets, we actually request close after all data sent
|
|
jsvObjectSetChildAndUnLock(httpClientReqVar, HTTP_NAME_CLOSE, jsvNewFromBool(true));
|
|
// if we never sent any data, make sure we close 'now'
|
|
JsVar *sendData = jsvObjectGetChild(httpClientReqVar, HTTP_NAME_SEND_DATA, 0);
|
|
if (!sendData || jsvIsEmptyString(sendData))
|
|
jsvObjectSetChildAndUnLock(httpClientReqVar, HTTP_NAME_CLOSENOW, jsvNewFromBool(true));
|
|
jsvUnLock(sendData);
|
|
}
|
|
}
|
|
|
|
|
|
void serverResponseWriteHead(JsVar *httpServerResponseVar, int statusCode, JsVar *headers) {
|
|
if (!jsvIsUndefined(headers) && !jsvIsObject(headers)) {
|
|
jsError("Headers sent to writeHead should be an object");
|
|
return;
|
|
}
|
|
|
|
JsVar *sendData = jsvObjectGetChild(httpServerResponseVar, HTTP_NAME_SEND_DATA, 0);
|
|
if (sendData) {
|
|
// If sendData!=0 then we were already called
|
|
jsError("Headers have already been sent");
|
|
jsvUnLock(sendData);
|
|
return;
|
|
}
|
|
|
|
sendData = jsvVarPrintf("HTTP/1.0 %d OK\r\nServer: Espruino "JS_VERSION"\r\n", statusCode);
|
|
if (headers) httpAppendHeaders(sendData, headers);
|
|
// finally add ending newline
|
|
jsvAppendString(sendData, "\r\n");
|
|
jsvObjectSetChildAndUnLock(httpServerResponseVar, HTTP_NAME_SEND_DATA, sendData);
|
|
}
|
|
|
|
|
|
void serverResponseWrite(JsVar *httpServerResponseVar, JsVar *data) {
|
|
// Append data to sendData
|
|
JsVar *sendData = jsvObjectGetChild(httpServerResponseVar, HTTP_NAME_SEND_DATA, 0);
|
|
if (!sendData) {
|
|
// There was no sent data, which means we haven't written headers yet.
|
|
// Do that now with default values
|
|
serverResponseWriteHead(httpServerResponseVar, 200, 0);
|
|
// sendData should now have been set
|
|
sendData = jsvObjectGetChild(httpServerResponseVar, HTTP_NAME_SEND_DATA, 0);
|
|
}
|
|
// check, just in case!
|
|
if (sendData && !jsvIsUndefined(data)) {
|
|
JsVar *s = jsvAsString(data, false);
|
|
if (s) jsvAppendStringVarComplete(sendData,s);
|
|
jsvUnLock(s);
|
|
}
|
|
jsvUnLock(sendData);
|
|
}
|
|
|
|
void serverResponseEnd(JsVar *httpServerResponseVar) {
|
|
serverResponseWrite(httpServerResponseVar, 0); // force connection->sendData to be created even if data not called
|
|
jsvObjectSetChildAndUnLock(httpServerResponseVar, HTTP_NAME_CLOSE, jsvNewFromBool(true));
|
|
}
|
|
|