Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
Fork of HTTPClient by
HTTPClient.cpp
00001 /* HTTPClient.cpp */ 00002 /* Copyright (C) 2012 mbed.org, MIT License 00003 * 00004 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 00005 * and associated documentation files (the "Software"), to deal in the Software without restriction, 00006 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 00007 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 00008 * furnished to do so, subject to the following conditions: 00009 * 00010 * The above copyright notice and this permission notice shall be included in all copies or 00011 * substantial portions of the Software. 00012 * 00013 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 00014 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 00015 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 00016 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 00017 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 00018 */ 00019 00020 //Debug is disabled by default 00021 #if 0 00022 //Enable debug 00023 #include <cstdio> 00024 #define DBG(x, ...) std::printf("[HTTPClient : DBG]"x"\r\n", ##__VA_ARGS__); 00025 #define WARN(x, ...) std::printf("[HTTPClient : WARN]"x"\r\n", ##__VA_ARGS__); 00026 #define ERR(x, ...) std::printf("[HTTPClient : ERR]"x"\r\n", ##__VA_ARGS__); 00027 00028 #else 00029 //Disable debug 00030 #define DBG(x, ...) 00031 #define WARN(x, ...) 00032 #define ERR(x, ...) 00033 00034 #endif 00035 00036 #define HTTP_PORT 80 00037 00038 #define OK 0 00039 00040 #define MIN(x,y) (((x)<(y))?(x):(y)) 00041 #define MAX(x,y) (((x)>(y))?(x):(y)) 00042 00043 #define CHUNK_SIZE 256 00044 00045 #include <cstring> 00046 00047 #include "HTTPClient.h" 00048 00049 HTTPClient::HTTPClient() : 00050 m_sock(), m_basicAuthUser(NULL), m_basicAuthPassword(NULL), m_httpResponseCode(0) 00051 { 00052 00053 } 00054 00055 HTTPClient::~HTTPClient() 00056 { 00057 00058 } 00059 00060 #if 0 00061 void HTTPClient::basicAuth(const char* user, const char* password) //Basic Authentification 00062 { 00063 m_basicAuthUser = user; 00064 m_basicAuthPassword = password; 00065 } 00066 #endif 00067 00068 HTTPResult HTTPClient::get(const char* url, IHTTPDataIn* pDataIn, int timeout /*= HTTP_CLIENT_DEFAULT_TIMEOUT*/) //Blocking 00069 { 00070 return connect(url, HTTP_GET, NULL, pDataIn, timeout); 00071 } 00072 00073 HTTPResult HTTPClient::get(const char* url, char* result, size_t maxResultLen, int timeout /*= HTTP_CLIENT_DEFAULT_TIMEOUT*/) //Blocking 00074 { 00075 HTTPText str(result, maxResultLen); 00076 return get(url, &str, timeout); 00077 } 00078 00079 HTTPResult HTTPClient::post(const char* url, const IHTTPDataOut& dataOut, IHTTPDataIn* pDataIn, int timeout /*= HTTP_CLIENT_DEFAULT_TIMEOUT*/) //Blocking 00080 { 00081 return connect(url, HTTP_POST, (IHTTPDataOut*)&dataOut, pDataIn, timeout); 00082 } 00083 00084 HTTPResult HTTPClient::put(const char* url, const IHTTPDataOut& dataOut, IHTTPDataIn* pDataIn, int timeout /*= HTTP_CLIENT_DEFAULT_TIMEOUT*/) //Blocking 00085 { 00086 return connect(url, HTTP_PUT, (IHTTPDataOut*)&dataOut, pDataIn, timeout); 00087 } 00088 00089 HTTPResult HTTPClient::del(const char* url, IHTTPDataIn* pDataIn, int timeout /*= HTTP_CLIENT_DEFAULT_TIMEOUT*/) //Blocking 00090 { 00091 return connect(url, HTTP_DELETE, NULL, pDataIn, timeout); 00092 } 00093 00094 00095 int HTTPClient::getHTTPResponseCode() 00096 { 00097 return m_httpResponseCode; 00098 } 00099 00100 #define CHECK_CONN_ERR(ret) \ 00101 do{ \ 00102 if(ret) { \ 00103 m_sock.close(); \ 00104 ERR("Connection error (%d)", ret); \ 00105 return HTTP_CONN; \ 00106 } \ 00107 } while(0) 00108 00109 #define PRTCL_ERR() \ 00110 do{ \ 00111 m_sock.close(); \ 00112 ERR("Protocol error"); \ 00113 return HTTP_PRTCL; \ 00114 } while(0) 00115 00116 HTTPResult HTTPClient::connect(const char* url, HTTP_METH method, IHTTPDataOut* pDataOut, IHTTPDataIn* pDataIn, int timeout) //Execute request 00117 { 00118 m_httpResponseCode = 0; //Invalidate code 00119 m_timeout = timeout; 00120 00121 pDataIn->writeReset(); 00122 if( pDataOut ) { 00123 pDataOut->readReset(); 00124 } 00125 00126 char scheme[8]; 00127 uint16_t port; 00128 char host[32]; 00129 char path[64]; 00130 //First we need to parse the url (http[s]://host[:port][/[path]]) -- HTTPS not supported (yet?) 00131 HTTPResult res = parseURL(url, scheme, sizeof(scheme), host, sizeof(host), &port, path, sizeof(path)); 00132 if(res != HTTP_OK) { 00133 ERR("parseURL returned %d", res); 00134 return res; 00135 } 00136 00137 if(port == 0) { //TODO do handle HTTPS->443 00138 port = 80; 00139 } 00140 00141 DBG("Scheme: %s", scheme); 00142 DBG("Host: %s", host); 00143 DBG("Port: %d", port); 00144 DBG("Path: %s", path); 00145 00146 //Connect 00147 DBG("Connecting socket to server"); 00148 int ret = m_sock.connect(host, port); 00149 if (ret < 0) { 00150 m_sock.close(); 00151 ERR("Could not connect"); 00152 return HTTP_CONN; 00153 } 00154 00155 //Send request 00156 DBG("Sending request"); 00157 char buf[CHUNK_SIZE]; 00158 const char* meth = (method==HTTP_GET)?"GET":(method==HTTP_POST)?"POST":(method==HTTP_PUT)?"PUT":(method==HTTP_DELETE)?"DELETE":""; 00159 snprintf(buf, sizeof(buf), "%s %s HTTP/1.1\r\nHost: %s\r\n", meth, path, host); //Write request 00160 ret = send(buf); 00161 if(ret) { 00162 m_sock.close(); 00163 ERR("Could not write request"); 00164 return HTTP_CONN; 00165 } 00166 00167 //Send all headers 00168 00169 //Send default headers 00170 DBG("Sending headers"); 00171 if( pDataOut != NULL ) { 00172 if( pDataOut->getIsChunked() ) { 00173 ret = send("Transfer-Encoding: chunked\r\n"); 00174 CHECK_CONN_ERR(ret); 00175 } else { 00176 snprintf(buf, sizeof(buf), "Content-Length: %d\r\n", pDataOut->getDataLen()); 00177 ret = send(buf); 00178 CHECK_CONN_ERR(ret); 00179 } 00180 char type[48]; 00181 if( pDataOut->getDataType(type, 48) == HTTP_OK ) { 00182 snprintf(buf, sizeof(buf), "Content-Type: %s\r\n", type); 00183 ret = send(buf); 00184 CHECK_CONN_ERR(ret); 00185 } 00186 00187 //Send specific headers 00188 while( pDataOut->getHeader(buf, sizeof(buf) - 3) ) { //must have space left for CRLF + 0 terminating char 00189 size_t headerlen = strlen(buf); 00190 snprintf(buf + headerlen, sizeof(buf) - headerlen, "\r\n"); 00191 ret = send(buf); 00192 CHECK_CONN_ERR(ret); 00193 } 00194 } 00195 00196 //Send specific headers 00197 while( pDataIn->getHeader(buf, sizeof(buf) - 3) ) { 00198 size_t headerlen = strlen(buf); 00199 snprintf(buf + headerlen, sizeof(buf) - headerlen, "\r\n"); 00200 ret = send(buf); 00201 CHECK_CONN_ERR(ret); 00202 } 00203 00204 //Close headers 00205 DBG("Headers sent"); 00206 ret = send("\r\n"); 00207 CHECK_CONN_ERR(ret); 00208 00209 size_t trfLen; 00210 00211 //Send data (if available) 00212 if( pDataOut != NULL ) { 00213 DBG("Sending data"); 00214 while(true) { 00215 size_t writtenLen = 0; 00216 pDataOut->read(buf, CHUNK_SIZE, &trfLen); 00217 if( pDataOut->getIsChunked() ) { 00218 //Write chunk header 00219 char chunkHeader[16]; 00220 snprintf(chunkHeader, sizeof(chunkHeader), "%X\r\n", trfLen); //In hex encoding 00221 ret = send(chunkHeader); 00222 CHECK_CONN_ERR(ret); 00223 } else if( trfLen == 0 ) { 00224 break; 00225 } 00226 if( trfLen != 0 ) { 00227 ret = send(buf, trfLen); 00228 CHECK_CONN_ERR(ret); 00229 } 00230 00231 if( pDataOut->getIsChunked() ) { 00232 ret = send("\r\n"); //Chunk-terminating CRLF 00233 CHECK_CONN_ERR(ret); 00234 } else { 00235 writtenLen += trfLen; 00236 if( writtenLen >= pDataOut->getDataLen() ) { 00237 break; 00238 } 00239 } 00240 00241 if( trfLen == 0 ) { 00242 break; 00243 } 00244 } 00245 } 00246 00247 //Receive response 00248 DBG("Receiving response"); 00249 ret = recv(buf, 1, CHUNK_SIZE - 1, &trfLen); //Read n bytes 00250 CHECK_CONN_ERR(ret); 00251 00252 buf[trfLen] = '\0'; 00253 00254 //Make sure we got the first response line 00255 char* crlfPtr = NULL; 00256 while( true ) { 00257 crlfPtr = strstr(buf, "\r\n"); 00258 if(crlfPtr == NULL) { 00259 if( trfLen < CHUNK_SIZE - 1 ) { 00260 size_t newTrfLen; 00261 ret = recv(buf + trfLen, 1, CHUNK_SIZE - trfLen - 1, &newTrfLen); 00262 trfLen += newTrfLen; 00263 buf[trfLen] = '\0'; 00264 DBG("Read %d chars; In buf: [%s]", newTrfLen, buf); 00265 CHECK_CONN_ERR(ret); 00266 continue; 00267 } else { 00268 PRTCL_ERR(); 00269 } 00270 } 00271 break; 00272 } 00273 00274 int crlfPos = crlfPtr - buf; 00275 buf[crlfPos] = '\0'; 00276 00277 //Parse HTTP response 00278 //if( sscanf(buf, "HTTP/%*d.%*d %d %*[^\r\n]", &m_httpResponseCode) != 1 ) 00279 if(crlfPos > 13) { 00280 buf[13] = '\0'; 00281 } 00282 if( sscanf(buf, "HTTP/%*d.%*d %d", &m_httpResponseCode) != 1 ) { //Kludge for newlib nano 00283 //Cannot match string, error 00284 ERR("Not a correct HTTP answer : %s\n", buf); 00285 PRTCL_ERR(); 00286 } 00287 00288 if( (m_httpResponseCode < 200) || (m_httpResponseCode >= 300) ) { 00289 //Did not return a 2xx code; TODO fetch headers/(&data?) anyway and implement a mean of writing/reading headers 00290 WARN("Response code %d", m_httpResponseCode); 00291 PRTCL_ERR(); 00292 } 00293 00294 DBG("Reading headers"); 00295 00296 memmove(buf, &buf[crlfPos+2], trfLen - (crlfPos + 2) + 1); //Be sure to move NULL-terminating char as well 00297 trfLen -= (crlfPos + 2); 00298 00299 size_t recvContentLength = 0; 00300 bool recvChunked = false; 00301 bool recvLengthUnknown = true; 00302 //Now get headers 00303 while( true ) { 00304 crlfPtr = strstr(buf, "\r\n"); 00305 if(crlfPtr == NULL) { 00306 if( trfLen < CHUNK_SIZE - 1 ) { 00307 size_t newTrfLen; 00308 ret = recv(buf + trfLen, 1, CHUNK_SIZE - trfLen - 1, &newTrfLen); 00309 trfLen += newTrfLen; 00310 buf[trfLen] = '\0'; 00311 DBG("Read %d chars; In buf: [%s]", newTrfLen, buf); 00312 CHECK_CONN_ERR(ret); 00313 continue; 00314 } else { 00315 PRTCL_ERR(); 00316 } 00317 } 00318 00319 crlfPos = crlfPtr - buf; 00320 00321 if(crlfPos == 0) { //End of headers 00322 DBG("Headers read"); 00323 memmove(buf, &buf[2], trfLen - 2 + 1); //Be sure to move NULL-terminating char as well 00324 trfLen -= 2; 00325 break; 00326 } 00327 00328 buf[crlfPos] = '\0'; 00329 00330 char key[32]; 00331 char value[32]; 00332 00333 //key[31] = '\0'; 00334 //value[31] = '\0'; 00335 00336 memset(key, 0, 32); 00337 memset(value, 0, 32); 00338 00339 //int n = sscanf(buf, "%31[^:]: %31[^\r\n]", key, value); 00340 00341 int n = 0; 00342 00343 char* keyEnd = strchr(buf, ':'); 00344 if(keyEnd != NULL) { 00345 *keyEnd = '\0'; 00346 if(strlen(buf) < 32) { 00347 strcpy(key, buf); 00348 n++; 00349 char* valueStart = keyEnd + 2; 00350 if( (valueStart - buf) < crlfPos ) { 00351 if (strlen(valueStart) > 31) { 00352 char* ee = valueStart + 31; 00353 *ee = '\0'; 00354 } 00355 strcpy(value, valueStart); 00356 n++; 00357 00358 } 00359 } 00360 } 00361 if ( n == 2 ) { 00362 DBG("Read header : %s: %s\n", key, value); 00363 if( !strcmp(key, "Content-Length") ) { 00364 sscanf(value, "%d", &recvContentLength); 00365 recvLengthUnknown = false; 00366 pDataIn->setDataLen(recvContentLength); 00367 } else if( !strcmp(key, "Transfer-Encoding") ) { 00368 if( !strcmp(value, "Chunked") || !strcmp(value, "chunked") ) { 00369 recvChunked = true; 00370 recvLengthUnknown = false; 00371 pDataIn->setIsChunked(true); 00372 } 00373 } else if( !strcmp(key, "Content-Type") ) { 00374 pDataIn->setDataType(value); 00375 } 00376 00377 memmove(buf, &buf[crlfPos+2], trfLen - (crlfPos + 2) + 1); //Be sure to move NULL-terminating char as well 00378 trfLen -= (crlfPos + 2); 00379 00380 } else { 00381 ERR("Could not parse header"); 00382 PRTCL_ERR(); 00383 } 00384 00385 } 00386 00387 //Receive data 00388 DBG("Receiving data"); 00389 while(true) { 00390 size_t readLen = 0; 00391 00392 if( recvChunked ) { 00393 //Read chunk header 00394 bool foundCrlf; 00395 do { 00396 foundCrlf = false; 00397 crlfPos=0; 00398 buf[trfLen]=0; 00399 if(trfLen >= 2) { 00400 for(; crlfPos < trfLen - 2; crlfPos++) { 00401 if( buf[crlfPos] == '\r' && buf[crlfPos + 1] == '\n' ) { 00402 foundCrlf = true; 00403 break; 00404 } 00405 } 00406 } 00407 if(!foundCrlf) { //Try to read more 00408 if( trfLen < CHUNK_SIZE ) { 00409 size_t newTrfLen; 00410 ret = recv(buf + trfLen, 0, CHUNK_SIZE - trfLen - 1, &newTrfLen); 00411 trfLen += newTrfLen; 00412 CHECK_CONN_ERR(ret); 00413 continue; 00414 } else { 00415 PRTCL_ERR(); 00416 } 00417 } 00418 } while(!foundCrlf); 00419 buf[crlfPos] = '\0'; 00420 int n = sscanf(buf, "%x", &readLen); 00421 if(n!=1) { 00422 ERR("Could not read chunk length"); 00423 PRTCL_ERR(); 00424 } 00425 00426 memmove(buf, &buf[crlfPos+2], trfLen - (crlfPos + 2)); //Not need to move NULL-terminating char any more 00427 trfLen -= (crlfPos + 2); 00428 00429 if( readLen == 0 ) { 00430 //Last chunk 00431 break; 00432 } 00433 } else { 00434 readLen = recvContentLength; 00435 } 00436 00437 DBG("Retrieving %d bytes (%d bytes in buffer)", readLen, trfLen); 00438 00439 do { 00440 if(recvLengthUnknown ) { 00441 readLen = trfLen; 00442 } 00443 pDataIn->write(buf, MIN(trfLen, readLen)); 00444 if(!recvLengthUnknown) { 00445 if( trfLen > readLen ) { 00446 memmove(buf, &buf[readLen], trfLen - readLen); 00447 trfLen -= readLen; 00448 readLen = 0; 00449 } else { 00450 readLen -= trfLen; 00451 } 00452 } else { 00453 trfLen = 0; 00454 } 00455 00456 if(readLen || recvLengthUnknown) { 00457 ret = recv(buf, 1, CHUNK_SIZE - trfLen - 1, &trfLen); 00458 if(recvLengthUnknown && (ret == HTTP_CLOSED)) { 00459 //Write and exit 00460 pDataIn->write(buf, trfLen); 00461 break; 00462 } 00463 CHECK_CONN_ERR(ret); 00464 if(recvLengthUnknown && (trfLen == 0)) { 00465 break; 00466 } 00467 } 00468 } while(readLen || recvLengthUnknown); 00469 00470 if( recvChunked ) { 00471 if(trfLen < 2) { 00472 size_t newTrfLen; 00473 //Read missing chars to find end of chunk 00474 ret = recv(buf + trfLen, 2 - trfLen, CHUNK_SIZE - trfLen - 1, &newTrfLen); 00475 CHECK_CONN_ERR(ret); 00476 trfLen += newTrfLen; 00477 } 00478 if( (buf[0] != '\r') || (buf[1] != '\n') ) { 00479 ERR("Format error"); 00480 PRTCL_ERR(); 00481 } 00482 memmove(buf, &buf[2], trfLen - 2); 00483 trfLen -= 2; 00484 } else { 00485 break; 00486 } 00487 00488 } 00489 00490 m_sock.close(); 00491 DBG("Completed HTTP transaction"); 00492 00493 return HTTP_OK; 00494 } 00495 00496 HTTPResult HTTPClient::recv(char* buf, size_t minLen, size_t maxLen, size_t* pReadLen) //0 on success, err code on failure 00497 { 00498 DBG("Trying to read between %d and %d bytes", minLen, maxLen); 00499 size_t readLen = 0; 00500 00501 if(!m_sock.is_connected()) { 00502 WARN("Connection was closed by server"); 00503 return HTTP_CLOSED; //Connection was closed by server 00504 } 00505 00506 int ret; 00507 while(readLen < maxLen) { 00508 if(readLen < minLen) { 00509 DBG("Trying to read at most %d bytes [Blocking]", minLen - readLen); 00510 m_sock.set_blocking(false, m_timeout); 00511 ret = m_sock.receive_all(buf + readLen, minLen - readLen); 00512 } else { 00513 DBG("Trying to read at most %d bytes [Not blocking]", maxLen - readLen); 00514 m_sock.set_blocking(false, 0); 00515 ret = m_sock.receive(buf + readLen, maxLen - readLen); 00516 } 00517 00518 if( ret > 0) { 00519 readLen += ret; 00520 } else if( ret == 0 ) { 00521 break; 00522 } else { 00523 if(!m_sock.is_connected()) { 00524 ERR("Connection error (recv returned %d)", ret); 00525 *pReadLen = readLen; 00526 return HTTP_CONN; 00527 } else { 00528 break; 00529 } 00530 } 00531 00532 if(!m_sock.is_connected()) { 00533 break; 00534 } 00535 } 00536 DBG("Read %d bytes", readLen); 00537 *pReadLen = readLen; 00538 return HTTP_OK; 00539 } 00540 00541 HTTPResult HTTPClient::send(char* buf, size_t len) //0 on success, err code on failure 00542 { 00543 if(len == 0) { 00544 len = strlen(buf); 00545 } 00546 DBG("Trying to write %d bytes", len); 00547 size_t writtenLen = 0; 00548 00549 if(!m_sock.is_connected()) { 00550 WARN("Connection was closed by server"); 00551 return HTTP_CLOSED; //Connection was closed by server 00552 } 00553 00554 m_sock.set_blocking(false, m_timeout); 00555 int ret = m_sock.send_all(buf, len); 00556 if(ret > 0) { 00557 writtenLen += ret; 00558 } else if( ret == 0 ) { 00559 WARN("Connection was closed by server"); 00560 return HTTP_CLOSED; //Connection was closed by server 00561 } else { 00562 ERR("Connection error (send returned %d)", ret); 00563 return HTTP_CONN; 00564 } 00565 00566 DBG("Written %d bytes", writtenLen); 00567 return HTTP_OK; 00568 } 00569 00570 HTTPResult HTTPClient::parseURL(const char* url, char* scheme, size_t maxSchemeLen, char* host, size_t maxHostLen, uint16_t* port, char* path, size_t maxPathLen) //Parse URL 00571 { 00572 char* schemePtr = (char*) url; 00573 char* hostPtr = (char*) strstr(url, "://"); 00574 if(hostPtr == NULL) { 00575 WARN("Could not find host"); 00576 return HTTP_PARSE; //URL is invalid 00577 } 00578 00579 if( maxSchemeLen < hostPtr - schemePtr + 1 ) { //including NULL-terminating char 00580 WARN("Scheme str is too small (%d >= %d)", maxSchemeLen, hostPtr - schemePtr + 1); 00581 return HTTP_PARSE; 00582 } 00583 memcpy(scheme, schemePtr, hostPtr - schemePtr); 00584 scheme[hostPtr - schemePtr] = '\0'; 00585 00586 hostPtr+=3; 00587 00588 size_t hostLen = 0; 00589 00590 char* portPtr = strchr(hostPtr, ':'); 00591 if( portPtr != NULL ) { 00592 hostLen = portPtr - hostPtr; 00593 portPtr++; 00594 if( sscanf(portPtr, "%hu", port) != 1) { 00595 WARN("Could not find port"); 00596 return HTTP_PARSE; 00597 } 00598 } else { 00599 *port=0; 00600 } 00601 char* pathPtr = strchr(hostPtr, '/'); 00602 if( hostLen == 0 ) { 00603 hostLen = pathPtr - hostPtr; 00604 } 00605 00606 if( maxHostLen < hostLen + 1 ) { //including NULL-terminating char 00607 WARN("Host str is too small (%d >= %d)", maxHostLen, hostLen + 1); 00608 return HTTP_PARSE; 00609 } 00610 memcpy(host, hostPtr, hostLen); 00611 host[hostLen] = '\0'; 00612 00613 size_t pathLen; 00614 char* fragmentPtr = strchr(hostPtr, '#'); 00615 if(fragmentPtr != NULL) { 00616 pathLen = fragmentPtr - pathPtr; 00617 } else { 00618 pathLen = strlen(pathPtr); 00619 } 00620 00621 if( maxPathLen < pathLen + 1 ) { //including NULL-terminating char 00622 WARN("Path str is too small (%d >= %d)", maxPathLen, pathLen + 1); 00623 return HTTP_PARSE; 00624 } 00625 memcpy(path, pathPtr, pathLen); 00626 path[pathLen] = '\0'; 00627 00628 return HTTP_OK; 00629 }
Generated on Tue Jul 12 2022 18:28:04 by
1.7.2
