From 10ddc97d221989e107c5283e3d5df8c48a23dc26 Mon Sep 17 00:00:00 2001 From: ame Date: Fri, 1 Aug 2025 14:20:08 -0500 Subject: write file, and code should be an int --- src/net.c | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index a6f3bf4..70400a2 100644 --- a/src/net.c +++ b/src/net.c @@ -590,6 +590,11 @@ int l_srequest(lua_State* L){ luaI_treplk(L, idx, "Path", "code"); luaI_treplk(L, idx, "Request", "version"); luaI_treplk(L, idx, "Version", "code-name"); + + lua_pushstring(L, "code"); + lua_gettable(L, idx); + int code = atoi(lua_tostring(L, -1)); + luaI_tseti(L, idx, "code", code); void* encoding = parray_get(owo, "Transfer-Encoding"); -- cgit v1.2.3 From 9ee19b1e0af44f48f39bd6ce57a0cb85eb1147ad Mon Sep 17 00:00:00 2001 From: ame Date: Fri, 1 Aug 2025 18:00:47 -0500 Subject: length should include host --- src/net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index 70400a2..9af82f7 100644 --- a/src/net.c +++ b/src/net.c @@ -545,7 +545,7 @@ int l_srequest(lua_State* L){ //char* req = "GET / HTTP/1.1\nHost: amyy.cc\nConnection: Close\n\n"; - char* request = calloc(cont_len + header->len + 512, sizeof * request); + char* request = calloc(cont_len + header->len + strlen(host) + strlen(path) + 512, sizeof * request); sprintf(request, "%s %s HTTP/1.1\r\nHost: %s\r\nConnection: Close%s\r\n\r\n%s", action, path, host, header->c, cont); //printf("%s\n", request); str_free(header); -- cgit v1.2.3 From 6b0cda77a3e04e4fb3024b21bb648b6bf9f62568 Mon Sep 17 00:00:00 2001 From: ame Date: Fri, 1 Aug 2025 19:27:34 -0500 Subject: send user-agent --- src/net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index 9af82f7..b5b75bf 100644 --- a/src/net.c +++ b/src/net.c @@ -546,7 +546,7 @@ int l_srequest(lua_State* L){ //char* req = "GET / HTTP/1.1\nHost: amyy.cc\nConnection: Close\n\n"; char* request = calloc(cont_len + header->len + strlen(host) + strlen(path) + 512, sizeof * request); - sprintf(request, "%s %s HTTP/1.1\r\nHost: %s\r\nConnection: Close%s\r\n\r\n%s", action, path, host, header->c, cont); + sprintf(request, "%s %s HTTP/1.1\r\nUser-Agent: lullaby/"MAJOR_VERSION"\r\nHost: %s\r\nConnection: Close%s\r\n\r\n%s", action, path, host, header->c, cont); //printf("%s\n", request); str_free(header); -- cgit v1.2.3 From cd1c124e2def659dd2919e4387047de733afcc59 Mon Sep 17 00:00:00 2001 From: ame Date: Fri, 1 Aug 2025 21:27:11 -0500 Subject: make user-agent editable with default header values --- src/net.c | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index b5b75bf..34e20c0 100644 --- a/src/net.c +++ b/src/net.c @@ -484,7 +484,6 @@ int _srequest_chunked_encoding(char* input, int length, struct chunked_encoding_ int l_srequest(lua_State* L){ int params = lua_gettop(L); - int check = 1; uint64_t ilen = 0; char* request_url = (char*)lua_tolstring(L, 1, &ilen); struct url awa = parse_url(request_url, ilen); @@ -510,12 +509,11 @@ int l_srequest(lua_State* L){ char* cont = ""; size_t cont_len = 0; - if(params >= check + 1){ - check++; - switch(lua_type(L, check)){ + if(params >= 2){ + switch(lua_type(L, 2)){ case LUA_TNUMBER: case LUA_TSTRING: - cont = (char*)luaL_tolstring(L, check, &cont_len); + cont = (char*)luaL_tolstring(L, 2, &cont_len); break; default: p_fatal("cant send type"); @@ -523,31 +521,35 @@ int l_srequest(lua_State* L){ } } - str* header = str_init(""); - if(params >= check + 1){ - check++; - lua_pushnil(L); - - for(;lua_next(L, check) != 0;){ - str_push(header, "\r\n"); - str_push(header, lua_tostring(L, -2)); - str_push(header, ": "); - str_push(header, lua_tostring(L, -1)); - lua_pop(L, 1); - } + lua_newtable(L); + int header_idx = lua_gettop(L); + luaI_tsets(L, header_idx, "User-Agent", "lullaby/"MAJOR_VERSION); + + if(params >= 3){ + luaI_jointable(L, header_idx, 3); } + + str* header = str_init(""); + lua_pushnil(L); + + for(;lua_next(L, header_idx) != 0;){ + str_push(header, "\r\n"); + str_push(header, lua_tostring(L, -2)); + str_push(header, ": "); + str_push(header, lua_tostring(L, -1)); + lua_pop(L, 1); + } char* action = "GET"; - if(params >= check + 1){ - check++; - action = (char*)lua_tostring(L, check); + if(params >= 4){ + action = (char*)lua_tostring(L, 4); } //char* req = "GET / HTTP/1.1\nHost: amyy.cc\nConnection: Close\n\n"; char* request = calloc(cont_len + header->len + strlen(host) + strlen(path) + 512, sizeof * request); - sprintf(request, "%s %s HTTP/1.1\r\nUser-Agent: lullaby/"MAJOR_VERSION"\r\nHost: %s\r\nConnection: Close%s\r\n\r\n%s", action, path, host, header->c, cont); - //printf("%s\n", request); + sprintf(request, "%s %s HTTP/1.1\r\nHost: %s\r\nConnection: Close%s\r\n\r\n%s", action, path, host, header->c, cont); + str_free(header); int s = SSL_write(ssl, request, strlen(request)); -- cgit v1.2.3 From 0f2fb182199bf0c7e8d996400b82b2dd2b2463df Mon Sep 17 00:00:00 2001 From: ame Date: Sat, 2 Aug 2025 04:23:14 -0500 Subject: l_request, http support --- src/net.c | 143 ++++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 79 insertions(+), 64 deletions(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index 34e20c0..04fd5a1 100644 --- a/src/net.c +++ b/src/net.c @@ -389,20 +389,27 @@ int l_wss(lua_State* L){ return 1; } -struct _srequest_state { +struct request_state { SSL* ssl; SSL_CTX* ctx; int sock; + int secure; str* buffer; //anything pre-existing struct chunked_encoding_state* state; }; +ssize_t _request_read(struct request_state* state, void* buffer, size_t count); + int _srequest_free(void** _state){ - struct _srequest_state* state = *((struct _srequest_state**)_state); - SSL_set_shutdown(state->ssl, SSL_RECEIVED_SHUTDOWN | SSL_SENT_SHUTDOWN); - SSL_shutdown(state->ssl); - SSL_free(state->ssl); - SSL_CTX_free(state->ctx); + struct request_state* state = *((struct request_state**)_state); + + if(state->secure){ + SSL_set_shutdown(state->ssl, SSL_RECEIVED_SHUTDOWN | SSL_SENT_SHUTDOWN); + SSL_shutdown(state->ssl); + SSL_free(state->ssl); + SSL_CTX_free(state->ctx); + } + close(state->sock); if(state->state != NULL){ @@ -419,7 +426,7 @@ int _srequest_free(void** _state){ } int _srequest_read(uint64_t reqlen, str** _output, void** _state){ - struct _srequest_state* state = *((struct _srequest_state**)_state); + struct request_state* state = *((struct request_state**)_state); str* output = *_output; //states using chunked encoding should skip this if(state->buffer != NULL){ @@ -431,7 +438,7 @@ int _srequest_read(uint64_t reqlen, str** _output, void** _state){ char buffer[BUFFER_LEN]; memset(buffer, 0, BUFFER_LEN); uint64_t len; - for(; (len = SSL_read(state->ssl, buffer, BUFFER_LEN)) > 0;){ + for(; (len = _request_read(state, buffer, BUFFER_LEN)) > 0;){ if(state->state != NULL){ chunked_encoding_round(buffer, len, state->state); } else { @@ -481,31 +488,67 @@ int _srequest_chunked_encoding(char* input, int length, struct chunked_encoding_ return 0; } +int _request(lua_State* L, struct request_state* state); + int l_srequest(lua_State* L){ + struct request_state* state = calloc(sizeof * state, 1); + state->secure = 1; + + return _request(L, state); +} + +int l_request(lua_State* L){ + struct request_state* state = calloc(sizeof * state, 1); + state->secure = 0; + + return _request(L, state); +} + +ssize_t _request_read(struct request_state* state, void* buffer, size_t count){ + if(state->secure){ + return SSL_read(state->ssl, buffer, count); + } else { + return read(state->sock, buffer, count); + } +} + +ssize_t _request_write(struct request_state* state, const void* buffer, size_t count){ + if(state->secure){ + return SSL_write(state->ssl, buffer, count); + } else { + return write(state->sock, buffer, count); + } +} + +int _request(lua_State* L, struct request_state* state){ int params = lua_gettop(L); uint64_t ilen = 0; char* request_url = (char*)lua_tolstring(L, 1, &ilen); struct url awa = parse_url(request_url, ilen); if(awa.proto != NULL && strcmp(awa.proto->c, "http") == 0){ - //send to l_request, todo - abort(); + state->secure = 0; + } else if (awa.proto != NULL && strcmp(awa.proto->c, "https") == 0){ + state->secure = 1; } + const char* host = awa.domain == NULL ? request_url : awa.domain->c; - const char* port = awa.port == NULL ? "443" : awa.port->c; + const char* port = awa.port == NULL ? + (state->secure ? "443" : "80") + : awa.port->c; char* path = awa.path == NULL ? "/" : awa.path->c; - int sock = get_host((char*)host, (char*)port); - if(sock == -1){ - //p_fatal("could not resolve address"); - //abort(); + state->sock = get_host((char*)host, (char*)port); + if(state->sock == -1){ luaI_error(L, -1, "error resolving address"); } - ssl_init(); - SSL_CTX* ctx = SSL_CTX_new(SSLv23_client_method()); - SSL* ssl = ssl_connect(ctx, sock, host); - if(ssl == NULL) luaI_error(L, -1, "ssl_connect error"); + if(state->secure){ + ssl_init(); + state->ctx = SSL_CTX_new(SSLv23_client_method()); + state->ssl = ssl_connect(state->ctx, state->sock, host); + if(state->ssl == NULL) luaI_error(L, -1, "ssl_connect error"); + } char* cont = ""; size_t cont_len = 0; @@ -552,10 +595,10 @@ int l_srequest(lua_State* L){ str_free(header); - int s = SSL_write(ssl, request, strlen(request)); + int s = _request_write(state, request, strlen(request)); free(request); - if(s <= 0) luaI_error(L, s, "SSL_write error"); + if(s <= 0) luaI_error(L, s, "_request_write error"); str* a = str_init(""); char buffer[BUFFER_LEN]; @@ -563,7 +606,7 @@ int l_srequest(lua_State* L){ int extra_len = 0; char* header_eof = NULL; - for(; (len = SSL_read(ssl, buffer, BUFFER_LEN)) > 0;){ + for(; (len = _request_read(state, buffer, BUFFER_LEN)) > 0;){ int blen = a->len; str_pushl(a, buffer, len); int offset = blen >= 4 ? 4 : blen; @@ -574,7 +617,7 @@ int l_srequest(lua_State* L){ memset(buffer, 0, BUFFER_LEN); } - if(len < 0) luaI_error(L, len, "SSL_read error"); + if(len < 0) luaI_error(L, len, "read error"); if(header_eof != NULL){ lua_newtable(L); @@ -600,29 +643,29 @@ int l_srequest(lua_State* L){ void* encoding = parray_get(owo, "Transfer-Encoding"); - struct _srequest_state *read_state = calloc(sizeof * read_state, 1); - read_state->ctx = ctx; - read_state->ssl = ssl; - read_state->sock = sock; + //struct _srequest_state *read_state = calloc(sizeof * read_state, 1); + //read_state->ctx = state->ctx; + //read_state->ssl = state->ssl; + //read_state->sock = state->sock; if(encoding != NULL){ if(strcmp(((str*)encoding)->c, "chunked") == 0){ - struct chunked_encoding_state* state = calloc(sizeof * state, 1); - state->reading_length = 1; - state->buffer = str_init(""); - state->content = str_init(""); + struct chunked_encoding_state* chunk_state = calloc(sizeof * state, 1); + chunk_state->reading_length = 1; + chunk_state->buffer = str_init(""); + chunk_state->content = str_init(""); - chunked_encoding_round(header_eof + 4, extra_len - 4, state); + chunked_encoding_round(header_eof + 4, extra_len - 4, chunk_state); memset(buffer, 0, BUFFER_LEN); - read_state->buffer = str_init(""); - read_state->state = state; + state->buffer = str_init(""); + state->state = chunk_state; } } else { - read_state->buffer = str_initl(header_eof + 4, extra_len - 4); + state->buffer = str_initl(header_eof + 4, extra_len - 4); } parray_clear(owo, STR); - luaI_newstream(L, _srequest_read, _srequest_free, read_state); + luaI_newstream(L, _srequest_read, _srequest_free, state); int v = lua_gettop(L); luaI_tsetv(L, idx, "content", v); lua_pushvalue(L, idx); @@ -640,34 +683,6 @@ int l_srequest(lua_State* L){ return 1; } -int l_request(lua_State* L){ - const char* host = luaL_checkstring(L, 1); - int sock = get_host((char*)host, (char*)lua_tostring(L, 2)); - - char* path = "/"; - if(lua_gettop(L) >= 3) - path = (char*)luaL_checkstring(L, 3); - - //char* req = "GET / HTTP/1.1\nHost: amyy.cc\nConnection: Close\n\n"; - - char request[2000]; - sprintf(request, "GET %s HTTP/1.1\nHost: %s\nConnection: Close\n\n", path, host); - write(sock, request, strlen(request)); - - str* a = str_init(""); - char buffer[512]; - int len = 0; - - for(; (len = read(sock, buffer, 511)) != 0;){ - str_pushl(a, buffer, len); - memset(buffer, 0, 512); - } - - lua_pushstring(L, a->c); - - return 1; -} - #define max_uri_size 2048 _Atomic size_t threads = 0; -- cgit v1.2.3 From 05cd214e8d4a4a3f560327f414a82d388cfe6755 Mon Sep 17 00:00:00 2001 From: ame Date: Tue, 5 Aug 2025 03:45:01 -0500 Subject: skip empty chunks --- src/net.c | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index 04fd5a1..a21579c 100644 --- a/src/net.c +++ b/src/net.c @@ -97,7 +97,8 @@ int chunked_encoding_round(char* input, int length, struct chunked_encoding_stat str_popb(state->buffer, 2); state->chunk_length = strtoll(state->buffer->c, NULL, 16); str_clear(state->buffer); - state->reading_length = 0; + if(state->chunk_length != 0) + state->reading_length = 0; } } else { int len = lesser(state->chunk_length - state->buffer->len, length - i); @@ -456,38 +457,6 @@ int _srequest_read(uint64_t reqlen, str** _output, void** _state){ return 1; } -int _srequest_chunked_encoding(char* input, int length, struct chunked_encoding_state* state){ - //printf("'%s'\n", input); - for(int i = 0; i < length; i++){ - //printf("%i/%i\n", i, length); - if(state->reading_length){ - str_pushl(state->buffer, input + i, 1); - - if(state->buffer->len >= 2 && memmem(state->buffer->c + state->buffer->len - 2, 2, "\r\n", 2)){ - - str_popb(state->buffer, 2); - state->chunk_length = strtoll(state->buffer->c, NULL, 16); - str_clear(state->buffer); - state->reading_length = 0; - } - } else { - int len = lesser(state->chunk_length - state->buffer->len, length - i); - str_pushl(state->buffer, input + i, len); - i += len; - - if(state->buffer->len >= state->chunk_length){ - state->reading_length = 1; - str_pushl(state->content, state->buffer->c, state->buffer->len); - str_clear(state->buffer); - } - } - } - - //printf("buffer '%s'\n", state->buffer->c); - - return 0; -} - int _request(lua_State* L, struct request_state* state); int l_srequest(lua_State* L){ -- cgit v1.2.3 From 7cc8cade712506c7eeaf3a8e0002cf2313218885 Mon Sep 17 00:00:00 2001 From: ame Date: Fri, 8 Aug 2025 20:36:01 -0500 Subject: free cookie from header table --- src/net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index a21579c..6944805 100644 --- a/src/net.c +++ b/src/net.c @@ -721,7 +721,7 @@ void* handle_client(void *_arg){ luaI_tsetv(L, req_idx, "cookies", lcookie); parray_clear(cookie, STR); - parray_remove(table, "Cookie", NONE); + parray_remove(table, "Cookie", STR); } lua_pushlightuserdata(L, file_cont); -- cgit v1.2.3 From 5666cbf8830c8b26337eb92f4da434d0d2242dc2 Mon Sep 17 00:00:00 2001 From: ame Date: Sat, 9 Aug 2025 15:17:04 -0500 Subject: dont auto-set content-type (yet) --- src/net.c | 1 - 1 file changed, 1 deletion(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index 6944805..713e56a 100644 --- a/src/net.c +++ b/src/net.c @@ -762,7 +762,6 @@ void* handle_client(void *_arg){ lua_newtable(L); int header_idx = lua_gettop(L); luaI_tseti(L, header_idx, "Code", 200); - luaI_tsets(L, header_idx, "Content-Type", "text/html"); luaI_tsetv(L, res_idx, "header", header_idx); -- cgit v1.2.3 From 432f7792d12dadc3adb605c018176bbc7359b503 Mon Sep 17 00:00:00 2001 From: ame Date: Tue, 12 Aug 2025 21:54:57 -0500 Subject: support percent encoding --- src/net.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index 713e56a..2766781 100644 --- a/src/net.c +++ b/src/net.c @@ -678,7 +678,7 @@ void* handle_client(void *_arg){ if(val == -2) net_error(client_fd, 414); if(val >= 0){ - str* sk = (str*)parray_get(table, "Path"); + str* path = (str*)parray_get(table, "Path"); str* sR = (str*)parray_get(table, "Request"); str* sT = (str*)parray_get(table, "Content-Type"); str* sC = (str*)parray_get(table, "Cookie"); @@ -693,13 +693,22 @@ void* handle_client(void *_arg){ sprintf(portc, "%i", args->port); str* aa = str_init(portc); - str_push(aa, sk->c); + str* decoded_path; + int decoded_err = percent_decode(path, &decoded_path); + larray_t* params = NULL; + parray_t* v = NULL; - larray_t* params = larray_init(); - parray_t* v = route_match(paths, aa->c, ¶ms); - - if(sT != NULL) - rolling_file_parse(L, &files_idx, &body_idx, header + 4, sT, bite - header_eof - 4, file_cont); + if(decoded_err == 1){ + net_error(client_fd, 400); + } else { + str_push(aa, decoded_path->c); + + params = larray_init(); + v = route_match(paths, aa->c, ¶ms); + + if(sT != NULL) + rolling_file_parse(L, &files_idx, &body_idx, header + 4, sT, bite - header_eof - 4, file_cont); + } str_free(aa); if(v != NULL){ -- cgit v1.2.3 From 70dcbc67382084d924d353f9c741cd8c0e46cb0f Mon Sep 17 00:00:00 2001 From: ame Date: Tue, 12 Aug 2025 22:02:18 -0500 Subject: mem leak oops --- src/net.c | 1 + 1 file changed, 1 insertion(+) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index 2766781..d68da2c 100644 --- a/src/net.c +++ b/src/net.c @@ -710,6 +710,7 @@ void* handle_client(void *_arg){ rolling_file_parse(L, &files_idx, &body_idx, header + 4, sT, bite - header_eof - 4, file_cont); } + str_free(decoded_path); str_free(aa); if(v != NULL){ lua_newtable(L); -- cgit v1.2.3 From 6e6e948a553cc062b439f349c0b545df0223d9a1 Mon Sep 17 00:00:00 2001 From: ame Date: Tue, 2 Sep 2025 00:07:22 -0500 Subject: parse net path and query --- src/net.c | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) (limited to 'src/net.c') diff --git a/src/net.c b/src/net.c index d68da2c..49eabd7 100644 --- a/src/net.c +++ b/src/net.c @@ -2,6 +2,7 @@ #include "net/util.h" #include "net/lua.h" #include "net/luai.h" +#include "types/str.h" #include @@ -652,6 +653,50 @@ int _request(lua_State* L, struct request_state* state){ return 1; } +struct net_path_t { + str* path; + parray_t* query; +}; + +void path_parse(struct net_path_t* path, str* raw){ + path->path = str_init(""); + path->query = parray_init(); + + str* query_key = str_init(""); + str* query_value = str_init(""); + + str** reading = &path->path; + + for(int i = 0; i <= raw->len; i++){ + if(raw->len - i > 1){ + switch(raw->c[i]){ + case '&': + parray_set(path->query, query_key->c, query_value); + str_clear(query_key); + query_value = str_init(""); + //dont want to break here + case '?': + reading = &query_key; + i++; + break; + case '=': + reading = &query_value; + i++; + break; + } + } + + str_pushl(*reading, raw->c + i, 1); + } + + if(*reading == query_value){ + parray_set(path->query, query_key->c, query_value); + } else { + str_free(query_value); + } + str_free(query_key); +} + #define max_uri_size 2048 _Atomic size_t threads = 0; @@ -693,12 +738,15 @@ void* handle_client(void *_arg){ sprintf(portc, "%i", args->port); str* aa = str_init(portc); + struct net_path_t parsed_path; + path_parse(&parsed_path, path); + str* decoded_path; - int decoded_err = percent_decode(path, &decoded_path); + int decoded_err = percent_decode(parsed_path.path, &decoded_path); larray_t* params = NULL; parray_t* v = NULL; - if(decoded_err == 1){ + if(decoded_err == 1 || paths == NULL){ net_error(client_fd, 400); } else { str_push(aa, decoded_path->c); @@ -734,6 +782,16 @@ void* handle_client(void *_arg){ parray_remove(table, "Cookie", STR); } + if(parsed_path.query->len != 0){ + lua_newtable(L); + int lquery = lua_gettop(L); + for(int i = 0; i != parsed_path.query->len; i++){ + luaI_tsetsl(L, lquery, parsed_path.query->P[i].key->c, ((str*)parsed_path.query->P[i].value)->c, ((str*)parsed_path.query->P[i].value)->len); + } + + luaI_tsetv(L, req_idx, "query", lquery); + } + lua_pushlightuserdata(L, file_cont); int ld = lua_gettop(L); @@ -747,6 +805,8 @@ void* handle_client(void *_arg){ } luaI_tsets(L, req_idx, "ip", inet_ntoa(args->cli.sin_addr)); + luaI_tsets(L, req_idx, "path", parsed_path.path->c); + luaI_tsets(L, req_idx, "rawpath", path->c); if(bite == -1){ client_fd = -2; @@ -837,6 +897,9 @@ net_end: } + str_free(parsed_path.path); + parray_clear(parsed_path.query, STR); + if(file_cont->boundary != NULL) str_free(file_cont->current); if(file_cont->boundary != NULL) str_free(file_cont->boundary); if(file_cont->boundary_id != NULL) str_free(file_cont->boundary_id); -- cgit v1.2.3