commit 6d9d52e31cdb2228542cf98e4d215cfec96c2291 Author: xamidev Date: Sat Jul 5 16:25:01 2025 +0200 done diff --git a/README.md b/README.md new file mode 100644 index 0000000..9cc496d --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# webserver + +Written as a tiny project to learn more about 64-bit x86 assembly and the Linux kernel ABI. The syntax here is Intel for GNU AS. The code is public domain. + +### Build process + +``` +as -g server.s -o server.o && ld server.o -o exe +sudo ./exe +``` + +### Testing + +``` +nc localhost 80 +GET /anything HTTP/1.0 +POST /anything HTTP/1.0 +``` diff --git a/server.s b/server.s new file mode 100644 index 0000000..62f4782 --- /dev/null +++ b/server.s @@ -0,0 +1,314 @@ +# HTTP GET/POST concurrent webserver targeting Linux ABI for x86_64 processors +# This code is public domain +# Author: xamidev + +.intel_syntax noprefix +.global _start + +.equ AF_INET, 2 +.equ SOCK_STREAM, 1 +.equ NULL, 0 +.equ O_RDONLY, 0 +.equ O_WRONLY, 1 +.equ O_CREAT, 64 + +.section .text + +_start: + # write(unsigned int fd, char* buf, size_t count) + mov rax, 1 + mov rdi, 1 + lea rsi, [rip + str_welcome] + mov rdx, 24 + syscall + + # socket(AF_INET, SOCK_STREAM, NULL) + mov rax, 41 + mov rdi, AF_INET + mov rsi, SOCK_STREAM + mov rdx, NULL + syscall + + # Save sockfd somewhere + mov r8, rax + + # bind(int sockfd, struct sockaddr_in*, int addrlen) + mov rax, 49 + mov rdi, r8 + lea rsi, [rip + sockaddr_in] + mov rdx, 16 + syscall + + # listen(int sockfd, int backlog) + mov rax, 50 + mov rdi, r8 + mov rsi, 0 + syscall + + .accept_incoming_connections: + # accept(int sockfd, struct sockaddr_in*, int* addrlen) + mov rax, 43 + mov rdi, r8 + mov rsi, NULL + mov rdx, NULL + syscall + + # Save sockfd to close it later + mov r14, rax + + # fork() -> new child process for incoming connection; PID in rax + mov rax, 57 + syscall + + # If return value is zero, we're in the child process: we can process the connection + # If return value is non-zero, we're in the parent process: we have to get back to accepting incoming connections + + test rax, rax + jz .continue + jmp .close_actual_sockfd + + .close_actual_sockfd: + # close(unsigned int fd) + mov rax, 3 + mov rdi, r14 + syscall + jmp .accept_incoming_connections + + .continue: + # close(unsigned int fd) + mov rax, 3 + mov rdi, r8 + syscall + + # read(unsigned int fd, char* buf, size_t count) + mov rax, 0 + mov rdi, r14 + lea rsi, [rip + request_buf] + mov rdx, 1024 + syscall + + # Save read bytes (full request size) + mov r13, rax + + .parse_http_path: + # Parse the HTTP request + # When one space is found, extract bytes until next space + lea rsi, [rip + request_buf] + xor rcx, rcx + .find_start: + mov al, byte [rsi + rcx] + cmp al, ' ' + je .path_start + inc rcx + jmp .find_start + + .path_start: + inc rcx + lea rdi, [rsi+rcx] + xor rdx, rdx + .path_loop: + mov al, byte [rdi + rdx] + cmp al, ' ' + je .path_done + mov byte [path_buf + rdx], al + inc rdx + jmp .path_loop + .path_done: + + # Detect if request is GET or POST (first char is G: GET, else POST) + .parse_get_post: + lea rsi, [rip + request_buf] + mov al, byte [rsi] + cmp al, 69 # 'E' (2nd letter, rsi+1) + je .get_request + jmp .post_request + + .post_request: + # open(const char* filename, int flags, int mode) + mov rax, 2 + lea rdi, [path_buf+1] + mov rsi, O_WRONLY | O_CREAT # O_RDONLY for GET requests + mov rdx, 0x1FF # All permissions + syscall + + # Save fd + mov r10, rax + + .parse_request_content: + # When we find \r\n\r\n we know that the content follows. + # request_buf already contains full request. + lea rsi, [rip + request_buf] + xor rcx, rcx + + .find_body: + mov al, byte [rsi+rcx] + cmp al, 13 # \r + jne .next + mov al, byte [rsi+rcx+1] + cmp al, 10 # \n + jne .next + mov al, byte [rsi+rcx+2] + cmp al, 13 + jne .next + mov al, byte[rsi+rcx+3] + cmp al, 10 + jne .next + + add rcx, 4 + jmp .body_found + + .next: + # Threshold to avoid infinite looping if request is malformed + inc rcx + cmp rcx, 512 + jl .find_body + + jmp .error_exit + + .body_found: + lea rdi, [rsi+rcx] + lea rsi, [rip+request_content_buf] + mov rbx, r13 + sub rbx, rcx + mov r13, rbx + + # Byte amount of request content is now in r13 + # Now we copy this to a buffer + xor rcx, rcx + + .content_copy_loop: + cmp rcx, r13 + jge .done_copy + + mov al, byte [rdi+rcx] + mov byte [rsi+rcx], al + inc rcx + jmp .content_copy_loop + .done_copy: + dec r13 + # Byte length in r13 (decrement cause of null-byte) + # Content in request_content_buf + + # Here, as we have a POST request, we will have to write to filename instead of reading from it. + + # write(unsigned int fd, const char* buf, size_t count) + mov rax, 1 + mov rdi, r10 + lea rsi, [rip + request_content_buf+1] # ADDRESS OF REQUEST CONTENT BUFFER + mov rdx, r13 # AMOUNT OF BYTES IN REQUEST CONTENT + syscall + + # Save read bytes + mov r15, rax + + # close(unsigned int fd) + mov rax, 3 + mov rdi, r10 + syscall + + # We simply return a 200 OK here + + # write(unsigned int fd, const char* buf, size_t count) + mov rax, 1 + mov rdi, r14 + lea rsi, [rip + response] + mov rdx, 19 + syscall + + # close(unsigned int fd) + mov rax, 3 + mov rdi, r14 + syscall + + jmp .normal_exit + + .get_request: + # open(const char* filename, int flags, int mode) + mov rax, 2 + lea rdi, [path_buf+1] + mov rsi, O_RDONLY + mov rdx, NULL # who cares? + syscall + + # Save fd + mov r10, rax + + # read(unsigned int fd, char* buf, size_t count) + mov rax, 0 + mov rdi, r10 + lea rsi, [rip + file_buf] + mov rdx, 1024 + syscall + + # Save read bytes + mov r15, rax + + # close(unsigned int fd) + mov rax, 3 + mov rdi, r10 + syscall + + # write(unsigned int fd, const char* buf, size_t count) + mov rax, 1 + mov rdi, r14 + lea rsi, [rip + response] + mov rdx, 19 + syscall + + # write(unsigned int fd, const char* buf, size_t count) + mov rax, 1 + mov rdi, r14 + lea rsi, [rip + file_buf] + mov rdx, r15 + syscall + + # close(unsigned int fd) + mov rax, 3 + mov rdi, r14 + syscall + + .normal_exit: + # exit(0) + mov rax, 60 + mov rdi, 0 + syscall + + .error_exit: + mov rax, 60 + mov rdi, -1 + syscall + +.section .data + +response: + .asciz "HTTP/1.0 200 OK\r\n\r\n" + +str_welcome: + .asciz "Starting HTTP server...\n" + +# Total size: 16 bytes +sockaddr_in: + .word AF_INET + # Port 80 = 0x0050 (unsigned short, 2 bytes) + # Big endian: bytes are in reverse order + .word 0x5000 + # 32-bits of zeroes: 0.0.0.0 + .int 0x00000000 + .zero 8 + +.section .bss + +path_buf: + .zero 64 + +request_buf: + .zero 1024 + +file_buf: + .zero 1024 + +stat_buf: + .zero 1024 + +request_content_buf: + .zero 1024