X Tutup
/* * HTTPSConnection.cpp * * Created on: Dec 6, 2017 * Author: frank */ #include "HTTPSConnection.hpp" namespace httpsserver { HTTPSConnection::HTTPSConnection(ResourceResolver * resResolver): _resResolver(resResolver) { _socket = -1; _addrLen = 0; _ssl = NULL; _bufferProcessed = 0; _bufferUnusedIdx = 0; _connectionState = STATE_UNDEFINED; _clientState = CSTATE_UNDEFINED; _httpHeaders = NULL; _defaultHeaders = NULL; _isKeepAlive = false; _lastTransmissionTS = millis(); _shutdownTS = 0; } HTTPSConnection::~HTTPSConnection() { // Close the socket closeConnection(); } /** * Initializes the connection from a server socket. * * The call WILL BLOCK if accept(serverSocketID) blocks. So use select() to check for that in advance. */ int HTTPSConnection::initialize(int serverSocketID, SSL_CTX * sslCtx, HTTPHeaders *defaultHeaders) { if (_connectionState == STATE_UNDEFINED) { _defaultHeaders = defaultHeaders; _socket = accept(serverSocketID, (struct sockaddr * )&_sockAddr, &_addrLen); // Build up SSL Connection context if the socket has been created successfully if (_socket >= 0) { HTTPS_DLOGHEX("[-->] New connection. Socket fid is: ", _socket); _ssl = SSL_new(sslCtx); if (_ssl) { // Bind SSL to the socket int success = SSL_set_fd(_ssl, _socket); if (success) { // Perform the handshake success = SSL_accept(_ssl); if (success) { _connectionState = STATE_INITIAL; _httpHeaders = new HTTPHeaders(); refreshTimeout(); return _socket; } else { HTTPS_DLOG("[ERR] SSL_accept failed. Aborting handshake."); } } else { HTTPS_DLOG("[ERR] SSL_set_fd failed. Aborting handshake."); } } else { HTTPS_DLOG("[ERR] SSL_new failed. Aborting handshake."); } } _connectionState = STATE_ERROR; _clientState = CSTATE_ACTIVE; // This will only be called if the connection could not be established and cleanup // variables like _ssl etc. closeConnection(); } // Error: The connection has already been established or could not be established return -1; } /** * True if the connection is timed out. * * (Should be checkd in the loop and transition should go to CONNECTION_CLOSE if exceeded) */ bool HTTPSConnection::isTimeoutExceeded() { return _lastTransmissionTS + HTTPS_CONNECTION_TIMEOUT < millis(); } /** * Resets the timeout to allow again the full HTTPS_CONNECTION_TIMEOUT milliseconds */ void HTTPSConnection::refreshTimeout() { _lastTransmissionTS = millis(); } /** * Returns true, if the connection has been closed. */ bool HTTPSConnection::isClosed() { return (_connectionState == STATE_ERROR || _connectionState == STATE_CLOSED); } /** * Returns true, if the connection has been closed due to error */ bool HTTPSConnection::isError() { return (_connectionState == STATE_ERROR); } void HTTPSConnection::closeConnection() { // TODO: Call an event handler here, maybe? if (_connectionState != STATE_ERROR && _connectionState != STATE_CLOSED) { // First call to closeConnection - set the timestamp to calculate the timeout later on if (_connectionState != STATE_CLOSING) { _shutdownTS = millis(); } // Set the connection state to closing. We stay in closing as long as SSL has not been shutdown // correctly _connectionState = STATE_CLOSING; // Try to tear down SSL if (_ssl) { if(SSL_shutdown(_ssl) == 0) { // SSL_shutdown will return 1 as soon as the client answered with close notify // This means we are safe to close the socket SSL_free(_ssl); _ssl = NULL; } else if (_shutdownTS + HTTPS_SHUTDOWN_TIMEOUT < millis()) { // The timeout has been hit, we force SSL shutdown now by freeing the context SSL_free(_ssl); _ssl = NULL; HTTPS_DLOG("[ERR] SSL_shutdown did not receive close notification from the client"); _connectionState = STATE_ERROR; } } // If SSL has been brought down, close the socket if (!_ssl) { // Tear down the socket if (_socket >= 0) { HTTPS_DLOGHEX("[<--] Connection has been closed. fid = ", _socket); close(_socket); _socket = -1; _addrLen = 0; } if (_connectionState != STATE_ERROR) { _connectionState = STATE_CLOSED; } if (_httpHeaders != NULL) { delete _httpHeaders; _httpHeaders = NULL; } } } } /** * This method will try to fill up the buffer with data from */ int HTTPSConnection::updateBuffer() { if (!isClosed()) { // If there is buffer data that has been marked as processed. // Some example is shown here: // // Previous configuration: // GET / HTTP/1.1\\Host: test\\Foo: bar\\\\[some uninitialized memory] // ^ processed ^ unusedIdx // // New configuration after shifting: // Host: test\\Foo: bar\\\\[some uninitialized memory] // ^ processed ^ unusedIdx if (_bufferProcessed > 0) { for(int i = 0; i < HTTPS_CONNECTION_DATA_CHUNK_SIZE; i++) { int copyFrom = i + _bufferProcessed; if (copyFrom < _bufferUnusedIdx) { _receiveBuffer[i] = _receiveBuffer[copyFrom]; } else { break; } } _bufferUnusedIdx -= _bufferProcessed; _bufferProcessed = 0; } if (_bufferUnusedIdx < HTTPS_CONNECTION_DATA_CHUNK_SIZE) { // Check only this socket for data fd_set sockfds; FD_ZERO( &sockfds ); FD_SET(_socket, &sockfds); // We define an immediate timeout (return immediately, if there's no data) timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; // Check for input // As by 2017-12-14, it seems that FD_SETSIZE is defined as 0x40, but socket IDs now // start at 0x1000, so we need to use _socket+1 here select(_socket + 1, &sockfds, NULL, NULL, &timeout); if (FD_ISSET(_socket, &sockfds) || SSL_pending(_ssl) > 0) { HTTPS_DLOGHEX("[ ] There is data on the connection socket. fid=", _socket) // The return code of SSL_read means: // > 0 : Length of the data that has been read // < 0 : Error // = 0 : Connection closed int readReturnCode = SSL_read( _ssl, // Only after the part of the buffer that has not been processed yet _receiveBuffer + sizeof(char) * _bufferUnusedIdx, // Only append up to the end of the buffer HTTPS_CONNECTION_DATA_CHUNK_SIZE - _bufferUnusedIdx ); if (readReturnCode > 0) { _bufferUnusedIdx += readReturnCode; refreshTimeout(); return readReturnCode; } else if (readReturnCode == 0) { // The connection has been closed by the client _clientState = CSTATE_CLOSED; HTTPS_DLOGHEX("[ x ] Client closed connection, fid=", _socket); // TODO: If we are in state websocket, we might need to do something here return 0; } else { // An error occured _connectionState = STATE_ERROR; HTTPS_DLOGHEX("[ERR] An SSL error occured, fid=", _socket); closeConnection(); return -1; } } // data pending } // buffer can read more } return 0; } size_t HTTPSConnection::readBuffer(byte* buffer, size_t length) { updateBuffer(); size_t bufferSize = _bufferUnusedIdx - _bufferProcessed; if (length > bufferSize) { length = bufferSize; } // Write until length is reached (either by param of by empty buffer for(int i = 0; i < length; i++) { buffer[i] = _receiveBuffer[_bufferProcessed++]; } return length; } size_t HTTPSConnection::pendingBufferSize() { updateBuffer(); return _bufferUnusedIdx - _bufferProcessed + SSL_pending(_ssl); } void HTTPSConnection::serverError() { _connectionState = STATE_ERROR; // TODO: Write 500 Serial.println("Server error"); char staticResponse[] = "HTTP/1.1 500 Internal Server Error\r\nServer: esp32https\r\nConnection:close\r\nContent-Type: text/html\r\nContent-Length:34\r\n\r\n

