Compare commits
29 Commits
feature/bo
...
main
Author | SHA1 | Date | |
---|---|---|---|
d9ee82597e | |||
28a91d9f6b | |||
5e66bc7446 | |||
9b9f31420e | |||
8564ac5240 | |||
d5d4dc6455 | |||
d24f0ae010 | |||
6ca997dafb | |||
b4b2305fde | |||
cb9f8c30c9 | |||
5a7553f902 | |||
101473dccc | |||
70579365c1 | |||
3e6dfd3d5a | |||
a30ee9d249 | |||
02d5fa4b8e | |||
db6bc13a49 | |||
8b95ade0be | |||
f9472dc426 | |||
f7183dbfac | |||
1780a67e1f | |||
eef103fd9a | |||
d3859091a2 | |||
3c6bd4dfc3 | |||
603ab98a14 | |||
2e087254c9 | |||
7e8d7e1b11 | |||
1d41948720 | |||
19fc5a2791 |
19
README.md
19
README.md
@ -1,2 +1,21 @@
|
||||
# postnummer-app-frontend
|
||||
|
||||
## usecase
|
||||
|
||||
Når man navigere Danmark er det træls at komme trælse steder hen. Derfor vil man gerne vide hvilke postnumre er bedre end andre, så man kan vurderer hvor man vil hen.
|
||||
|
||||
Kunden skal kunne skrive anmeldelser af forskellige postnumre.
|
||||
|
||||
Kunden skal kunne se andres anmeldelser af forskellige postnumre.
|
||||
|
||||
Kunden skal kunne søge efter postnumre, så kunden ikke behøver at finde dem i hånden hver gang.
|
||||
|
||||
Kunden skal nemt kunne se postnumre rundt i danmark gennem et interaktivt kort.
|
||||
|
||||
## implementeret
|
||||
|
||||
- [x] Slå postnumre op
|
||||
- [x] Vælge postnumre på interaktivt kort
|
||||
- [ ] Lave reviews
|
||||
- [ ] Læse reviews
|
||||
|
||||
|
48
api.md
Normal file
48
api.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Api specification
|
||||
|
||||
|
||||
|
||||
## Review
|
||||
|
||||
### Review model
|
||||
|
||||
```ts
|
||||
{
|
||||
id: number,
|
||||
location: string,
|
||||
title: string,
|
||||
content: string,
|
||||
stars: number
|
||||
}
|
||||
```
|
||||
|
||||
### GET reviews
|
||||
|
||||
#### Response
|
||||
|
||||
```ts
|
||||
{
|
||||
reviews: Review[]
|
||||
}
|
||||
```
|
||||
|
||||
### Post createReview
|
||||
|
||||
#### Request
|
||||
|
||||
```ts
|
||||
{
|
||||
location: string,
|
||||
title: string,
|
||||
content: string,
|
||||
stars: number
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```ts
|
||||
{
|
||||
message: "Ok" | "Invalid request"
|
||||
}
|
||||
```
|
6
backend/.clang-format
Normal file
6
backend/.clang-format
Normal file
@ -0,0 +1,6 @@
|
||||
Language: Cpp
|
||||
BasedOnStyle: WebKit
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 100
|
||||
IndentCaseLabels: true
|
||||
|
4
backend/.gitignore
vendored
Normal file
4
backend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.o
|
||||
compile_flags.txt
|
||||
server
|
||||
|
26
backend/Makefile
Normal file
26
backend/Makefile
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
CFLAGS = \
|
||||
-std=c17 \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wpedantic \
|
||||
-Wconversion \
|
||||
|
||||
CC = gcc
|
||||
|
||||
HEADERS = $(wildcard *.h)
|
||||
|
||||
all: compile_flags.txt server
|
||||
|
||||
server: main.o http.o linux.o
|
||||
$(CC) $^ -o $@
|
||||
|
||||
%.o: %.c $(HEADERS)
|
||||
$(CC) $< -c -o $@ $(CFLAGS)
|
||||
|
||||
clean:
|
||||
rm -rf *.o server client
|
||||
|
||||
compile_flags.txt:
|
||||
echo -xc $(CFLAGS) | sed 's/\s\+/\n/g' > compile_flags.txt
|
||||
|
12
backend/NMakefile
Normal file
12
backend/NMakefile
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
OBJS=main.obj windows.obj http.obj
|
||||
|
||||
all: $(OBJS)
|
||||
link /out:server.exe $(OBJS) WS2_32.lib
|
||||
|
||||
.obj:
|
||||
cl $*.c
|
||||
|
||||
clean:
|
||||
del *.obj server.exe
|
||||
|
156
backend/http.c
Normal file
156
backend/http.c
Normal file
@ -0,0 +1,156 @@
|
||||
#include "http.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char* message;
|
||||
size_t message_size;
|
||||
size_t i;
|
||||
} HttpRequestParser;
|
||||
|
||||
#define PARSER_PANIC(message) \
|
||||
(printf("error: http parser: %s\n at %s:%d in '%s()'\n", message, __FILE__, __LINE__, \
|
||||
__func__), \
|
||||
exit(1))
|
||||
|
||||
HttpMethod parse_method(HttpRequestParser* parser)
|
||||
{
|
||||
if (parser->i + 3 < parser->message_size && strncmp(&parser->message[0], "GET", 3) == 0) {
|
||||
parser->i += 3;
|
||||
return HttpMethodGet;
|
||||
} else if (parser->i + 4 < parser->message_size
|
||||
&& strncmp(&parser->message[0], "POST", 4) == 0) {
|
||||
parser->i += 4;
|
||||
return HttpMethodPost;
|
||||
} else {
|
||||
PARSER_PANIC("failed to parse http method");
|
||||
}
|
||||
}
|
||||
|
||||
void skip_char(HttpRequestParser* parser, char value)
|
||||
{
|
||||
if (parser->i >= parser->message_size)
|
||||
PARSER_PANIC("unexpected end");
|
||||
if (parser->message[parser->i] != value)
|
||||
PARSER_PANIC("unexpected character, expected ' '");
|
||||
parser->i += 1;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
size_t index, length;
|
||||
} PathSpan;
|
||||
|
||||
PathSpan parse_path(HttpRequestParser* parser)
|
||||
{
|
||||
size_t index = parser->i;
|
||||
while (parser->i < parser->message_size && parser->message[parser->i] != ' '
|
||||
&& parser->message[parser->i] != '\r') {
|
||||
parser->i += 1;
|
||||
}
|
||||
if (parser->i >= parser->message_size)
|
||||
PARSER_PANIC("unexpected end");
|
||||
else if (parser->message[parser->i] != ' ')
|
||||
PARSER_PANIC("unexpected char, expected ' '");
|
||||
return (PathSpan) {
|
||||
.index = index,
|
||||
.length = parser->i - index,
|
||||
};
|
||||
}
|
||||
|
||||
void skip_newline(HttpRequestParser* parser)
|
||||
{
|
||||
skip_char(parser, '\r');
|
||||
skip_char(parser, '\n');
|
||||
}
|
||||
|
||||
void skip_http_version_tag(HttpRequestParser* parser)
|
||||
{
|
||||
skip_char(parser, 'H');
|
||||
skip_char(parser, 'T');
|
||||
skip_char(parser, 'T');
|
||||
skip_char(parser, 'P');
|
||||
skip_char(parser, '/');
|
||||
skip_char(parser, '1');
|
||||
skip_char(parser, '.');
|
||||
skip_char(parser, '1');
|
||||
skip_newline(parser);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
size_t key_index, key_length;
|
||||
size_t value_index, value_length;
|
||||
} HeaderPair;
|
||||
|
||||
HeaderPair parse_header_pair(HttpRequestParser* parser)
|
||||
{
|
||||
size_t key_begin = parser->i;
|
||||
while (parser->i < parser->message_size && parser->message[parser->i] != ':'
|
||||
&& parser->message[parser->i] != '\r') {
|
||||
parser->i += 1;
|
||||
}
|
||||
if (parser->i >= parser->message_size)
|
||||
PARSER_PANIC("unexpected end");
|
||||
size_t key_end = parser->i;
|
||||
skip_char(parser, ':');
|
||||
skip_char(parser, ' ');
|
||||
size_t value_begin = parser->i;
|
||||
while (parser->i < parser->message_size && parser->message[parser->i] != '\r') {
|
||||
parser->i += 1;
|
||||
}
|
||||
if (parser->i >= parser->message_size)
|
||||
PARSER_PANIC("unexpected end");
|
||||
return (HeaderPair) {
|
||||
.key_index = key_begin,
|
||||
.key_length = key_end - key_begin,
|
||||
.value_index = value_begin,
|
||||
.value_length = parser->i - value_begin,
|
||||
};
|
||||
}
|
||||
|
||||
bool is_content_length_header(HttpRequestParser* parser, HeaderPair pair)
|
||||
{
|
||||
return strncmp(&parser->message[pair.key_index], "Content-Length", pair.key_length) == 0;
|
||||
}
|
||||
|
||||
size_t extract_content_length_value(HttpRequestParser* parser, HeaderPair pair)
|
||||
{
|
||||
char string_value[21] = { 0 };
|
||||
strncpy(string_value, &parser->message[pair.value_index], pair.value_length);
|
||||
int64_t int_value = atoll(string_value);
|
||||
if (int_value < 0)
|
||||
PARSER_PANIC("Content-Length < 0");
|
||||
return (size_t)int_value;
|
||||
}
|
||||
|
||||
HttpRequestHeader parse_http_request_header(const char* message, size_t message_size)
|
||||
{
|
||||
HttpRequestParser parser = (HttpRequestParser) {
|
||||
.message = message,
|
||||
.message_size = message_size,
|
||||
.i = 0,
|
||||
};
|
||||
HttpMethod method = parse_method(&parser);
|
||||
skip_char(&parser, ' ');
|
||||
PathSpan path_span = parse_path(&parser);
|
||||
skip_char(&parser, ' ');
|
||||
skip_http_version_tag(&parser);
|
||||
size_t content_length = 0;
|
||||
while (parser.i < message_size && message[parser.i] != '\r') {
|
||||
HeaderPair pair = parse_header_pair(&parser);
|
||||
if (is_content_length_header(&parser, pair)) {
|
||||
content_length = extract_content_length_value(&parser, pair);
|
||||
}
|
||||
skip_newline(&parser);
|
||||
}
|
||||
skip_newline(&parser);
|
||||
return (HttpRequestHeader) {
|
||||
.method = method,
|
||||
.path_index = path_span.index,
|
||||
.path_length = path_span.length,
|
||||
.content_length = content_length,
|
||||
.body_index = parser.i,
|
||||
};
|
||||
}
|
20
backend/http.h
Normal file
20
backend/http.h
Normal file
@ -0,0 +1,20 @@
|
||||
#ifndef HTTP_H
|
||||
#define HTTP_H
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef enum {
|
||||
HttpMethodGet,
|
||||
HttpMethodPost,
|
||||
} HttpMethod;
|
||||
|
||||
typedef struct {
|
||||
HttpMethod method;
|
||||
size_t path_index, path_length;
|
||||
size_t content_length;
|
||||
size_t body_index;
|
||||
} HttpRequestHeader;
|
||||
|
||||
HttpRequestHeader parse_http_request_header(const char* message, size_t message_size);
|
||||
|
||||
#endif
|
98
backend/linux.c
Normal file
98
backend/linux.c
Normal file
@ -0,0 +1,98 @@
|
||||
#include "tcp.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
struct TcpServer {
|
||||
int server_socket;
|
||||
struct sockaddr_in server_address;
|
||||
};
|
||||
|
||||
struct TcpConnection {
|
||||
int client_socket;
|
||||
struct sockaddr_in client_address;
|
||||
};
|
||||
|
||||
void tcp_global_initialize_sockets(void) { }
|
||||
|
||||
TcpServer* tcp_server_create(const char* ip, uint16_t port)
|
||||
{
|
||||
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (server_socket < 0) {
|
||||
printf("error: tcp: could not open socket\n");
|
||||
return NULL;
|
||||
}
|
||||
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &(int) { 1 }, sizeof(int)) < 0) {
|
||||
printf("warning: tcp: could not setsockopt SO_REUSEADDR\n");
|
||||
}
|
||||
struct sockaddr_in server_address;
|
||||
server_address.sin_addr.s_addr = inet_addr(ip);
|
||||
server_address.sin_family = AF_INET;
|
||||
server_address.sin_port = htons(port);
|
||||
if (bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
|
||||
printf("error: tcp: could not bind socket\n");
|
||||
close(server_socket);
|
||||
return NULL;
|
||||
}
|
||||
if (listen(server_socket, SOMAXCONN - 1) < 0) {
|
||||
printf("error: tcp: could not listen on server\n");
|
||||
close(server_socket);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TcpServer* self = malloc(sizeof(TcpServer));
|
||||
*self = (TcpServer) {
|
||||
.server_socket = server_socket,
|
||||
.server_address = server_address,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
void tcp_server_destroy(TcpServer* server)
|
||||
{
|
||||
close(server->server_socket);
|
||||
free(server);
|
||||
}
|
||||
|
||||
TcpConnection* tcp_server_accept(TcpServer* server)
|
||||
{
|
||||
struct sockaddr_in client_address = { 0 };
|
||||
socklen_t client_size = 0;
|
||||
int client_socket
|
||||
= accept(server->server_socket, (struct sockaddr*)&client_address, &client_size);
|
||||
if (client_socket < 0) {
|
||||
printf("error: tcp: could not accept connection\n");
|
||||
printf("errno: %d %s\n", errno, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TcpConnection* connection = malloc(sizeof(TcpConnection));
|
||||
*connection = (TcpConnection) {
|
||||
.client_socket = client_socket,
|
||||
.client_address = client_address,
|
||||
};
|
||||
return connection;
|
||||
}
|
||||
|
||||
void tcp_connection_destroy(TcpConnection* connection)
|
||||
{
|
||||
close(connection->client_socket);
|
||||
free(connection);
|
||||
}
|
||||
|
||||
ssize_t tcp_recieve(TcpConnection* connection, uint8_t* data, size_t amount)
|
||||
{
|
||||
return recv(connection->client_socket, data, amount, 0);
|
||||
}
|
||||
|
||||
ssize_t tcp_send(TcpConnection* connection, uint8_t* data, size_t amount)
|
||||
{
|
||||
return send(connection->client_socket, data, amount, 0);
|
||||
}
|
167
backend/main.c
Normal file
167
backend/main.c
Normal file
@ -0,0 +1,167 @@
|
||||
#include "http.h"
|
||||
#include "tcp.h"
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
TcpServer* server = NULL;
|
||||
|
||||
void interrupt_handler(int a)
|
||||
{
|
||||
(void)a;
|
||||
printf("\nShutting down gracefully...\n");
|
||||
if (server != NULL)
|
||||
tcp_server_destroy(server);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void handle_client_connection(TcpConnection* connection)
|
||||
{
|
||||
|
||||
uint8_t buffer[8192] = { 0 };
|
||||
ssize_t recieved = tcp_recieve(connection, buffer, 8192);
|
||||
if (recieved < 0) {
|
||||
printf("error: could not recieve\n");
|
||||
return;
|
||||
} else if (recieved == 0) {
|
||||
printf("client disconnected\n");
|
||||
return;
|
||||
}
|
||||
HttpRequestHeader header = parse_http_request_header((char*)buffer, strlen((char*)buffer));
|
||||
char* path = calloc(header.path_length + 1, sizeof(char));
|
||||
strncpy(path, (char*)&buffer[header.path_index], header.path_length);
|
||||
if (strncmp(path, "/api", 4) == 0) {
|
||||
// something something api
|
||||
} else {
|
||||
if (strstr(path, "..") != NULL) {
|
||||
uint8_t send_buffer[]
|
||||
= "HTTP/1.1 400 BAD\r\n"
|
||||
"Content-Type: text/html\r\n"
|
||||
"\r\n"
|
||||
"<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Bad "
|
||||
"request</title></head><body><h1>Fuck you!</h1></body></html>\r\n";
|
||||
ssize_t written = tcp_send(connection, send_buffer, sizeof(send_buffer));
|
||||
if (written < 0) {
|
||||
printf("error: could not write\n");
|
||||
return;
|
||||
}
|
||||
} else if (header.path_length == 0 || strncmp(path, "/", header.path_length) == 0) {
|
||||
FILE* file = fopen("../frontend/index.html", "r");
|
||||
if (file == NULL) {
|
||||
printf("error: could not open file\n");
|
||||
return;
|
||||
}
|
||||
uint8_t send_buffer[] = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/html\r\n"
|
||||
"\r\n";
|
||||
ssize_t written = tcp_send(connection, send_buffer, sizeof(send_buffer));
|
||||
if (written < 0) {
|
||||
printf("error: could not write\n");
|
||||
return;
|
||||
}
|
||||
char char_read;
|
||||
while ((char_read = (char)fgetc(file)) != EOF) {
|
||||
tcp_send(connection, (uint8_t*)&char_read, sizeof(char));
|
||||
}
|
||||
} else {
|
||||
char rootpath[] = "../frontend";
|
||||
size_t filepath_size = sizeof(rootpath) + header.path_length + 1;
|
||||
char* filepath = calloc(filepath_size, sizeof(char));
|
||||
snprintf(filepath, filepath_size, "%s%s", rootpath, path);
|
||||
|
||||
char* dot = strrchr(path, '.');
|
||||
char mime_type[20] = { 0 };
|
||||
char file_flag[3] = { 'r', 0 };
|
||||
if (dot != NULL && strncmp(dot, ".html", 5) == 0) {
|
||||
snprintf(mime_type, 20, "text/html");
|
||||
} else if (dot != NULL && strncmp(dot, ".css", 4) == 0) {
|
||||
snprintf(mime_type, 20, "text/css");
|
||||
} else if (dot != NULL && strncmp(dot, ".js", 3) == 0) {
|
||||
snprintf(mime_type, 20, "text/javascript");
|
||||
} else if (dot != NULL && strncmp(dot, ".map", 4) == 0) {
|
||||
snprintf(mime_type, 20, "application/json");
|
||||
} else if (dot != NULL && strncmp(dot, ".ico", 4) == 0) {
|
||||
snprintf(mime_type, 20, "image/x-icon");
|
||||
} else if (dot != NULL && strncmp(dot, ".jpg", 4) == 0) {
|
||||
snprintf(mime_type, 20, "image/jpeg");
|
||||
file_flag[1] = 'b';
|
||||
} else if (dot != NULL && strncmp(dot, ".png", 4) == 0) {
|
||||
snprintf(mime_type, 20, "image/png");
|
||||
file_flag[1] = 'b';
|
||||
} else if (dot != NULL && strncmp(dot, ".woff2", 6) == 0) {
|
||||
snprintf(mime_type, 20, "font/woff2");
|
||||
file_flag[1] = 'b';
|
||||
} else {
|
||||
printf("error: unknown file type\n");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE* file = fopen(filepath, file_flag);
|
||||
if (file == NULL) {
|
||||
printf("error: could not open file\n");
|
||||
return;
|
||||
}
|
||||
|
||||
char send_buffer_1[] = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: ";
|
||||
char send_buffer_2[] = "\r\n"
|
||||
"\r\n";
|
||||
ssize_t written = tcp_send(connection, (uint8_t*)send_buffer_1, strlen(send_buffer_1));
|
||||
if (written < 0) {
|
||||
printf("error: could not write\n");
|
||||
return;
|
||||
}
|
||||
written = tcp_send(connection, (uint8_t*)mime_type, strlen(mime_type));
|
||||
if (written < 0) {
|
||||
printf("error: could not write\n");
|
||||
return;
|
||||
}
|
||||
written = tcp_send(connection, (uint8_t*)send_buffer_2, strlen(send_buffer_2));
|
||||
if (written < 0) {
|
||||
printf("error: could not write\n");
|
||||
return;
|
||||
}
|
||||
int char_read;
|
||||
while ((char_read = fgetc(file)) != EOF) {
|
||||
tcp_send(connection, (uint8_t*)&char_read, sizeof(char));
|
||||
}
|
||||
|
||||
free(filepath);
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
free(path);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
tcp_global_initialize_sockets();
|
||||
|
||||
signal(SIGINT, &interrupt_handler);
|
||||
|
||||
const uint16_t port = 8000;
|
||||
|
||||
printf("starting server...\n");
|
||||
server = tcp_server_create("127.0.0.1", port);
|
||||
if (server == NULL)
|
||||
return 1;
|
||||
printf("listening on port %d\n", port);
|
||||
|
||||
while (true) {
|
||||
printf("waiting for client...\n");
|
||||
TcpConnection* connection = tcp_server_accept(server);
|
||||
if (connection == NULL) {
|
||||
printf("error: could not accept client\n");
|
||||
continue;
|
||||
}
|
||||
printf("client connected\n");
|
||||
handle_client_connection(connection);
|
||||
printf("disconnecting client\n");
|
||||
tcp_connection_destroy(connection);
|
||||
}
|
||||
tcp_server_destroy(server);
|
||||
return 0;
|
||||
}
|
32
backend/tcp.h
Normal file
32
backend/tcp.h
Normal file
@ -0,0 +1,32 @@
|
||||
#ifndef TCP_H
|
||||
#define TCP_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <BaseTsd.h>
|
||||
typedef SSIZE_T ssize_t;
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
void tcp_global_initialize_sockets(void);
|
||||
|
||||
typedef struct TcpServer TcpServer;
|
||||
typedef struct TcpConnection TcpConnection;
|
||||
|
||||
// returns NULL on errors, and prints the error
|
||||
TcpServer* tcp_server_create(const char* ip, uint16_t port);
|
||||
void tcp_server_destroy(TcpServer* server);
|
||||
|
||||
// returns NULL on errors, and prints the error
|
||||
TcpConnection* tcp_server_accept(TcpServer* server);
|
||||
void tcp_connection_destroy(TcpConnection* connection);
|
||||
|
||||
// returns amount transmittet >0 on success, ==0 if client was disconnected, and <0 on error
|
||||
ssize_t tcp_recieve(TcpConnection* connection, uint8_t* data, size_t amount);
|
||||
// returns amount transmittet >0 on success, ==0 if client was disconnected, and <0 on error
|
||||
ssize_t tcp_send(TcpConnection* connection, uint8_t* data, size_t amount);
|
||||
|
||||
#endif
|
4
backend/utils.h
Normal file
4
backend/utils.h
Normal file
@ -0,0 +1,4 @@
|
||||
#ifndef UTILS_H
|
||||
#define UTILS_H
|
||||
|
||||
#endif
|
95
backend/windows.c
Normal file
95
backend/windows.c
Normal file
@ -0,0 +1,95 @@
|
||||
#include "tcp.h"
|
||||
#include <BaseTsd.h>
|
||||
#include <WS2tcpip.h>
|
||||
#include <WinSock2.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct TcpServer {
|
||||
int server_socket;
|
||||
struct sockaddr_in server_address;
|
||||
};
|
||||
|
||||
struct TcpConnection {
|
||||
int client_socket;
|
||||
struct sockaddr_in client_address;
|
||||
};
|
||||
|
||||
void tcp_global_initialize_sockets(void)
|
||||
{
|
||||
WSADATA data;
|
||||
WSAStartup(0x0202, &data);
|
||||
}
|
||||
|
||||
TcpServer* tcp_server_create(const char* ip, uint16_t port)
|
||||
{
|
||||
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (server_socket < 0) {
|
||||
printf("error: tcp: could not open socket\n");
|
||||
return NULL;
|
||||
}
|
||||
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &(int) { 1 }, sizeof(int)) < 0) {
|
||||
printf("warning: tcp: could not setsockopt SO_REUSEADDR\n");
|
||||
}
|
||||
struct sockaddr_in server_address;
|
||||
server_address.sin_addr.s_addr = inet_addr(ip);
|
||||
server_address.sin_family = AF_INET;
|
||||
server_address.sin_port = htons(port);
|
||||
if (bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
|
||||
printf("error: tcp: could not bind socket\n");
|
||||
return NULL;
|
||||
}
|
||||
if (listen(server_socket, SOMAXCONN) < 0) {
|
||||
printf("error: tcp: could not listen on server\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TcpServer* self = malloc(sizeof(TcpServer));
|
||||
*self = (TcpServer) {
|
||||
.server_socket = server_socket,
|
||||
.server_address = server_address,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
void tcp_server_destroy(TcpServer* server)
|
||||
{
|
||||
closesocket(server->server_socket);
|
||||
free(server);
|
||||
}
|
||||
|
||||
TcpConnection* tcp_server_accept(TcpServer* server)
|
||||
{
|
||||
struct sockaddr_in client_address;
|
||||
socklen_t client_size;
|
||||
int client_socket
|
||||
= accept(server->server_socket, (struct sockaddr*)&client_address, &client_size);
|
||||
if (client_socket < 0) {
|
||||
printf("error: tcp: could not accept connection\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TcpConnection* connection = malloc(sizeof(TcpConnection));
|
||||
*connection = (TcpConnection) {
|
||||
.client_socket = client_socket,
|
||||
.client_address = client_address,
|
||||
};
|
||||
return connection;
|
||||
}
|
||||
|
||||
void tcp_connection_destroy(TcpConnection* connection)
|
||||
{
|
||||
closesocket(connection->client_socket);
|
||||
free(connection);
|
||||
}
|
||||
|
||||
ssize_t tcp_recieve(TcpConnection* connection, uint8_t* data, size_t amount)
|
||||
{
|
||||
return recv(connection->client_socket, data, amount, 0);
|
||||
}
|
||||
|
||||
ssize_t tcp_send(TcpConnection* connection, uint8_t* data, size_t amount)
|
||||
{
|
||||
return send(connection->client_socket, data, amount, 0);
|
||||
}
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@ -8,6 +8,8 @@
|
||||
<script src="bundle.js" defer></script>
|
||||
<title>Postnummer App</title>
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
|
||||
@ -18,11 +20,11 @@
|
||||
<div class="spacer"></div>
|
||||
<button id="dropdown-button">≡</button>
|
||||
<div id="dropdown">
|
||||
<a href="https://tpho.dk">based site</a>
|
||||
<a href="https://tpho.dk">based site</a>
|
||||
<button id="map-redirect">Kort</button>
|
||||
<button id="reviews-redirect">Anmeldelser</button>
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<main id="main">
|
||||
<form id="search-bar">
|
||||
<input id="search-input" type="text" placeholder="Postnummer" maxlength="4">
|
||||
<button id="search-button" type="submit">Search</button>
|
||||
|
@ -1,17 +1,23 @@
|
||||
export class Throttler {
|
||||
private hasBeenCalledWithinTime = false;
|
||||
private lastCallFunc: (() => any) | null = null;
|
||||
private lastCallFunc: (() => Promise<any>) | null = null;
|
||||
private timeout: number | null = null;
|
||||
|
||||
public constructor(private minimumTimeBetweenCall: number) {}
|
||||
|
||||
public call(func: () => any) {
|
||||
public async call(func: () => any) {
|
||||
this.lastCallFunc = func;
|
||||
if (this.hasBeenCalledWithinTime) return;
|
||||
this.hasBeenCalledWithinTime = true;
|
||||
func();
|
||||
setTimeout(() => {
|
||||
this.hasBeenCalledWithinTime = false;
|
||||
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
if (this.lastCallFunc) this.lastCallFunc();
|
||||
}, this.minimumTimeBetweenCall);
|
||||
|
||||
if (this.hasBeenCalledWithinTime) return;
|
||||
|
||||
this.hasBeenCalledWithinTime = true;
|
||||
await func();
|
||||
|
||||
setTimeout(() => this.hasBeenCalledWithinTime = false, this.minimumTimeBetweenCall);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export class Tooltip {
|
||||
document.body.addEventListener("mousemove", (event: MouseEvent) => {
|
||||
this.element.style.opacity = "1";
|
||||
this.element.style.left = event.x + OFFSET + "px";
|
||||
this.element.style.top = event.y + OFFSET + "px";
|
||||
this.element.style.top = event.y + OFFSET + document.documentElement.scrollTop + "px";
|
||||
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
|
@ -3,6 +3,9 @@ import { setTopbarOffset, addToggleDropdownListener } from "./topbar";
|
||||
import { Coordinate, Position, convertPixelsToCoordinate, convertCoordinateToPixels } from "./coordinates";
|
||||
import { Size } from "./size";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { loadReviews } from "./review";
|
||||
|
||||
const domSelect = <E extends Element>(query: string) => document.querySelector<E>(query);
|
||||
|
||||
const tooltip = new Tooltip(document.getElementById("tooltip")!);
|
||||
|
||||
@ -38,6 +41,28 @@ async function fetchZipCode({
|
||||
.catch(() => null as never);
|
||||
}
|
||||
|
||||
let currentBoundary: Array<number> | null = null;
|
||||
async function fetchAndDisplayZipCode(coords: Coordinate) {
|
||||
if (currentBoundary &&
|
||||
coords.longitude > currentBoundary[0] &&
|
||||
coords.latitude > currentBoundary[1] &&
|
||||
coords.longitude < currentBoundary[2] &&
|
||||
coords.latitude < currentBoundary[3]
|
||||
) return;
|
||||
|
||||
const response = await fetchZipCode(coords);
|
||||
|
||||
currentBoundary = response.bbox;
|
||||
|
||||
displayZipCode(
|
||||
domSelect<HTMLParagraphElement>("#zip-code")!,
|
||||
response.nr,
|
||||
response.navn,
|
||||
response.visueltcenter ? { longitude: response.visueltcenter[0], latitude: response.visueltcenter[1] } : null,
|
||||
response.bbox ? { x1: response.bbox[0], y1: response.bbox[1], x2: response.bbox[2], y2: response.bbox[3] } : null,
|
||||
);
|
||||
}
|
||||
|
||||
function displayMousePosition(element: HTMLParagraphElement, mouse: Position) {
|
||||
element.innerHTML = `Mouse position: <code>(${mouse.x}px, ${mouse.y}px)</code>`;
|
||||
}
|
||||
@ -63,9 +88,11 @@ function displayZipCode(
|
||||
tooltip.setText(zipCode ? `<code>${zipCode}</code> ${name}` : "");
|
||||
|
||||
const dot = document.getElementById("dot")!;
|
||||
const boundaryElem = document.getElementById("boundary")!;
|
||||
|
||||
if (!center) {
|
||||
if (!center || !boundary) {
|
||||
dot.style.display = "none";
|
||||
boundaryElem.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
@ -79,20 +106,18 @@ function displayZipCode(
|
||||
const position = convertCoordinateToPixels(center, mapSize);
|
||||
const rect = document.getElementById("map")!.getBoundingClientRect();
|
||||
dot.style.display = "block";
|
||||
dot.style.left = position.x + rect.left + "px";
|
||||
dot.style.top = position.y + rect.top + document.documentElement.scrollTop + "px";
|
||||
dot.style.left = `${position.x + rect.left}px`;
|
||||
dot.style.top = `${position.y + rect.top + document.documentElement.scrollTop}px`;
|
||||
|
||||
// Draw boundary
|
||||
if (!boundary) return;
|
||||
|
||||
const bottomleft = convertCoordinateToPixels({ longitude: boundary.x1, latitude: boundary.y1 }, mapSize);
|
||||
const topright = convertCoordinateToPixels({ longitude: boundary.x2, latitude: boundary.y2 }, mapSize);
|
||||
const topright = convertCoordinateToPixels({ longitude: boundary.x2, latitude: boundary.y2 }, mapSize);
|
||||
|
||||
const boundaryElem = document.getElementById("boundary")!;
|
||||
boundaryElem.style.left = bottomleft.x + rect.left + "px";
|
||||
boundaryElem.style.top = topright.y + rect.top + document.documentElement.scrollTop + "px";
|
||||
boundaryElem.style.width = topright.x - bottomleft.x + "px";
|
||||
boundaryElem.style.height = bottomleft.y - topright.y + document.documentElement.scrollTop + "px";
|
||||
boundaryElem.style.display = "block";
|
||||
boundaryElem.style.left = `${bottomleft.x + rect.left}px`;
|
||||
boundaryElem.style.top = `${topright.y + rect.top + document.documentElement.scrollTop}px`;
|
||||
boundaryElem.style.width = `${topright.x - bottomleft.x}px`;
|
||||
boundaryElem.style.height = `${bottomleft.y - topright.y}px`;
|
||||
}
|
||||
|
||||
function setupMap(
|
||||
@ -100,82 +125,63 @@ function setupMap(
|
||||
coordsElement: HTMLParagraphElement,
|
||||
zipCodeElement: HTMLParagraphElement,
|
||||
) {
|
||||
const mapImg = document.querySelector<HTMLImageElement>("#map")!;
|
||||
const mapImg = domSelect<HTMLImageElement>("#map")!;
|
||||
const fetcher = new Throttler(200);
|
||||
|
||||
mapImg.onmousemove = async (event: MouseEvent) => {
|
||||
mapImg.addEventListener('mousemove', async (event: MouseEvent) => {
|
||||
const mousePosition: Position = { x: event.offsetX, y: event.offsetY };
|
||||
displayMousePosition(mousePositionElement, mousePosition);
|
||||
|
||||
const mapSize: Size = {
|
||||
width: mapImg.clientWidth,
|
||||
height: mapImg.clientHeight,
|
||||
};
|
||||
|
||||
const coords = convertPixelsToCoordinate(mousePosition, mapSize);
|
||||
displayCoords(coordsElement, coords);
|
||||
fetcher.call(async () => {
|
||||
const response = await fetchZipCode(coords);
|
||||
displayZipCode(
|
||||
zipCodeElement,
|
||||
response.nr,
|
||||
response.navn,
|
||||
response.visueltcenter ? { longitude: response.visueltcenter[0], latitude: response.visueltcenter[1] } : null,
|
||||
response.bbox ? { x1: response.bbox[0], y1: response.bbox[1], x2: response.bbox[2], y2: response.bbox[3] } : null,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
mapImg.onmouseup = async (event: MouseEvent) => {
|
||||
fetcher.call(async () => await fetchAndDisplayZipCode(coords));
|
||||
});
|
||||
|
||||
mapImg.addEventListener("mouseup", async (event: MouseEvent) => {
|
||||
const mousePosition: Position = { x: event.offsetX, y: event.offsetY };
|
||||
displayMousePosition(mousePositionElement, mousePosition);
|
||||
|
||||
const mapSize: Size = {
|
||||
width: mapImg.clientWidth,
|
||||
height: mapImg.clientHeight,
|
||||
};
|
||||
|
||||
const coords = convertPixelsToCoordinate(mousePosition, mapSize);
|
||||
displayCoords(coordsElement, coords);
|
||||
fetcher.call(async () => {
|
||||
const response = await fetchZipCode(coords);
|
||||
displayZipCode(
|
||||
zipCodeElement,
|
||||
response.nr,
|
||||
response.navn,
|
||||
response.visueltcenter ? { longitude: response.visueltcenter[0], latitude: response.visueltcenter[1] } : null,
|
||||
response.bbox ? { x1: response.bbox[0], y1: response.bbox[1], x2: response.bbox[2], y2: response.bbox[3] } : null,
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
fetcher.call(async () => await fetchAndDisplayZipCode(coords));
|
||||
});
|
||||
|
||||
mapImg.onmouseleave = (_event: MouseEvent) => {
|
||||
document.getElementById("dot")!.style.display = "none";
|
||||
[mousePositionElement, coordsElement].forEach(
|
||||
(e) => (e.innerHTML = ""),
|
||||
);
|
||||
zipCodeElement.innerHTML = "Postnummer ikke fundet";
|
||||
};
|
||||
mapImg.addEventListener("mouseleave", (_event: MouseEvent) => {
|
||||
displayZipCode(zipCodeElement, null, null, null, null);
|
||||
});
|
||||
}
|
||||
|
||||
function setupSearchBar(zipCodeElement: HTMLParagraphElement) {
|
||||
const searchBar =
|
||||
document.querySelector<HTMLFormElement>("#search-bar")!;
|
||||
domSelect<HTMLFormElement>("#search-bar")!;
|
||||
const searchInput =
|
||||
document.querySelector<HTMLInputElement>("#search-input")!;
|
||||
domSelect<HTMLInputElement>("#search-input")!;
|
||||
|
||||
// Prevent typing letters
|
||||
searchBar.onkeypress = event => {
|
||||
event.key !== "Enter" || !isNaN(parseInt(event.key));
|
||||
}
|
||||
searchInput.addEventListener("keydown", (event) => {
|
||||
if (event.key.length === 1 && !"0123456789".includes(event.key))
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
searchBar.addEventListener("submit", async (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const inputValue = searchInput.value;
|
||||
if (!/^\d+$/.test(inputValue)) return;
|
||||
const data = await (
|
||||
await fetch(
|
||||
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
|
||||
)
|
||||
).json();
|
||||
const data = await fetch(
|
||||
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
|
||||
).then((response) => response.json())
|
||||
|
||||
displayZipCode(
|
||||
zipCodeElement,
|
||||
@ -188,21 +194,64 @@ function setupSearchBar(zipCodeElement: HTMLParagraphElement) {
|
||||
});
|
||||
}
|
||||
|
||||
function pageRedirects() {
|
||||
const reviewRedirect = document.getElementById("reviews-redirect")!
|
||||
const mapRedirect = document.getElementById("map-redirect")!
|
||||
const mainElement = document.getElementById("main")!
|
||||
|
||||
|
||||
reviewRedirect.addEventListener("click", () => {
|
||||
mainElement.innerHTML = /*html*/`
|
||||
<h2 id="reviews-title">Anmeldelser</h2>
|
||||
<div id="reviews-container">${loadReviews()}</div>
|
||||
`;
|
||||
const dropdown = document.getElementById("dropdown")!;
|
||||
dropdown.classList.remove("enabled");
|
||||
|
||||
});
|
||||
|
||||
mapRedirect.addEventListener("click", () => {
|
||||
mainElement.innerHTML = /*html*/`
|
||||
<form id="search-bar">
|
||||
<input id="search-input" type="text" placeholder="Postnummer" maxlength="4">
|
||||
<button id="search-button" type="submit">Search</button>
|
||||
</form>
|
||||
<img src="assets/map.jpg" id="map">
|
||||
<div id="dot"></div>
|
||||
<div id="boundary"></div>
|
||||
<div id="info">
|
||||
<p id="zip-code">Postnummer ikke fundet</p>
|
||||
<p id="mouse-position"></p>
|
||||
<p id="coords"></p>
|
||||
</div>
|
||||
`;
|
||||
const [mousePositionElement, coordsElement, zipCodeElement] = [
|
||||
"#mouse-position",
|
||||
"#coords",
|
||||
"#zip-code",
|
||||
].map((id) => domSelect<HTMLParagraphElement>(id)!);
|
||||
setupSearchBar(zipCodeElement);
|
||||
setupMap(mousePositionElement, coordsElement, zipCodeElement);
|
||||
const dropdown = document.getElementById("dropdown")!;
|
||||
dropdown.classList.remove("enabled");
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (navigator.userAgent.match("Chrome")) {
|
||||
location.href = "https://mozilla.org/firefox";
|
||||
}
|
||||
|
||||
const [mousePositionElement, coordsElement, zipCodeElement] = [
|
||||
"#mouse-position",
|
||||
"#coords",
|
||||
"#zip-code",
|
||||
].map((id) => document.querySelector<HTMLParagraphElement>(id)!);
|
||||
const mousePositionElement = domSelect<HTMLParagraphElement>("#mouse-position")!;
|
||||
const coordsElement = domSelect<HTMLParagraphElement>("#coords")!;
|
||||
const zipCodeElement = domSelect<HTMLParagraphElement>("#zip-code")!;
|
||||
|
||||
setupSearchBar(zipCodeElement);
|
||||
setupMap(mousePositionElement, coordsElement, zipCodeElement);
|
||||
setTopbarOffset();
|
||||
addToggleDropdownListener();
|
||||
pageRedirects();
|
||||
}
|
||||
|
||||
main();
|
||||
|
28
frontend/src/review.ts
Normal file
28
frontend/src/review.ts
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
export function loadReviews() {
|
||||
let result: string = ""
|
||||
|
||||
result += `
|
||||
<div class="review">
|
||||
<h3>Hvor er min scooter?</h3>
|
||||
<div class="location-and-stars">
|
||||
<p>Randers</p>
|
||||
<div>
|
||||
<span class="material-symbols-outlined">star</span>
|
||||
<span class="material-symbols-outlined">star</span>
|
||||
<span class="material-symbols-outlined">star</span>
|
||||
<span class="material-symbols-outlined">star</span>
|
||||
<span class="material-symbols-outlined">star</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="review-content">lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet</p>
|
||||
</div>`
|
||||
|
||||
// const body = (await (await fetch("/api/review")).json()).body()
|
||||
|
||||
// if (!body.reviews) return;
|
||||
|
||||
return result
|
||||
|
||||
}
|
||||
|
@ -89,20 +89,22 @@ body {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
#dropdown a {
|
||||
#dropdown button {
|
||||
border: none;
|
||||
background-color: var(--brand);
|
||||
color: var(--light);
|
||||
padding: 1rem 1rem;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#dropdown a:hover, #dropdown a:focus {
|
||||
#dropdown button:hover, #dropdown button:focus {
|
||||
background-color: var(--brand-300);
|
||||
}
|
||||
|
||||
#dropdown a:last-child {
|
||||
#dropdown button:last-child {
|
||||
border-radius: 0 0 0 5px;
|
||||
}
|
||||
|
||||
@ -192,11 +194,12 @@ code {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#tooltip {
|
||||
display: none;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 5px;
|
||||
@ -219,3 +222,23 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
#reviews-title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.review h3, p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.location-and-stars {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
gap: 0.4em;
|
||||
flex-direction: row;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
color: yellow;
|
||||
fill: yellow;
|
||||
}
|
1
rs-backend/.gitignore
vendored
Normal file
1
rs-backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target/
|
7
rs-backend/Cargo.lock
generated
Normal file
7
rs-backend/Cargo.lock
generated
Normal file
@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "rs-backend"
|
||||
version = "0.1.0"
|
8
rs-backend/Cargo.toml
Normal file
8
rs-backend/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "rs-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
39
rs-backend/src/main.rs
Normal file
39
rs-backend/src/main.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
mod parse;
|
||||
|
||||
fn parse_client(stream_buffer: &mut [u8]) -> parse::Result<()> {
|
||||
let start = parse::http_start(stream_buffer)?;
|
||||
loop {
|
||||
match parse::http_header(stream_buffer, start.total_length) {
|
||||
Ok(header) => {
|
||||
todo!();
|
||||
}
|
||||
Err(err) => match err {
|
||||
parse::Error::InvalidHeader => {
|
||||
break;
|
||||
}
|
||||
err => return Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_client(mut stream: TcpStream) -> parse::Result<()> {
|
||||
let mut stream_buffer = Vec::from([0u8; 4096]);
|
||||
let bytes_read = stream.read(&mut stream_buffer)?;
|
||||
stream_buffer.truncate(bytes_read);
|
||||
parse_client(&mut stream_buffer)
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let listener = TcpListener::bind("127.0.0.1:8080")?;
|
||||
for stream in listener.incoming() {
|
||||
if let Err(err) = handle_client(stream?) {
|
||||
println!("error occurred: {err:#?}");
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
136
rs-backend/src/parse/client.rs
Normal file
136
rs-backend/src/parse/client.rs
Normal file
@ -0,0 +1,136 @@
|
||||
fn read_until_whitespace(
|
||||
stream_buffer: &mut [u8],
|
||||
stream_initial_index: usize,
|
||||
) -> super::Result<String> {
|
||||
let buffer = String::from_utf8(
|
||||
(stream_initial_index..stream_buffer.len())
|
||||
.map_while(|stream_index| {
|
||||
let byte = stream_buffer[stream_index];
|
||||
if byte == b' ' || byte == b'\r' && stream_buffer[stream_index + 1] == b'\n' {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(byte)
|
||||
})
|
||||
.collect(),
|
||||
)?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub struct HttpStart {
|
||||
pub method: String,
|
||||
pub path: String,
|
||||
pub version: String,
|
||||
pub total_length: usize,
|
||||
}
|
||||
|
||||
pub fn http_start(stream_buffer: &mut [u8]) -> super::Result<HttpStart> {
|
||||
let mut total_length = 0;
|
||||
let method = read_until_whitespace(stream_buffer, total_length)?;
|
||||
total_length += method.len() + 1;
|
||||
let path = read_until_whitespace(stream_buffer, total_length)?;
|
||||
total_length += path.len() + 1;
|
||||
let version = read_until_whitespace(stream_buffer, total_length)?;
|
||||
total_length += version.len() + 2; // CRLF
|
||||
Ok(HttpStart {
|
||||
method,
|
||||
path,
|
||||
version,
|
||||
total_length,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct HttpHeader {
|
||||
pub key: String,
|
||||
pub content: String,
|
||||
pub total_length: usize,
|
||||
}
|
||||
|
||||
pub fn http_header(
|
||||
stream_buffer: &mut [u8],
|
||||
stream_initial_index: usize,
|
||||
) -> super::Result<HttpHeader> {
|
||||
if read_until_whitespace(stream_buffer, stream_initial_index)?.is_empty() {
|
||||
return Err(super::Error::InvalidHeader);
|
||||
}
|
||||
|
||||
let key = String::from_utf8(
|
||||
(stream_initial_index..stream_buffer.len())
|
||||
.map_while(|stream_index| {
|
||||
let byte = stream_buffer[stream_index];
|
||||
if byte == b':' {
|
||||
None
|
||||
} else {
|
||||
Some(byte)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)?;
|
||||
|
||||
let content = String::from_utf8(
|
||||
(stream_initial_index + key.len() + 2..stream_buffer.len())
|
||||
.map_while(|stream_index| {
|
||||
let byte = stream_buffer[stream_index];
|
||||
if byte == b'\r' && stream_buffer[stream_index + 1] == b'\n' {
|
||||
None
|
||||
} else {
|
||||
Some(byte)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)?;
|
||||
|
||||
let total_length = key.len() + content.len() + 4; // ': ' + '\r\n'
|
||||
|
||||
Ok(HttpHeader {
|
||||
key,
|
||||
content,
|
||||
total_length,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn valid_http_header() {
|
||||
let mut buffer = b"Content-Type: application/json\r\n".to_owned();
|
||||
let len = buffer.len();
|
||||
let header = http_header(&mut buffer, 0).expect("should not fail with valid input");
|
||||
assert_eq!(header.total_length, len, "total length should be {len}");
|
||||
assert_eq!(
|
||||
&header.content, "application/json",
|
||||
"header content should be application/json"
|
||||
);
|
||||
assert_eq!(
|
||||
&header.key, "Content-Type",
|
||||
"header key should be Content-Type"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_http_header() {
|
||||
let mut buffer = b"\r\n".to_owned();
|
||||
let result = http_header(&mut buffer, 0)
|
||||
.err()
|
||||
.expect("should fail with invalid input");
|
||||
assert_eq!(
|
||||
result,
|
||||
crate::parse::Error::InvalidHeader,
|
||||
"should return ParseError::InvalidHeader on invalid header"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_http_start() {
|
||||
let mut buffer = b"GET /resource HTTP/1.1\r\n".to_owned();
|
||||
let len = buffer.len();
|
||||
let start = http_start(&mut buffer).expect("should not fail with valid input");
|
||||
assert_eq!(start.total_length, len, "total length should be {len}");
|
||||
assert_eq!(&start.method, "GET", "method should be GET");
|
||||
assert_eq!(&start.path, "/resource", "path should be /resource");
|
||||
assert_eq!(&start.version, "HTTP/1.1", "version should be HTTP/1.1");
|
||||
}
|
||||
}
|
20
rs-backend/src/parse/error.rs
Normal file
20
rs-backend/src/parse/error.rs
Normal file
@ -0,0 +1,20 @@
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
InvalidHeader,
|
||||
InvalidString,
|
||||
Io,
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(_: std::string::FromUtf8Error) -> Self {
|
||||
Error::InvalidString
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(_: std::io::Error) -> Self {
|
||||
Error::Io
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
4
rs-backend/src/parse/mod.rs
Normal file
4
rs-backend/src/parse/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
pub use client::*;
|
||||
pub use error::*;
|
Loading…
Reference in New Issue
Block a user