@@ -692,6 +692,249 @@ static int test_keepalive(char *ip)
692692 return r ;
693693}
694694
695+ /*
696+ * URI sanitization tests for kws_parse_header → clean_uri.
697+ *
698+ * Drives a parallel HTTP server that echoes request->uri (after
699+ * kws_parse_header has normalized it) back to the client as the
700+ * response body "URI=<value>". The client sends a table of probes
701+ * over one keepalive connection and asserts on the echoed value.
702+ */
703+
704+ struct uri_case {
705+ const char * send ;
706+ const char * expect ;
707+ };
708+
709+ static const struct uri_case g_uri_cases [] = {
710+ /* /../ at root is clamped — cannot escape document root */
711+ { "/../../etc/passwd" , "/etc/passwd" },
712+ { "/foo/../../bar" , "/bar" },
713+ /* dot and empty segments are collapsed */
714+ { "/./a" , "/a" },
715+ { "//a//b" , "/a/b" },
716+ { "/foo/." , "/foo" },
717+ /* URL-encoded dots are NOT decoded; the literal %2E survives */
718+ { "/%2E%2E/%2E%2E/etc/passwd" , "/%2E%2E/%2E%2E/etc/passwd" },
719+ { NULL , NULL }
720+ };
721+
722+ static void make_deep_uri (char * buf , size_t bufsz , int segments )
723+ {
724+ size_t off = 0 ;
725+ int i ;
726+ for (i = 0 ; i < segments && off + 2 < bufsz ; i ++ ) {
727+ buf [off ++ ] = '/' ;
728+ buf [off ++ ] = 'a' ;
729+ }
730+ buf [off ] = '\0' ;
731+ }
732+
733+ static void uri_sanitize_server_callback (ks_socket_t server_sock , ks_socket_t client_sock , ks_sockaddr_t * addr , void * user_data )
734+ {
735+ struct tcp_data * tcp_data = (struct tcp_data * ) user_data ;
736+ kws_t * kws = NULL ;
737+ kws_request_t * request = NULL ;
738+ int flags = KWS_BLOCK | KWS_STAY_OPEN | KWS_HTTP ;
739+ int keepalive = 0 ;
740+
741+ if (kws_init (& kws , client_sock , NULL , NULL , flags , tcp_data -> pool ) != KS_STATUS_SUCCESS ) {
742+ goto end ;
743+ }
744+
745+ new_req :
746+ if (kws_parse_header (kws , & request ) != KS_STATUS_SUCCESS ) {
747+ /* Expected for the >=64-segment probe: clean_uri rejects, connection drops. */
748+ goto end ;
749+ }
750+
751+ if (!strncmp (request -> method , "GET" , 3 )) {
752+ char body [2048 ];
753+ char hdr [256 ];
754+ int blen = ks_snprintf (body , sizeof (body ), "URI=%s" , request -> uri );
755+ ks_snprintf (hdr , sizeof (hdr ),
756+ "HTTP/1.1 200 OK\r\n"
757+ "Content-Length: %d\r\n"
758+ "Content-Type: text/plain\r\n"
759+ "Connection: keep-alive\r\n"
760+ "Server: libks-uri-test\r\n\r\n" ,
761+ blen );
762+ kws_raw_write (kws , hdr , strlen (hdr ));
763+ kws_raw_write (kws , body , blen );
764+ }
765+
766+ end :
767+ if (request ) {
768+ keepalive = request -> keepalive ;
769+ kws_request_free (& request );
770+ }
771+
772+ if (keepalive ) {
773+ int pflags = kws_wait_sock (kws , 1000 , KS_POLL_READ );
774+ if (pflags > 0 && (pflags & KS_POLL_READ )) {
775+ if (kws_keepalive (kws ) == KS_STATUS_SUCCESS ) {
776+ goto new_req ;
777+ }
778+ }
779+ }
780+
781+ ks_socket_close (& client_sock );
782+ kws_destroy (& kws );
783+ }
784+
785+ static void * uri_sanitize_tcp_sock_server (ks_thread_t * thread , void * thread_data )
786+ {
787+ struct tcp_data * tcp_data = (struct tcp_data * ) thread_data ;
788+
789+ tcp_data -> ready = 1 ;
790+ ks_listen_sock (tcp_data -> sock , & tcp_data -> addr , 0 , uri_sanitize_server_callback , tcp_data );
791+
792+ return NULL ;
793+ }
794+
795+ static int extract_echoed_uri (const char * response , ks_size_t response_len , char * out , size_t outsz )
796+ {
797+ const char * body ;
798+
799+ if (response_len == 0 ) return 0 ;
800+
801+ body = strstr (response , "\r\n\r\n" );
802+ if (!body ) return 0 ;
803+ body += 4 ;
804+
805+ if (strncmp (body , "URI=" , 4 ) != 0 ) return 0 ;
806+ body += 4 ;
807+
808+ ks_snprintf (out , outsz , "%s" , body );
809+ return 1 ;
810+ }
811+
812+ static int test_uri_sanitize (char * ip )
813+ {
814+ ks_thread_t * thread_p = NULL ;
815+ ks_pool_t * pool ;
816+ ks_sockaddr_t addr ;
817+ int family = AF_INET ;
818+ ks_socket_t cl_sock = KS_SOCK_INVALID ;
819+ struct tcp_data tcp_data = { 0 };
820+ int r = 1 , sanity = 100 ;
821+ int i ;
822+
823+ ks_pool_open (& pool );
824+ tcp_data .pool = pool ;
825+
826+ if (strchr (ip , ':' )) family = AF_INET6 ;
827+
828+ if (ks_addr_set (& tcp_data .addr , ip , tcp_port , family ) != KS_STATUS_SUCCESS ) {
829+ r = 0 ;
830+ printf ("URI CLIENT Can't set ADDR\n" );
831+ goto end ;
832+ }
833+
834+ if ((tcp_data .sock = socket (family , SOCK_STREAM , IPPROTO_TCP )) == KS_SOCK_INVALID ) {
835+ r = 0 ;
836+ printf ("URI CLIENT Can't create sock family %d\n" , family );
837+ goto end ;
838+ }
839+
840+ ks_socket_option (tcp_data .sock , SO_REUSEADDR , KS_TRUE );
841+ ks_socket_option (tcp_data .sock , TCP_NODELAY , KS_TRUE );
842+ tcp_data .ip = ip ;
843+
844+ ks_thread_create (& thread_p , uri_sanitize_tcp_sock_server , & tcp_data , pool );
845+
846+ while (!tcp_data .ready && -- sanity > 0 ) {
847+ ks_sleep (10000 );
848+ }
849+
850+ ks_addr_set (& addr , ip , tcp_port , family );
851+ cl_sock = ks_socket_connect (SOCK_STREAM , IPPROTO_TCP , & addr );
852+
853+ for (i = 0 ; g_uri_cases [i ].send ; i ++ ) {
854+ char req [2048 ];
855+ char response [4096 ] = { 0 };
856+ char echoed [1024 ] = { 0 };
857+ ks_size_t len ;
858+ const char * send_uri = g_uri_cases [i ].send ;
859+ const char * expect = g_uri_cases [i ].expect ;
860+
861+ ks_snprintf (req , sizeof (req ),
862+ "GET %s HTTP/1.1\r\n"
863+ "HOST: localhost\r\n"
864+ "Connection: keep-alive\r\n\r\n" ,
865+ send_uri );
866+
867+ len = strlen (req );
868+ ks_socket_send (cl_sock , req , & len );
869+ ks_sleep_ms (200 );
870+
871+ len = sizeof (response ) - 1 ;
872+ if (ks_socket_recv (cl_sock , response , & len ) != KS_STATUS_SUCCESS || len == 0 ) {
873+ printf ("URI sanitize [fail]: %s -> no response\n" , send_uri );
874+ r = 0 ;
875+ continue ;
876+ }
877+ response [len ] = '\0' ;
878+
879+ if (!extract_echoed_uri (response , len , echoed , sizeof (echoed ))) {
880+ printf ("URI sanitize [fail]: %s -> response missing URI= marker\n" , send_uri );
881+ r = 0 ;
882+ continue ;
883+ }
884+
885+ printf ("URI sanitize: %-40s -> %-40s (expect %s)\n" , send_uri , echoed , expect );
886+
887+ if (strcmp (echoed , expect ) != 0 ) {
888+ printf ("URI sanitize [fail]: %s -> got %s, expected %s\n" , send_uri , echoed , expect );
889+ r = 0 ;
890+ }
891+ }
892+
893+ /* Deep-URI probe: >64 segments must hit clean_uri's reject branch. Server
894+ closes without responding; client recv should not yield a body. */
895+ {
896+ char deep [2048 ];
897+ char req [4096 ];
898+ char buf [1024 ];
899+ ks_size_t len ;
900+ ks_status_t recv_status ;
901+
902+ make_deep_uri (deep , sizeof (deep ), 70 );
903+ ks_snprintf (req , sizeof (req ),
904+ "GET %s HTTP/1.1\r\n"
905+ "HOST: localhost\r\n"
906+ "Connection: keep-alive\r\n\r\n" ,
907+ deep );
908+ len = strlen (req );
909+ ks_socket_send (cl_sock , req , & len );
910+ ks_sleep_ms (200 );
911+
912+ len = sizeof (buf );
913+ recv_status = ks_socket_recv (cl_sock , buf , & len );
914+ if (recv_status == KS_STATUS_SUCCESS && len > 0 ) {
915+ printf ("URI sanitize [fail]: deep URI got %zu-byte response\n" , (size_t )len );
916+ r = 0 ;
917+ } else {
918+ printf ("URI sanitize: deep URI correctly rejected (status %d)\n" , (int )recv_status );
919+ }
920+ }
921+
922+ end :
923+ if (tcp_data .sock != KS_SOCK_INVALID ) {
924+ ks_socket_shutdown (tcp_data .sock , 2 );
925+ ks_socket_close (& tcp_data .sock );
926+ }
927+
928+ if (thread_p ) {
929+ ks_thread_join (thread_p );
930+ }
931+
932+ ks_socket_close (& cl_sock );
933+ ks_pool_close (& pool );
934+
935+ return r ;
936+ }
937+
695938int main (void )
696939{
697940 int have_v4 = 0 , have_v6 = 0 ;
@@ -704,7 +947,7 @@ int main(void)
704947 have_v4 = ks_zstr_buf (v4 ) ? 0 : 1 ;
705948 // have_v6 = ks_zstr_buf(v6) ? 0 : 1;
706949
707- plan ((have_v4 * 5 ) + (have_v6 * 4 ) + 1 );
950+ plan ((have_v4 * 6 ) + (have_v6 * 4 ) + 1 );
708951
709952 ok (have_v4 || have_v6 );
710953
@@ -718,6 +961,7 @@ int main(void)
718961 ok (test_post (v4 ));
719962 ok (test_keepalive (v4 ));
720963 ok (test_post_json_read_buffer (v4 ));
964+ ok (test_uri_sanitize (v4 ));
721965 }
722966
723967 if (have_v6 ) {
0 commit comments