500 Internal Server Error

"; SSL_write(_ssl, staticResponse, strlen(staticResponse)); closeConnection(); } void HTTPSConnection::clientError() { _connectionState = STATE_ERROR; // TODO: Write 400 Serial.println("Client error"); char staticResponse[] = "HTTP/1.1 400 Bad Request\r\nServer: esp32https\r\nConnection:close\r\nContent-Type: text/html\r\nContent-Length:26\r\n\r\n

400 Bad Request

"; SSL_write(_ssl, staticResponse, strlen(staticResponse)); closeConnection(); } void HTTPSConnection::readLine(int lengthLimit) { while(_bufferProcessed < _bufferUnusedIdx) { char newChar = _receiveBuffer[_bufferProcessed]; if ( newChar == '\r') { // Look ahead for \n (if not possible, wait for next round if (_bufferProcessed+1 < _bufferUnusedIdx) { if (_receiveBuffer[_bufferProcessed+1] == '\n') { _bufferProcessed += 2; _parserLine.parsingFinished = true; return; } else { // Line has not been terminated by \r\n HTTPS_DLOG("[ERR] Line that has not been terminated by \\r\\n (got only \\r). Client error."); clientError(); return; } } } else { _parserLine.text += newChar; _bufferProcessed += 1; } // Check that the max request string size is not exceeded if (_parserLine.text.length() > lengthLimit) { HTTPS_DLOG("[ERR] Line length exceeded. Server error."); serverError(); return; } } } /** * Called by the request to signal that the client has closed the connection */ void HTTPSConnection::signalClientClose() { _clientState = CSTATE_CLOSED; } /** * Called by the request to signal that an error has occured */ void HTTPSConnection::signalRequestError() { // TODO: Check that no response has been transmitted yet serverError(); } /** * Returns the cache size that should be cached (in the response) to enable keep-alive requests. * * 0 = no keep alive. */ size_t HTTPSConnection::getCacheSize() { return (_isKeepAlive ? HTTPS_KEEPALIVE_CACHESIZE : 0); } void HTTPSConnection::loop() { // First, update the buffer // newByteCount will contain the number of new bytes that have to be processed updateBuffer(); if (_clientState == CSTATE_CLOSED) { HTTPS_DLOGHEX("[ ] Client closed in state", _clientState) } if (_clientState == CSTATE_CLOSED && _bufferProcessed == _bufferUnusedIdx && _connectionState < STATE_HEADERS_FINISHED) { closeConnection(); } if (!isClosed() && isTimeoutExceeded()) { HTTPS_DLOGHEX("[zZz] Connection timeout exceeded, closing connection. fid=", _socket) closeConnection(); } if (!isError()) { // State machine (Reading request, reading headers, ...) switch(_connectionState) { case STATE_INITIAL: // Read request line readLine(HTTPS_REQUEST_MAX_REQUEST_LENGTH); if (_parserLine.parsingFinished && !isClosed()) { // Find the method size_t spaceAfterMethodIdx = _parserLine.text.find(' '); if (spaceAfterMethodIdx == std::string::npos) { HTTPS_DLOG("[ERR] Missing space after HTTP method. Client Error.") clientError(); break; } _httpMethod = _parserLine.text.substr(0, spaceAfterMethodIdx); // Find the resource string: size_t spaceAfterResourceIdx = _parserLine.text.find(' ', spaceAfterMethodIdx + 1); if (spaceAfterResourceIdx == std::string::npos) { HTTPS_DLOG("[ERR] Missing space after HTTP resource. Client Error.") clientError(); break; } _httpResource = _parserLine.text.substr(spaceAfterMethodIdx + 1, spaceAfterResourceIdx - _httpMethod.length() - 1); _parserLine.parsingFinished = false; _parserLine.text = ""; HTTPS_DLOG(("[ ] Request line finished: method="+_httpMethod+", resource="+_httpResource).c_str()); _connectionState = STATE_REQUEST_FINISHED; } break; case STATE_REQUEST_FINISHED: // Read headers while (_bufferProcessed < _bufferUnusedIdx && !isClosed()) { readLine(HTTPS_REQUEST_MAX_HEADER_LENGTH); if (_parserLine.parsingFinished && _connectionState != STATE_ERROR) { if (_parserLine.text.empty()) { HTTPS_DLOG("[ ] Headers finished"); _connectionState = STATE_HEADERS_FINISHED; // Break, so that the rest of the body does not get flushed through _parserLine.parsingFinished = false; _parserLine.text = ""; break; } else { int idxColon = _parserLine.text.find(':'); if ( (idxColon != std::string::npos) && (_parserLine.text[idxColon+1]==' ') ) { _httpHeaders->set(new HTTPHeader( _parserLine.text.substr(0, idxColon), _parserLine.text.substr(idxColon+2) )); } else { HTTPS_DLOG("Malformed header line detected. Client error."); HTTPS_DLOG(_parserLine.text.c_str()); clientError(); break; } } _parserLine.parsingFinished = false; _parserLine.text = ""; } } break; case STATE_HEADERS_FINISHED: // Handle body { HTTPS_DLOG("[ ] Resolving resource..."); ResolvedResource resolvedResource; _resResolver->resolveNode(_httpMethod, _httpResource, resolvedResource); if (resolvedResource.didMatch()) { // Did the client request connection:keep-alive? HTTPHeader * connectionHeader = _httpHeaders->get("Connection"); if (connectionHeader != NULL && std::string("keep-alive").compare(connectionHeader->_value)==0) { HTTPS_DLOGHEX("[ ] Keep-Alive activated. fid=", _socket); _isKeepAlive = true; } else { HTTPS_DLOGHEX("[ ] Keep-Alive disabled. fid=", _socket); _isKeepAlive = false; } // Create request context HTTPRequest req = HTTPRequest(this, _httpHeaders, resolvedResource.getParams()); HTTPResponse res = HTTPResponse(this); // Add default headers to the response auto allDefaultHeaders = _defaultHeaders->getAll(); for(std::vector::iterator header = allDefaultHeaders->begin(); header != allDefaultHeaders->end(); ++header) { res.setHeader((*header)->_name, (*header)->_value); } // Call the callback HTTPS_DLOG("[ ] Calling handler function"); resolvedResource.getMatchingNode()->_callback(&req, &res); // The callback-function should have read all of the request body. // However, if it does not, we need to clear the request body now, // because otherwise it would be parsed in the next request. if (!req.requestComplete()) { HTTPS_DLOG("[ERR] Callback function did not parse full request body"); req.discardRequestBody(); } // Handling the request is done HTTPS_DLOG("[ ] Handler function done, request complete"); // Now we need to check if we can use keep-alive to reuse the SSL connection // However, if the client did not set content-size or defined connection: close, // we have no chance to do so. if (!_isKeepAlive) { // No KeepAlive -> We are done. Transition to next state. if (!isClosed()) { _connectionState = STATE_BODY_FINISHED; } } else { if (res.isResponseBuffered()) { // If the response could be buffered: res.setHeader("Connection", "keep-alive"); res.finalize(); if (_clientState != CSTATE_CLOSED) { // Refresh the timeout for the new request refreshTimeout(); // Reset headers for the new connection _httpHeaders->clearAll(); // Go back to initial state _connectionState = STATE_INITIAL; } } // The response could not be buffered or the client has closed: if (!isClosed() && _connectionState!=STATE_INITIAL) { _connectionState = STATE_BODY_FINISHED; } } } else { HTTPS_DLOG("[ERR] Could not find a matching resource. Server error."); serverError(); } } break; case STATE_BODY_FINISHED: // Request is complete closeConnection(); break; case STATE_CLOSING: // As long as we are in closing state, we call closeConnection() again and wait for it to finish or timeout closeConnection(); break; case STATE_WEBSOCKET: // Do handling of the websocket break; default:; } } } } /* namespace httpsserver */
X Tutup