From b4f64f72a3e4bf590c60b0cbd6cd365aa1a58542 Mon Sep 17 00:00:00 2001 From: Lennart Weller Date: Wed, 25 May 2016 12:36:24 +0200 Subject: Imported Upstream version 1.2.0 --- src/web_client.c | 513 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 376 insertions(+), 137 deletions(-) (limited to 'src/web_client.c') diff --git a/src/web_client.c b/src/web_client.c index 6500a59b2..601dda083 100644 --- a/src/web_client.c +++ b/src/web_client.c @@ -26,6 +26,7 @@ #include "global_statistics.h" #include "rrd.h" #include "rrd2json.h" +#include "registry.h" #include "web_client.h" #include "../config.h" @@ -72,8 +73,8 @@ struct web_client *web_client_create(int listener) if(getnameinfo(sadr, addrlen, w->client_ip, NI_MAXHOST, w->client_port, NI_MAXSERV, NI_NUMERICHOST | NI_NUMERICSERV) != 0) { error("Cannot getnameinfo() on received client connection."); - strncpy(w->client_ip, "UNKNOWN", NI_MAXHOST); - strncpy(w->client_port, "UNKNOWN", NI_MAXSERV); + strncpyz(w->client_ip, "UNKNOWN", NI_MAXHOST); + strncpyz(w->client_port, "UNKNOWN", NI_MAXSERV); } w->client_ip[NI_MAXHOST] = '\0'; w->client_port[NI_MAXSERV] = '\0'; @@ -128,6 +129,7 @@ struct web_client *web_client_create(int listener) return NULL; } + w->origin[0] = '*'; w->wait_receive = 1; if(web_clients) web_clients->prev = w; @@ -173,8 +175,18 @@ void web_client_reset(struct web_client *w) } w->last_url[0] = '\0'; + w->cookie1[0] = '\0'; + w->cookie2[0] = '\0'; + w->origin[0] = '*'; + w->origin[1] = '\0'; w->mode = WEB_CLIENT_MODE_NORMAL; + w->enable_gzip = 0; + w->keepalive = 0; + if(w->decoded_url) { + free(w->decoded_url); + w->decoded_url = NULL; + } buffer_reset(w->response.header_output); buffer_reset(w->response.header); @@ -316,7 +328,7 @@ int mysendfile(struct web_client *w, char *filename) // access the file char webfilename[FILENAME_MAX + 1]; - snprintf(webfilename, FILENAME_MAX, "%s/%s", web_dir, filename); + snprintfz(webfilename, FILENAME_MAX, "%s/%s", web_dir, filename); // check if the file exists struct stat stat; @@ -341,7 +353,7 @@ int mysendfile(struct web_client *w, char *filename) } if((stat.st_mode & S_IFMT) == S_IFDIR) { - snprintf(webfilename, FILENAME_MAX+1, "%s/index.html", filename); + snprintfz(webfilename, FILENAME_MAX, "%s/index.html", filename); return mysendfile(w, webfilename); } @@ -644,7 +656,7 @@ int web_client_api_request_v1_data(struct web_client *w, char *url) if(!name || !*name) continue; if(!value || !*value) continue; - debug(D_WEB_CLIENT, "%llu: API v1 query param '%s' with value '%s'", w->id, name, value); + debug(D_WEB_CLIENT, "%llu: API v1 data query param '%s' with value '%s'", w->id, name, value); // name and value are now the parameters // they are not null and not empty @@ -784,19 +796,149 @@ cleanup: return ret; } +int web_client_api_request_v1_registry(struct web_client *w, char *url) +{ + char person_guid[36 + 1] = ""; + + debug(D_WEB_CLIENT, "%llu: API v1 registry with URL '%s'", w->id, url); + + // FIXME + // The browser may send multiple cookies with our id + + char *cookie = strstr(w->response.data->buffer, " " NETDATA_REGISTRY_COOKIE_NAME "="); + if(cookie) + strncpyz(person_guid, &cookie[sizeof(NETDATA_REGISTRY_COOKIE_NAME) + 1], 36); + + char action = '\0'; + char *machine_guid = NULL, + *machine_url = NULL, + *url_name = NULL, + *search_machine_guid = NULL, + *delete_url = NULL, + *to_person_guid = NULL; + + while(url) { + char *value = mystrsep(&url, "?&[]"); + if (!value || !*value) continue; + + char *name = mystrsep(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + debug(D_WEB_CLIENT, "%llu: API v1 registry query param '%s' with value '%s'", w->id, name, value); + + if(!strcmp(name, "action")) { + if(!strcmp(value, "access")) action = 'A'; + else if(!strcmp(value, "hello")) action = 'H'; + else if(!strcmp(value, "delete")) action = 'D'; + else if(!strcmp(value, "search")) action = 'S'; + else if(!strcmp(value, "switch")) action = 'W'; + } + else if(!strcmp(name, "machine")) + machine_guid = value; + + else if(!strcmp(name, "url")) + machine_url = value; + + else if(action == 'A') { + if(!strcmp(name, "name")) + url_name = value; + } + else if(action == 'D') { + if(!strcmp(name, "delete_url")) + delete_url = value; + } + else if(action == 'S') { + if(!strcmp(name, "for")) + search_machine_guid = value; + } + else if(action == 'W') { + if(!strcmp(name, "to")) + to_person_guid = value; + } + } + + if(action == 'A' && (!machine_guid || !machine_url || !url_name)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Invalid registry request - access requires these parameters: machine ('%s'), url ('%s'), name ('%s')", + machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", url_name?url_name:"UNSET"); + return 400; + } + else if(action == 'D' && (!machine_guid || !machine_url || !delete_url)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Invalid registry request - delete requires these parameters: machine ('%s'), url ('%s'), delete_url ('%s')", + machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", delete_url?delete_url:"UNSET"); + return 400; + } + else if(action == 'S' && (!machine_guid || !machine_url || !search_machine_guid)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Invalid registry request - search requires these parameters: machine ('%s'), url ('%s'), for ('%s')", + machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", search_machine_guid?search_machine_guid:"UNSET"); + return 400; + } + else if(action == 'W' && (!machine_guid || !machine_url || !to_person_guid)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Invalid registry request - switching identity requires these parameters: machine ('%s'), url ('%s'), to ('%s')", + machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", to_person_guid?to_person_guid:"UNSET"); + return 400; + } + + switch(action) { + case 'A': + return registry_request_access_json(w, person_guid, machine_guid, machine_url, url_name, time(NULL)); + + case 'D': + return registry_request_delete_json(w, person_guid, machine_guid, machine_url, delete_url, time(NULL)); + + case 'S': + return registry_request_search_json(w, person_guid, machine_guid, machine_url, search_machine_guid, time(NULL)); + + case 'W': + return registry_request_switch_json(w, person_guid, machine_guid, machine_url, to_person_guid, time(NULL)); + + case 'H': + return registry_request_hello_json(w); + + default: + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Invalid registry request - you need to set an action: hello, access, delete, search"); + return 400; + } + + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Invalid or no registry action."); + return 400; +} + int web_client_api_request_v1(struct web_client *w, char *url) { + static uint32_t data_hash = 0, chart_hash = 0, charts_hash = 0, registry_hash = 0; + + if(unlikely(data_hash == 0)) { + data_hash = simple_hash("data"); + chart_hash = simple_hash("chart"); + charts_hash = simple_hash("charts"); + registry_hash = simple_hash("registry"); + } + // get the command char *tok = mystrsep(&url, "/?&"); if(tok && *tok) { debug(D_WEB_CLIENT, "%llu: Searching for API v1 command '%s'.", w->id, tok); + uint32_t hash = simple_hash(tok); - if(strcmp(tok, "data") == 0) + if(hash == data_hash && !strcmp(tok, "data")) return web_client_api_request_v1_data(w, url); - else if(strcmp(tok, "chart") == 0) + + else if(hash == chart_hash && !strcmp(tok, "chart")) return web_client_api_request_v1_chart(w, url); - else if(strcmp(tok, "charts") == 0) + + else if(hash == charts_hash && !strcmp(tok, "charts")) return web_client_api_request_v1_charts(w, url); + + else if(hash == registry_hash && !strcmp(tok, "registry")) + return web_client_api_request_v1_registry(w, url); + else { buffer_flush(w->response.data); buffer_sprintf(w->response.data, "Unsupported v1 API command: %s", tok); @@ -1043,110 +1185,209 @@ cleanup: } */ -void web_client_process(struct web_client *w) { - int code = 500; - ssize_t bytes; - int enable_gzip = 0; - w->wait_receive = 0; +static inline char *http_header_parse(struct web_client *w, char *s) { + char *e = s; - // check if we have an empty line (end of HTTP header) - if(strstr(w->response.data->buffer, "\r\n\r\n")) { - global_statistics_lock(); - global_statistics.web_requests++; - global_statistics_unlock(); + // find the : + while(*e && *e != ':') e++; + if(!*e || e[1] != ' ') return e; - gettimeofday(&w->tv_in, NULL); - debug(D_WEB_DATA, "%llu: Processing data buffer of %d bytes: '%s'.", w->id, w->response.data->len, w->response.data->buffer); + // get the name + *e = '\0'; + + // find the value + char *v, *ve; + v = ve = e + 2; + + // find the \r + while(*ve && *ve != '\r') ve++; + if(!*ve || ve[1] != '\n') { + *e = ':'; + return ve; + } + + // terminate the value + *ve = '\0'; - // check if the client requested keep-alive HTTP - if(strcasestr(w->response.data->buffer, "Connection: keep-alive")) w->keepalive = 1; - else w->keepalive = 0; + // fprintf(stderr, "HEADER: '%s' = '%s'\n", s, v); + if(!strcasecmp(s, "Origin")) + strncpyz(w->origin, v, ORIGIN_MAX); + + else if(!strcasecmp(s, "Connection")) { + if(strcasestr(v, "keep-alive")) + w->keepalive = 1; + } #ifdef NETDATA_WITH_ZLIB - // check if the client accepts deflate - if(web_enable_gzip && strstr(w->response.data->buffer, "gzip")) - enable_gzip = 1; -#endif // NETDATA_WITH_ZLIB + else if(!strcasecmp(s, "Accept-Encoding")) { + if(web_enable_gzip && strcasestr(v, "gzip")) { + w->enable_gzip = 1; + } + } +#endif /* NETDATA_WITH_ZLIB */ + + *e = ':'; + *ve = '\r'; + return ve; +} - int datasource_type = DATASOURCE_DATATABLE_JSONP; - //if(strstr(w->response.data->buffer, "X-DataSource-Auth")) - // datasource_type = DATASOURCE_GOOGLE_JSON; +// http_request_validate() +// returns: +// = 0 : all good, process the request +// > 0 : request is complete, but is not supported +// < 0 : request is incomplete - wait for more data - char *buf = (char *)buffer_tostring(w->response.data); - char *tok = strsep(&buf, " \r\n"); - char *url = NULL; - char *pointer_to_free = NULL; // keep url_decode() allocated buffer +static inline int http_request_validate(struct web_client *w) { + char *s = w->response.data->buffer, *encoded_url = NULL; + // is is a valid request? + if(!strncmp(s, "GET ", 4)) { + encoded_url = s = &s[4]; w->mode = WEB_CLIENT_MODE_NORMAL; + } + else if(!strncmp(s, "OPTIONS ", 8)) { + encoded_url = s = &s[8]; + w->mode = WEB_CLIENT_MODE_OPTIONS; + } + else { + w->wait_receive = 0; + return 1; + } + + // find the SPACE + "HTTP/" + while(*s) { + // find the space + while (*s && *s != ' ') s++; + + // is it SPACE + "HTTP/" ? + if(*s && !strncmp(s, " HTTP/", 6)) break; + else s++; + } + + // incomplete requests + if(!*s) { + w->wait_receive = 1; + return -2; + } + + // we have the end of encoded_url - remember it + char *ue = s; + + while(*s) { + // find a line feed + while (*s && *s != '\r') s++; + + // did we reach the end? + if(unlikely(!*s)) break; - if(buf && strcmp(tok, "GET") == 0) { - tok = strsep(&buf, " \r\n"); - pointer_to_free = url = url_decode(tok); - debug(D_WEB_CLIENT, "%llu: Processing HTTP GET on url '%s'.", w->id, url); + // is it \r\n ? + if (likely(s[1] == '\n')) { + + // is it again \r\n ? (header end) + if(unlikely(s[2] == '\r' && s[3] == '\n')) { + // a valid complete HTTP request found + + *ue = '\0'; + w->decoded_url = url_decode(encoded_url); + *ue = ' '; + + w->wait_receive = 0; + return 0; + } + + // another header line + s = http_header_parse(w, &s[2]); } - else if(buf && strcmp(tok, "OPTIONS") == 0) { - tok = strsep(&buf, " \r\n"); - pointer_to_free = url = url_decode(tok); - debug(D_WEB_CLIENT, "%llu: Processing HTTP OPTIONS on url '%s'.", w->id, url); - w->mode = WEB_CLIENT_MODE_OPTIONS; + else s++; + } + + // incomplete request + w->wait_receive = 1; + return -3; +} + +void web_client_process(struct web_client *w) { + int code = 500; + ssize_t bytes; + + int what_to_do = http_request_validate(w); + + // wait for more data + if(what_to_do < 0) { + if(w->response.data->len > TOO_BIG_REQUEST) { + strcpy(w->last_url, "too big request"); + + debug(D_WEB_CLIENT_ACCESS, "%llu: Received request is too big (%zd bytes).", w->id, w->response.data->len); + + code = 400; + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Received request is too big (%zd bytes).\r\n", w->response.data->len); } - else if (buf && strcmp(tok, "POST") == 0) { - w->keepalive = 0; - tok = strsep(&buf, " \r\n"); - pointer_to_free = url = url_decode(tok); - debug(D_WEB_CLIENT, "%llu: I don't know how to handle POST with form data. Assuming it is a GET on url '%s'.", w->id, url); + else { + // wait for more data + return; } + } + else if(what_to_do > 0) { + strcpy(w->last_url, "not a valid response"); - w->last_url[0] = '\0'; + debug(D_WEB_CLIENT_ACCESS, "%llu: Cannot understand '%s'.", w->id, w->response.data->buffer); - if(w->mode == WEB_CLIENT_MODE_OPTIONS) { - strncpy(w->last_url, url, URL_MAX); - w->last_url[URL_MAX] = '\0'; + code = 500; + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "I don't understand you...\r\n"); + } + else { // what_to_do == 0 + gettimeofday(&w->tv_in, NULL); + global_statistics_lock(); + global_statistics.web_requests++; + global_statistics_unlock(); + + // copy the URL - we are going to overwrite parts of it + // FIXME -- we should avoid it + strncpyz(w->last_url, w->decoded_url, URL_MAX); + + if(w->mode == WEB_CLIENT_MODE_OPTIONS) { code = 200; w->response.data->contenttype = CT_TEXT_PLAIN; buffer_flush(w->response.data); buffer_strcat(w->response.data, "OK"); } - else if(url) { + else { #ifdef NETDATA_WITH_ZLIB - if(enable_gzip) + if(w->enable_gzip) web_client_enable_deflate(w); #endif - strncpy(w->last_url, url, URL_MAX); - w->last_url[URL_MAX] = '\0'; - - tok = mystrsep(&url, "/?"); + char *url = w->decoded_url; + char *tok = mystrsep(&url, "/?"); if(tok && *tok) { debug(D_WEB_CLIENT, "%llu: Processing command '%s'.", w->id, tok); if(strcmp(tok, "api") == 0) { // the client is requesting api access - datasource_type = DATASOURCE_JSON; code = web_client_api_request(w, url); } -#ifdef NETDATA_INTERNAL_CHECKS - else if(strcmp(tok, "exit") == 0) { - netdata_exit = 1; + else if(strcmp(tok, "netdata.conf") == 0) { code = 200; + debug(D_WEB_CLIENT_ACCESS, "%llu: Sending netdata.conf ...", w->id); + w->response.data->contenttype = CT_TEXT_PLAIN; buffer_flush(w->response.data); - buffer_strcat(w->response.data, "will do"); + generate_config(w->response.data, 0); } -#endif else if(strcmp(tok, WEB_PATH_DATA) == 0) { // "data" - // the client is requesting rrd data - datasource_type = DATASOURCE_JSON; - code = web_client_data_request(w, url, datasource_type); + // the client is requesting rrd data -- OLD API + code = web_client_data_request(w, url, DATASOURCE_JSON); } else if(strcmp(tok, WEB_PATH_DATASOURCE) == 0) { // "datasource" - // the client is requesting google datasource - code = web_client_data_request(w, url, datasource_type); + // the client is requesting google datasource -- OLD API + code = web_client_data_request(w, url, DATASOURCE_DATATABLE_JSONP); } else if(strcmp(tok, WEB_PATH_GRAPH) == 0) { // "graph" - // the client is requesting an rrd graph + // the client is requesting an rrd graph -- OLD API // get the name of the data to show tok = mystrsep(&url, "/?&"); @@ -1176,7 +1417,40 @@ void web_client_process(struct web_client *w) { buffer_strcat(w->response.data, "Graph name?\r\n"); } } + else if(strcmp(tok, "list") == 0) { + // OLD API + code = 200; + + debug(D_WEB_CLIENT_ACCESS, "%llu: Sending list of RRD_STATS...", w->id); + + buffer_flush(w->response.data); + RRDSET *st = rrdset_root; + + for ( ; st ; st = st->next ) + buffer_sprintf(w->response.data, "%s\n", st->name); + } + else if(strcmp(tok, "all.json") == 0) { + // OLD API + code = 200; + debug(D_WEB_CLIENT_ACCESS, "%llu: Sending JSON list of all monitors of RRD_STATS...", w->id); + + w->response.data->contenttype = CT_APPLICATION_JSON; + buffer_flush(w->response.data); + rrd_stats_all_json(w->response.data); + } #ifdef NETDATA_INTERNAL_CHECKS + else if(strcmp(tok, "exit") == 0) { + code = 200; + w->response.data->contenttype = CT_TEXT_PLAIN; + buffer_flush(w->response.data); + + if(!netdata_exit) + buffer_strcat(w->response.data, "ok, will do..."); + else + buffer_strcat(w->response.data, "I am doing it already"); + + netdata_exit = 1; + } else if(strcmp(tok, "debug") == 0) { buffer_flush(w->response.data); @@ -1196,7 +1470,7 @@ void web_client_process(struct web_client *w) { else { code = 200; debug_flags |= D_RRD_STATS; - st->debug = st->debug?0:1; + st->debug = !st->debug; buffer_sprintf(w->response.data, "Chart %s has now debug %s.\r\n", tok, st->debug?"enabled":"disabled"); debug(D_WEB_CLIENT_ACCESS, "%llu: debug for %s is %s.", w->id, tok, st->debug?"enabled":"disabled"); } @@ -1218,39 +1492,11 @@ void web_client_process(struct web_client *w) { // just leave the buffer as is // it will be copied back to the client } -#endif - else if(strcmp(tok, "list") == 0) { - code = 200; - - debug(D_WEB_CLIENT_ACCESS, "%llu: Sending list of RRD_STATS...", w->id); - - buffer_flush(w->response.data); - RRDSET *st = rrdset_root; - - for ( ; st ; st = st->next ) - buffer_sprintf(w->response.data, "%s\n", st->name); - } - else if(strcmp(tok, "all.json") == 0) { - code = 200; - debug(D_WEB_CLIENT_ACCESS, "%llu: Sending JSON list of all monitors of RRD_STATS...", w->id); - - w->response.data->contenttype = CT_APPLICATION_JSON; - buffer_flush(w->response.data); - rrd_stats_all_json(w->response.data); - } - else if(strcmp(tok, "netdata.conf") == 0) { - code = 200; - debug(D_WEB_CLIENT_ACCESS, "%llu: Sending netdata.conf ...", w->id); - - w->response.data->contenttype = CT_TEXT_PLAIN; - buffer_flush(w->response.data); - generate_config(w->response.data, 0); - } +#endif /* NETDATA_INTERNAL_CHECKS */ else { char filename[FILENAME_MAX+1]; url = filename; - strncpy(filename, w->last_url, FILENAME_MAX); - filename[FILENAME_MAX] = '\0'; + strncpyz(filename, w->last_url, FILENAME_MAX); tok = mystrsep(&url, "?"); buffer_flush(w->response.data); code = mysendfile(w, (tok && *tok)?tok:"/"); @@ -1259,42 +1505,12 @@ void web_client_process(struct web_client *w) { else { char filename[FILENAME_MAX+1]; url = filename; - strncpy(filename, w->last_url, FILENAME_MAX); - filename[FILENAME_MAX] = '\0'; + strncpyz(filename, w->last_url, FILENAME_MAX); tok = mystrsep(&url, "?"); buffer_flush(w->response.data); code = mysendfile(w, (tok && *tok)?tok:"/"); } } - else { - strcpy(w->last_url, "not a valid response"); - - if(buf) debug(D_WEB_CLIENT_ACCESS, "%llu: Cannot understand '%s'.", w->id, buf); - - code = 500; - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "I don't understand you...\r\n"); - } - - // free url_decode() buffer - if(pointer_to_free) { - free(pointer_to_free); - pointer_to_free = NULL; - } - } - else if(w->response.data->len > TOO_BIG_REQUEST) { - strcpy(w->last_url, "too big request"); - - debug(D_WEB_CLIENT_ACCESS, "%llu: Received request is too big (%zd bytes).", w->id, w->response.data->len); - - code = 400; - buffer_flush(w->response.data); - buffer_sprintf(w->response.data, "Received request is too big (%zd bytes).\r\n", w->response.data->len); - } - else { - // wait for more data - w->wait_receive = 1; - return; } gettimeofday(&w->tv_ready, NULL); @@ -1415,6 +1631,10 @@ void web_client_process(struct web_client *w) { code_msg = "Not Found"; break; + case 412: + code_msg = "Preconditions Failed"; + break; + default: code_msg = "Internal Server Error"; break; @@ -1428,18 +1648,37 @@ void web_client_process(struct web_client *w) { "HTTP/1.1 %d %s\r\n" "Connection: %s\r\n" "Server: NetData Embedded HTTP Server\r\n" + "Access-Control-Allow-Origin: %s\r\n" + "Access-Control-Allow-Credentials: true\r\n" "Content-Type: %s\r\n" - "Access-Control-Allow-Origin: *\r\n" - "Access-Control-Allow-Methods: GET, OPTIONS\r\n" - "Access-Control-Allow-Headers: accept, x-requested-with\r\n" - "Access-Control-Max-Age: 86400\r\n" "Date: %s\r\n" , code, code_msg , w->keepalive?"keep-alive":"close" + , w->origin , content_type_string , date ); + if(w->cookie1[0]) { + buffer_sprintf(w->response.header_output, + "Set-Cookie: %s\r\n", + w->cookie1); + } + + if(w->cookie2[0]) { + buffer_sprintf(w->response.header_output, + "Set-Cookie: %s\r\n", + w->cookie2); + } + + if(w->mode == WEB_CLIENT_MODE_OPTIONS) { + buffer_strcat(w->response.header_output, + "Access-Control-Allow-Methods: GET, OPTIONS\r\n" + "Access-Control-Allow-Headers: accept, x-requested-with, origin, content-type, cookie\r\n" + "Access-Control-Max-Age: 1209600\r\n" // 86400 * 14 + ); + } + if(buffer_strlen(w->response.header)) buffer_strcat(w->response.header_output, buffer_tostring(w->response.header)); @@ -1449,7 +1688,7 @@ void web_client_process(struct web_client *w) { "Cache-Control: no-cache\r\n" , date); } - else { + else if(w->mode != WEB_CLIENT_MODE_OPTIONS) { char edate[100]; time_t et = w->response.data->date + (86400 * 14); struct tm etmbuf, *etm = gmtime_r(&et, &etmbuf); -- cgit v1.2.3