- ymawky macOS के लिए aarch64 assembly में लिखा गया एक छोटा static HTTP server है, जो libc wrapper के बिना केवल Darwin raw system calls का उपयोग करता है
- यह
GET, HEAD, PUT, OPTIONS, DELETE, byte-range requests, directory listing, custom error pages को support करता है, लेकिन nginx का विकल्प बनने के लिए नहीं, बल्कि web server के काम करने के तरीके को समझने के लिए convenience layers हटाकर बनाया गया implementation है
- request parsing, percent decoding, header inspection, range value conversion, error handling, file closing, response generation तक सब कुछ सीधे लिखना पड़ता है, और Python में simple string splitting या
int(string) जैसा काम भी assembly में दर्जनों से सैकड़ों लाइनों के validation code में बदल जाता है
- server हर नए connection पर
fork() कॉल करने वाली fork-on-request संरचना अपनाता है, इसलिए implementation आसान है, लेकिन concurrent connections की throughput कम हो सकती है और slowloris के प्रति कमजोर हो सकता है; इसी वजह से header timeout और Content-Length आधारित body timeout लागू किए गए हैं
PUT पहले .ymawky_tmp_<pid> temporary file में लिखता है और सफल होने पर replace करता है, साथ ही path traversal रोकथाम, O_NOFOLLOW_ANY, fstat64(), directory listing में URL encoding और HTML escaping जैसी filesystem safety को भी सीधे handle करता है
ymawky का अवलोकन और सीमाएँ
- ymawky macOS के लिए aarch64 assembly में लिखा गया एक छोटा static HTTP server है
- यह libc wrapper के बिना केवल Darwin raw system calls का उपयोग करता है, और कोई external library या existing parser इस्तेमाल नहीं करता
- supported features हैं
GET, HEAD, PUT, OPTIONS, DELETE, byte-range requests, directory listing, और custom error pages
- project की सीमाएँ इस प्रकार हैं
- aarch64 assembly only
- macOS/Darwin target
- raw syscalls only, libc wrappers नहीं
- static files only
- preexisting parsers नहीं
- external libraries नहीं
- इसका उद्देश्य nginx को replace करना नहीं है, बल्कि web server वास्तव में कैसे काम करता है यह समझने के लिए convenience layers हटाकर implementation करना है
assembly में web server बनाते समय क्या-क्या करना पड़ता है
- assembly machine code और high-level languages के बीच की layer है, और
mov, add, ldr, str, cmp जैसे instructions executable binary के bytes से सीधे मेल खाते हैं
svc #0x80 executable binary के D4 00 10 01 bytes का human-readable रूप है
- string type न होने के कारण strings memory में लगातार bytes के क्षेत्र के रूप में मौजूद होती हैं, और C के
struct जैसी language feature भी नहीं होती, इसलिए field offsets और total size खुद जानने पड़ते हैं
- HTTP library, automatic cleanup, exceptions, objects कुछ भी नहीं होने के कारण request parsing, error handling, file closing, response generation जैसे सारे काम सीधे लिखने पड़ते हैं
- गलत काम करने पर भी CPU बिना warning दिए वैसे ही execute करता रहता है, इसलिए समस्या लिखे गए instructions और memory access में ही होती है
raw system calls और server flow
-
Darwin system calls
- ymawky libc wrapper के बजाय kernel को सीधे call करता है
- Darwin aarch64 में system call number
x16 register में रखा जाता है, जबकि Linux aarch64 में x8 में
open() system call number 5 है, और filename व mode जैसे arguments को registers में सीधे रखकर svc #0x80 के माध्यम से kernel को call किया जाता है
open() fail होने पर carry flag set होता है, और b.cs open_failed की तरह carry flag जाँचकर failure-handling code पर branch किया जाता है
-
basic server behavior
- web server का बुनियादी flow request लेना, उसे process करना, और status code व आवश्यक file वापस करना है
- socket setup में
socket(AF_INET, SOCK_STREAM, 0), setsockopt(... SO_REUSEADDR ...), bind(sockfd, &addr, 16), listen(sockfd, 5), accept(sockfd, NULL, NULL) जैसे चरण शामिल हैं
- ymawky हर नए connection पर
fork() call करने वाला fork-on-request server है
- यह तरीका request handling के बीच memory share नहीं करता, इसलिए इसे समझना और implement करना आसान है, लेकिन process-per-memory-space के कारण overhead बढ़ता है और nginx के event-driven asynchronous non-blocking model की तुलना में concurrent connection throughput कम रहती है
- concurrent connections बढ़ने पर kernel, process के भीतर execution से ज्यादा समय process switching में खर्च कर सकता है
-
request handling में जरूरी काम
- request method
GET, HEAD, OPTIONS, PUT, DELETE में से कौन-सी है यह पहचानना
- request path निकालना और
%20 जैसी percent encoding decode करना
- path safety checks करना, और client द्वारा भेजे गए header fields parse करना
- requested file की जानकारी लाकर यह तय करना कि वह directory है या regular file
PUT request body को temporary file में लिखना और response headers व body बनाना
- खुले हुए files बंद करना और server crash न हो इसके लिए errors handle करना
HTTP parsing खुद implement करना
-
request line और header termination
-
path extraction
- ymawky supported methods और शुरुआती bytes की तुलना करके request type पहचानता है, फिर path निकालता है
- यह headers को byte-by-byte scan करके
/ या * ढूँढता है, लेकिन HTTP/1.0 के अंदर के / को path न समझ ले, इसके लिए / से ठीक पहले का byte space है या नहीं यह जाँचता है
- उदाहरण के लिए
GET HTTP/1.0\r\n\r\n में HTTP/1.0 के अंदर / है, इसलिए अगर उसके पहले का byte space न हो तो 400 Bad Request लौटाया जाता है
- अधिकांश systems में
PATH_MAX 4096 bytes होता है, इसलिए ymawky में 4096-byte filename buffer और null terminator के 1 byte के लिए filename_buffer: .skip 4097 रखा गया है
- अगर request path buffer से लंबा हो, तो arbitrary memory overwrite करने के बजाय
414 URI Too Long लौटाना चाहिए
- Python के
text.split("GET /")[1].split(" ")[0] जैसे काम को assembly में HTTP validity checks सहित लगभग 200 lines लगती हैं
-
percent decoding और header field validation
- path में
% मिलने पर अगले दो bytes 0-9, a-f, A-F के valid hex digits हैं या नहीं, यह जाँचा जाता है, और फिर उन्हें संबंधित byte value में बदला जाता है
GET में Range: header हो सकता है और PUT में Content-Length: आवश्यक होता है
- ये headers request URL की तरह fixed position पर नहीं होते, इसलिए पूरे header को character-by-character traverse करना पड़ता है
- अगर
\r के बाद \n न हो, या पहले \r के बिना \n आ जाए, तो header को invalid मानकर 400 Bad Request लौटाया जाता है
- अगर नई header line space से शुरू होती है, तो header field space से शुरू नहीं हो सकती, इसलिए
400 Bad Request लौटाया जाता है
-
string comparison और number conversion
PUT handling और temporary file strategy
PUT एक idempotent method है, यानी वही request कई बार भेजने पर भी अंतिम server state समान रहती है
PUT /file.txt file.txt को बनाता है या existing file को पूरी तरह overwrite करता है, और 1234 को दो बार भेजने पर file content 12341234 नहीं बल्कि 1234 रहता है
- globally open
PUT खतरनाक हो सकता है, और handling के दौरान ये समस्याएँ ध्यान में रखनी होती हैं
- request processing के दौरान process crash हो जाना
- client का
Content-Length 2KB बताकर केवल 100 bytes भेजना
- client का
Content-Length 50GB जैसी बहुत बड़ी value भेजना
config.S में MAX_BODY_SIZE default रूप से 1GB है, और Content-Length इससे ज्यादा होने पर 413 Content Too Large लौटाया जाता है
- existing file को सीधे खोलकर लिखने पर failure की स्थिति में आधी-लिखी file रह सकती है, इसलिए ymawky पहले
.ymawky_tmp_<pid> format की temporary file में लिखता है
getpid() system call number 20 से pid लिया जाता है, और custom itoa() से उसे string में बदला जाता है, साथ ही buffer overflow की जाँच भी होती है
- client body को temporary file में पूरा लिखने और सफलता मिलने पर temporary file को in-place नाम से replace किया जाता है, जिससे requested file server पर बनती है
- अगर client अनपेक्षित रूप से connection तोड़ दे, timeout हो जाए, या invalid body भेजे, तो temporary file को
unlink() system call 10 या unlinkat() system call 472 से delete कर दिया जाता है
- existing file को केवल तभी overwrite किया जाता है जब पूरी request सफलतापूर्वक transfer हो जाए
directory listing और escaping
network security और timeouts
filesystem safety
-
file information check का क्रम
GET और HEAD में requested path खोलने के बाद file descriptor पर fstat64() system call 339 चलाकर file type और size जैसी जानकारी ली जाती है
- अगर पहले path पर
stat64() system call 338 चलाया जाए और उसके बाद file खोली जाए, तो check और use के बीच file बदल जाने वाली TOCTOU race condition पैदा हो सकती है
-
docroot और path traversal रोकथाम
- हर request path के आगे docroot जोड़ा जाता है
- default docroot
config.S के DEFAULT_DIR यानी www/ है
/etc/shadow request www/etc/shadow बन जाती है, इसलिए जब तक www/etc/shadow वास्तव में मौजूद न हो, 404 मिलेगा
- लेकिन
/../../../../etc/shadow www/../../../../etc/shadow बनकर docroot के बाहर resolve हो सकती है, इसलिए अतिरिक्त बचाव जरूरी है
- ymawky केवल
.. string वाले हर path को reject नहीं करता, बल्कि केवल उन path segments को reject करता है जो ठीक-ठीक .. हों
%2E%2E decode होने के बाद .. बनता है, इसलिए यह जाँच percent decoding के बाद करनी चाहिए
-
symbolic links handling
- POSIX का
O_NOFOLLOW flag यह सुनिश्चित करता है कि अगर अंतिम path component symbolic link हो, तो open() fail हो जाए
- Darwin का
O_NOFOLLOW_ANY यह सुनिश्चित करता है कि path का कोई भी component symbolic link हो, तो call fail हो जाए
- अगर कोई docroot के भीतर खास symbolic link डाल सकता है, तो शायद पहले से कोई और बड़ी समस्या मौजूद है, लेकिन यह flag अतिरिक्त सुरक्षा देता है
Apple-specific behavior
-
timeout handling और sigaction()
- request timeout implement करने के लिए
setitimer() system call 83 से कुछ समय बाद SIGALRM भेजना पड़ता है
- default रूप से
SIGALRM child को मार देता है, लेकिन ymawky पहले 408 Request Timeout भेजना चाहता है
- इसके लिए
sigaction() system call 46 का उपयोग होता है
- Darwin का raw
sigaction struct sa_tramp field को expose करता है
- सामान्यतः libc
sa_tramp set करके stack और registers save करता है, sigreturn तैयार करता है, और फिर handler पर branch करता है
- ymawky का timeout handler
408 Request Timeout भेजता है, जरूरी चीजें बंद करता है, और child को exit कर देता है, इसलिए उसे return करने की जरूरत नहीं होती
- इसलिए trampoline slot को सीधे timeout response चलाने वाले code की ओर point कराया जाता है, और
sa_handler व sigreturn को bypass किया जाता है
-
proc_info() और child process count limit
- Apple में running processes और उनके child की जानकारी लेने के लिए कम documented
proc_info() system call 336 उपलब्ध है
- यह call आम तौर पर
ps, lsof, top जैसे tools में उपयोग होती है
- ymawky active child processes की गिनती के लिए
proc_info() का उपयोग करता है
- maximum connections configurable हैं, इसलिए live child count जानना जरूरी है
proc_info() child process information को buffer में लिखता है, और हर element का size ज्ञात होने के कारण written bytes से child count निकाला जा सकता है
- अगर child count
MAX_PROCS से ऊपर हो जाए, तो नए connections को 503 Service Unavailable के साथ reject किया जाता है
निष्कर्ष और project जानकारी
- static web server में कठिन हिस्सा sockets खोलना और listen करना नहीं, बल्कि request parsing और सभी edge cases को संभालना था
- requests, paths, responses सब bytes हैं; range requests को सटीक होना पड़ता है और filenames को उनकी position के हिसाब से अलग-अलग escape करना पड़ता है
- assembly request parsing, memory management, error handling, string conversion, timeouts, और file safety जैसे हर काम को सीधे लिखने पर मजबूर करती है
- ymawky को imtomt maintain करते हैं
1 टिप्पणियां
Lobste.rs की राय
कमाल है। पहले मैंने एक छोटी कंपनी के साथ इंटीग्रेशन का काम किया था जो smart devices बनाती थी, और उस कंपनी का इकलौता engineer assembly language के अलावा कुछ जानता ही नहीं था
hardware control code से लेकर server operating system, और यहाँ तक कि हमारे द्वारा इस्तेमाल किया जाने वाला JSON web API भी सब कुछ उसने सीधे assembly में लिखा था
एक बार हमें ऐसा bug मिला जहाँ web API किसी बिल्कुल अलग device का data लौटाता था, और बाद में पता चला कि operating system scheduling system में off-by-one error थी, जिसकी वजह से “database” web service को गलत row वापस दे रहा था
“आत्महत्या” जैसे expressions को संभालते समय कृपया content warning लगाएँ। इससे भी बेहतर है कि उसका ज़िक्र ही न किया जाए
यह comment देखने के बाद मैंने फिर से ढूँढा, लेकिन तब भी नहीं मिला — क्या मुझसे कुछ छूट गया?
“सब कुछ assembly में लिखा गया” वाली बात देखकर मुझे Therac-25 investigation report याद आ गई