-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathprogram.asm
More file actions
837 lines (601 loc) · 18.9 KB
/
program.asm
File metadata and controls
837 lines (601 loc) · 18.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
; program.asm - HTTP/1.0 server entry point
%include "./macros/envutils.asm"
%include "./macros/fileutils.asm"
%include "./macros/httputils.asm"
%include "./macros/logutils.asm"
%include "./macros/strutils.asm"
%include "./macros/sysutils.asm"
%include "./macros/whatmimeisthat.asm"
%include "./labels/flagparser.asm"
%include "./labels/initialsetup.asm"
%include "./labels/startupchecks.asm"
extern inet_ntop ; to process the client IP address
section .data
version db "1.11", 0
; socket setup
sockaddr:
dw 2 ; AF_INET (ipv4)
dw 0x1F90 ; port 8080 big-endian (edited at runtime)
dd 0 ; 0.0.0.0 = listen on all interfaces
dq 0 ; padding
sockopt dd 1 ; value for SO_REUSEADDR
client_addr_len dd 16 ; data directive for accept() (at .wait)
; HTTP constants
crlf db 0xd, 0xa, 0
response_405 db "HTTP/1.0 405 Method Not Allowed", 0
response_404 db "HTTP/1.0 404 Not Found", 0
response_403 db "HTTP/1.0 403 Forbidden", 0
response_401 db "HTTP/1.0 401 Unauthorized", 0
response_400 db "HTTP/1.0 400 Bad Request", 0
response_304 db "HTTP/1.0 304 Not Modified", 0
response_200 db "HTTP/1.0 200 OK", 0
allow_header db "Allow: GET, HEAD", 0
www_authenticate_header db "WWW-Authenticate: Basic realm=", 0
date_header db "Date: ", 0
server_header db "Server: ", 0
pragma_header db "Pragma: no-cache", 0
last_modified_header db "Last-Modified: ", 0
expires_header db "Expires: ", 0
content_type_header db "Content-Type: ", 0
content_encoding_header db "Content-Encoding: identity", 0 ; identity = not changed
content_length_header db "Content-Length: ", 0
accept_ranges_header db "Accept-Ranges: none", 0 ; We don't support ranging
connection_close_header db "Connection: close", 0 ; We don't support keep alive
empty db 0 ; for some checks
section .bss
; network
client_addr resb 16
client_ip_str resb 16 ; "255.255.255.255\0"
; request / response
request resb 8192 ; requests can get big
response resb 1024 ; 1024 should be enough for headers
; path handling
path resb 768 ; docroot + url + index
file_to_serve resq 1 ; pointer to path to serve, or 0 for none
; authentication
auth resb 258 ; 128 (user) + 1 (:) + 128 (pwd) + null term (1)
username resb 129
password resb 129
; client http headers
user_agent resb 1025 ; 1024 + "\0"
referer resb 1025
; misc
last_status resw 1 ; for logs
content_length_b resb 20
process_count resw 1 ; current processes count
log_port_buf resb 8 ; "65535\n\0" worst case
header_time resb 32 ; "Mon, 01 Jan 2000 00:00:00 GMT\0" + padding
request_type resb 1 ; GET = 0, HEAD = 1
is_dir resb 1 ; To 403 on dirs that don't have an index file
section .text
global _start
; consistent register usage, after startup (persistent across the request handling):
; r15 = server socket fd
; r14 = client socket fd (per request)
; r13 = response buffer start (anchor) / client IP str (at .end, for logging)
; r12 = response buffer write position / last status code (at .end, for logging)
; r11 = file fd (when serving a file)
_start:
mov r15, [rsp] ; argc
call parse_flags ; sets flag_* bytes, strips flags, etc. From labels/flagparser.asm
call initial_setup ; from labels/initialsetup.asm
call startup_checks ; from labels/startupchecks.asm
LF
PRINTN log_started_nasmserver, log_started_nasmserver_len
.start_server:
; create the server TCP socket
; socket(domain, type, protocol)
mov rax, 41
mov rdi, 2 ; ipv4
mov rsi, 1 ; stream
mov rdx, 0 ; tcp
syscall
cmp rax, 0
jl .fail_socket
mov r15, rax ; r15 will hold the socket fd
; allow reuse of the address so we can restart without waiting for TIME_WAIT
; setsockopt(fd, level, optname, optval, optlen)
mov rax, 54
mov rdi, r15
mov rsi, 1 ; SOL_SOCKET
mov rdx, 2 ; SO_REUSEADDR
mov r10, sockopt
mov r8, 4
syscall
cmp rax, 0
jne .fail_setsockopt ;
.bind_port:
; bind the socket to the configured port and interface
; bind(fd, sockaddr, addrlen)
mov rax, 49
mov rdi, r15
mov rsi, sockaddr
mov rdx, 16
syscall
cmp rax, 0
jl .fail_bind
; start listening for incoming connections
; listen(fd, backlog)
mov rax, 50
mov rdi, r15
movzx rsi, byte [max_requests]
syscall
; this mess prints the port log
PRINT_TIMESTAMP
PRINT log_prefix_info, log_prefix_info_len
PRINT log_listening_on, log_listening_on_len
STRLEN bind_addr_str, r9
PRINT bind_addr_str, r9 ; x.x.x.x
PRINT log_two_dots, log_two_dots_len ; ":"
; port int to ascii
movzx rbx, word [port]
ITOA rbx, log_port_buf, r9
PRINTN log_port_buf, r9 ; XXXX
.wait:
; from here, we're NOT stopping the program anymore
; block until a new connection arrives, then store the client fd in r14
; accept(fd, sockaddr, addrlen)
mov rax, 43
mov rdi, r15
mov rsi, client_addr
mov rdx, client_addr_len
syscall
cmp rax, 0
jl .fail_accept
mov r14, rax ; r14 will contain the client file descriptor
.wait_for_slot:
movzx rax, word [process_count]
cmp ax, [max_requests]
jb .do_fork
; try reaping first in case some just finished
call .reap_loop
movzx rax, word [process_count]
cmp ax, [max_requests]
jb .do_fork
; still full, drop the connection and warn
; close the file
; close(fd)
mov rax, 3
mov rdi, r14
syscall
LOG_WARNING log_too_many_concurrent, log_too_many_concurrent_len
jmp .wait
.do_fork:
; save client_addr to stack before forking to avoid race conds
push qword [client_addr + 8]
push qword [client_addr]
; spawn a child process to handle this request
; fork()
mov rax, 57
syscall
cmp rax, 0
jl .fail_accept
; small issue: .close_client only gets called on each new request,
; so zombie processes linger until the next connection comes in
jg .close_client
; we're now in the child process
; child doesn't need the server listening socket
; close(fd)
mov rax, 3
mov rdi, r15
syscall
movzx eax, byte [rsp + 4] ; first octet
movzx ebx, byte [rsp + 5] ; second
movzx ecx, byte [rsp + 6] ; third
movzx edx, byte [rsp + 7] ; fourth
; convert the binary client address to a printable string
; inet_ntop(af, src, dst, size)
mov edi, 2 ; AF_INET
lea rsi, [rsp + 4] ; pointer to sin_addr (client_addr + 4)
lea rdx, [client_ip_str] ; output buffer
mov ecx, 16 ; buffer size
call inet_ntop
.handle_request:
READ_FILE r14, request, 8192
IS_HTTP_REQUEST request, 8192
cmp rax, -405
je .method_not_allowed
cmp rax, -400
je .bad_request
cmp rax, -200
je .head
cmp rax, 200
je .get
jmp .forbidden ; in case i add a new code and forgot to implement it here
.get:
mov byte [request_type], 0
jmp .auth_check
.head:
mov byte [request_type], 1
.auth_check:
; if no auth is configured, go to auth_ok (no auth setuped)
cmp byte [auth_username], 0
je .auth_ok
PARSE_AUTH_HEADER request, 8192, auth, 264
STRLEN auth, rcx
; if nothing was decoded (no header sent), demand credentials
cmp rcx, 0
je .unauthorized
STRSPLIT auth, ':', username, password, rcx ; using rcx since rax gets clobbered
cmp rcx, 0 ; 0 = no ':', so bad creds format
je .unauthorized
; check if they're the correct ones
STREQ username, auth_username, rcx
cmp rcx, 0 ; 0 = not equal
je .unauthorized
STREQ password, auth_password, rcx
cmp rcx, 0
je .unauthorized
; both passed = auth passed
.auth_ok:
; prepend document_root so the path is relative to it
lea rsi, [document_root]
lea rdi, [path]
.copy_docroot:
mov al, [rsi]
test al, al
jz .copy_docroot_done
mov [rdi], al
inc rsi
inc rdi
jmp .copy_docroot
.copy_docroot_done:
lea rax, [path]
sub rdi, rax ; rdi = docroot length
mov rbx, rdi ; rbx = docroot length for offsetting
mov r10, 767
sub r10, rbx ; rcx = 255 - docroot_len = remaining space
lea rdi, [path + rbx]
PARSE_HTTP_PATH request, 8192, rdi, rax, r10 ; parse path into [path + docroot_len]
cmp rax, 0
jle .forbidden
add rax, rbx ; full length = docroot + http path
mov byte [path + rax + 1], 0
STRCUT path, '?' ; remove the ?query=string, we won't process it as a static site
cmp byte [path + rax], '/'
jne .check_dotfile
.add_index:
mov byte [is_dir], 1 ; for later processing
mov al, [rsi]
mov [rdi], al
inc rsi
inc rdi
test al, al
jnz .add_index
.check_dotfile:
; check if someone is trying to get a dotfile
cmp byte [serve_dots], 1 ; if the user want to serve dotfiles, just skip
je .check_exists
PATH_HAS_DOT path, rcx
cmp rcx, 1
je .forbidden
.check_exists:
; check if the file exists before continuing
lea rdi, [path]
FILE_EXISTS rdi
cmp rax, 0 ; does not exist
je .not_found
cmp rax, 1 ; exists and is a readable file
je .ok
cmp rax, 2 ; is a dir
je .add_slash
; 3 = exists but we can't read it
; but we're just not checking it to fallback to forbidden
jmp .forbidden
.add_slash:
lea rdi, [path]
.find_path_end:
cmp byte [rdi], 0
je .add_slash_2
inc rdi
jmp .find_path_end
.add_slash_2:
mov byte [rdi], '/'
inc rdi ; rdi now points past the slash (= where index_file goes)
lea rsi, [index_file]
jmp .add_index
.ok:
lea r13, [response]
lea r12, [response]
lea r10, [path]
mov [file_to_serve], r10
; check for If-Modified-Since header
PARSE_IMS_HEADER request, 8192, header_time
cmp byte [header_time], 0 ; no ims header = serve
je .ok_modified
; otherwise, process the ims thing and eventually return a 304
HTTP_PARSE_TIME header_time, rbx
cmp rbx, -1 ; invalid format
je .ok_modified
; stat(path, statbuf)
mov rax, 4
mov rdi, [file_to_serve]
lea rsi, [stat] ; [stat] is from fileutils.asm
syscall
cmp rax, 0
jl .ok_modified
mov rax, [stat + 88] ; st_mtime
cmp rax, rbx ; file_mtime >= ims_time?
jle .not_modified
.ok_modified:
mov rdi, 200
call .write_header
sub r12, r13
mov word [last_status], 200
jmp .send
.method_not_allowed:
lea r13, [response]
lea r12, [response]
mov qword [file_to_serve], errordoc_405_path
mov rdi, 405
call .write_header
sub r12, r13
mov word [last_status], 405
jmp .send
.not_found:
cmp byte [is_dir], 1 ; if it's a dir, but with no index file, send a 403 instead
je .forbidden
lea r13, [response]
lea r12, [response]
mov qword [file_to_serve], errordoc_404_path
mov rdi, 404
call .write_header
sub r12, r13
mov word [last_status], 404
jmp .send
.forbidden:
lea r13, [response]
lea r12, [response]
mov qword [file_to_serve], errordoc_403_path
mov rdi, 403
call .write_header
sub r12, r13
mov word [last_status], 403
jmp .send
.unauthorized:
mov al, [empty]
mov [username], al ; clear the username field to not send a wrong username in logs
lea r13, [response]
lea r12, [response]
mov qword [file_to_serve], errordoc_401_path
mov rdi, 401
call .write_header
sub r12, r13
mov word [last_status], 401 ;
jmp .send
.bad_request:
lea r13, [response]
lea r12, [response]
mov qword [file_to_serve], errordoc_400_path
mov rdi, 400
call .write_header
sub r12, r13
mov word [last_status], 400
jmp .send
.not_modified:
lea r13, [response]
lea r12, [response]
mov qword [file_to_serve], empty
mov rdi, 304
call .write_header
sub r12, r13
mov word [last_status], 304
jmp .send
.write_header:
; rdi: status code (200, 400, 403, 404 or 405)
; appends the HTTP header to the 'response' buffer
cmp rdi, 405
je .write_405
cmp rdi, 404
je .write_404
cmp rdi, 403
je .write_403
cmp rdi, 401
je .write_401
cmp rdi, 400
je .write_400
cmp rdi, 304
je .write_304
jmp .write_200
.write_405:
AAPPEND r12, response_405
AAPPEND r12, crlf
AAPPEND r12, allow_header
AAPPEND r12, crlf
jmp .header_date
.write_404:
AAPPEND r12, response_404
AAPPEND r12, crlf
jmp .header_date
.write_403:
AAPPEND r12, response_403
AAPPEND r12, crlf
jmp .header_date
.write_401:
AAPPEND r12, response_401
AAPPEND r12, crlf
AAPPEND r12, www_authenticate_header ; [...] Realm=
AAPPEND r12, log_quotation_mark ; "
AAPPEND r12, auth_realm ; Config realm name
AAPPEND r12, log_quotation_mark ; "
AAPPEND r12, crlf
jmp .header_date
.write_400:
AAPPEND r12, response_400
AAPPEND r12, crlf
jmp .header_date
.write_304:
AAPPEND r12, response_304
AAPPEND r12, crlf
jmp .header_date
.write_200:
AAPPEND r12, response_200
AAPPEND r12, crlf
.header_date:
GET_HTTP_TIME header_time
AAPPEND r12, date_header
AAPPEND r12, header_time
AAPPEND r12, crlf
.header_server:
AAPPEND r12, server_header
AAPPEND r12, server_name
AAPPEND r12, crlf
.header_pragma:
cmp dword [max_age], 0 ; if maxage is < 0, we don't send the pragma: no-cache header
ja .header_last_modified ; jump above (jg unsigned)
AAPPEND r12, pragma_header
AAPPEND r12, crlf
.header_last_modified:
mov rdi, [file_to_serve]
cmp byte [rdi], 0
je .header_expires
AAPPEND r12, last_modified_header
FILE_LAST_MODIFIED rdi, header_time
AAPPEND r12, header_time
AAPPEND r12, crlf
.header_expires:
mov r8d, dword [max_age]
HTTP_EXPIRE_DATE r8, header_time
AAPPEND r12, expires_header
AAPPEND r12, header_time
AAPPEND r12, crlf
.header_content_type:
; content type detection
mov rdi, [file_to_serve]
cmp byte [rdi], 0
je .header_content_encoding
AAPPEND r12, content_type_header
GET_MIME_TYPE rdi, rbx ; content type will be in rsi
mov rdi, rbx ; aappend doesn't clobbers rdi
AAPPEND r12, rdi
AAPPEND r12, crlf
.header_content_encoding:
; content type detection
mov rdi, [file_to_serve]
cmp byte [rdi], 0
je .header_content_length
AAPPEND r12, content_encoding_header
AAPPEND r12, crlf
.header_content_length:
; very similar to the previous one
mov rdi, [file_to_serve]
cmp byte [rdi], 0
je .accept_ranges_header
FILE_SIZE rdi, rbx
cmp rbx, 0 ; rbx < 0 means that it failed, skipping header
jl .accept_ranges_header
ITOA rbx, content_length_b, rcx
AAPPEND r12, content_length_header
AAPPEND r12, content_length_b
AAPPEND r12, crlf
.accept_ranges_header:
AAPPEND r12, accept_ranges_header
AAPPEND r12, crlf
.header_conn_close:
AAPPEND r12, connection_close_header
AAPPEND r12, crlf
.header_end:
AAPPEND r12, crlf ; blank line = end of headers
ret
.send:
PRINTF r14, r13, r12 ; send the headers first
; directly end if it's a HEAD request
cmp byte [request_type], 1
je .end
; serve the file if one was set
mov r10, [file_to_serve]
test r10, r10
jz .end
FILE_EXISTS r10
cmp rax, 1
jne .end ; file doesn't exist, just send headers
; open the file
mov rdi, r10
OPEN_FILE_R rdi
cmp rax, 0
jl .end ; shouldn't happen cuz FILE_EXISTS passed, but just in case
mov r11, rax ; r11 = file fd
; stream the file directly from the fd to the client socket
; sendfile(out_fd, in_fd, offset, count)
mov rax, 40
mov rdi, r14 ; client socket
mov rsi, r11 ; file fd
xor rdx, rdx ; offset = NULL (start from beginning)
mov r10, 0x7fffffff ; send as much as possible
syscall
; close(fd)
mov rax, 3
mov rdi, r11
syscall
.end:
; signal we're done writing so the client knows the response is complete
; shutdown(fd, how)
mov rax, 48
mov rdi, r14
mov rsi, 1 ; SHUT_WR
syscall
; drain remaining input so TCP can close cleanly
.__drain:
; read(fd, buffer, count)
mov rax, 0
mov rdi, r14
lea rsi, [response] ; we don't use response anymore, empty it in there
mov rdx, 16
syscall
cmp rax, 0
jg .__drain ; keep reading until eof / err
; close(fd)
mov rax, 3
mov rdi, r14
syscall
call .log_request
add rsp, 16
EXIT 0 ; child exits
.close_client:
add rsp, 16
; close the client fd in the parent, the child owns it now
; close(fd)
mov rax, 3
mov rdi, r14
syscall
inc word [process_count]
call .reap_loop
jmp .wait
.reap_loop:
; reap zombie processes
; wait4(pid, status, options, usage)
mov rax, 61
mov rdi, -1 ; any child
xor rsi, rsi
mov rdx, 1 ; WNOHANG
xor r10, r10
syscall
cmp rax, 0
jle .reap_done ; no child reaped, stop
dec word [process_count]
jmp .reap_loop
.reap_done:
ret
.log_request:
; parse other headers for the logs
PARSE_UA_HEADER request, 8192, user_agent, 1024
PARSE_REFERER_HEADER request, 8192, referer, 1024
cmp byte [use_xri], 1
jne .__log_req ; check if we need to use the X-Real-Ip header
PARSE_XRI_HEADER request, 8192, client_ip_str, 15
.__log_req:
mov r8, qword [log_file]
LOG_REQUEST_CLFE r8
ret
.fail_socket:
LOG_ERR log_fail_socket, log_fail_socket_len
EXIT rax
.fail_setsockopt:
LOG_ERR log_fail_setsockopt, log_fail_setsockopt_len
EXIT rax
.fail_bind:
LOG_ERR log_fail_bind, log_fail_bind_len
EXIT rax
.fail_accept:
LOG_ERR log_fail_accept, log_fail_accept_len
jmp .wait ; child exits