;;; SPDX-License-Identifier: CC0-1.0 ;;; ;;; Copyright (C) 2023, 2024 Wojtek Kosior <koszko@koszko.org> (use-modules ((scheme base) #:select (bytevector-copy string->utf8)) ((rnrs bytevectors) #:prefix bv:) ((srfi srfi-8) #:select (receive)) ((srfi srfi-26) #:select (cut)) ((srfi srfi-34) #:select (guard raise)) ((srfi srfi-35) #:select (condition-has-type?)) ((ice-9 safe-r5rs) #:select (null-environment)) ((ice-9 format) #:select (format)) ((ice-9 regex) #:select (regexp-substitute/global)) ((web client) #:select (open-socket-for-uri http-get)) ((web response) #:prefix rsp:) ((web uri) #:prefix uri:) ((myra-test-utils) #:prefix tu:) ((cantius) #:prefix cant:)) (define (use-crlf text) (regexp-substitute/global #f "\n" text 'pre "\r\n" 'post)) (define %form-data-good (use-crlf "\ ----boundary Content-Disposition: form-data; name=\"binary-field\"; filename=\"somefile\" Content-Type: application/octet-stream śómę-ńóń-ĄŚĆII-dątą\\ ----boundary Content-Disposition: form-data; name=\"text-field1\" Content-Transfer-Encoding: binary óthęr-ńóń-ĄŚĆII-dątą\\ ----boundary Content-Disposition: form-data; name=\"text-field2\"; Content-Type: text/plain //yęt-óthęr-ńóń-ĄŚĆII-dątą ----boundary-- ")) (define %form-data-bad-epilogue (use-crlf "\ ----boundary Content-Disposition: form-data; name=\"field\" value ----boundary--SOMETHING-INCORRECT-HERE ")) (define %form-data-unsupported-encoding (use-crlf "\ ----boundary Content-Disposition: form-data; name=\"field\" Content-Transfer-Encoding: 7bit value ----boundary-- ")) (define %form-boundary-1 "--boundary") (define %form-boundary-2 "--boundary-with-\\-(a-backslash)") (tu:test-group "parse-multipart/form-data" (for-each (lambda (boundary form-data) (define (parsed-multipart) (cant:parse-multipart/form-data boundary (bv:string->utf8 form-data))) (tu:test-eqv 3 (length (parsed-multipart))) (tu:test-equal '("binary-field" "text-field1" "text-field2") (map cant:form-data-part-name (parsed-multipart))) (tu:test-equal '(((content-disposition . (form-data (name . "binary-field") (filename . "somefile"))) (content-type . (application/octet-stream))) ((content-disposition . (form-data (name . "text-field1"))) (content-transfer-encoding . "binary")) ((content-disposition . (form-data (name . "text-field2"))) (content-type . (text/plain)))) (map cant:form-data-part-headers (parsed-multipart))) (tu:test-equal '(#t #f #f) (map cant:form-data-part-binary? (parsed-multipart))) (tu:test-equal `(,(bv:string->utf8 "śómę-ńóń-ĄŚĆII-dątą\\") "óthęr-ńóń-ĄŚĆII-dątą\\" "//yęt-óthęr-ńóń-ĄŚĆII-dątą") (map cant:form-data-part-data (parsed-multipart)))) (list %form-boundary-1 %form-boundary-2) (list %form-data-good (regexp-substitute/global #f %form-boundary-1 %form-data-good 'pre %form-boundary-2 'post))) (tu:test-error (cut condition-has-type? <> cant:&bad-request) (cant:parse-multipart/form-data %form-boundary-1 (bv:string->utf8 %form-data-bad-epilogue))) (tu:test-error (compose (cut = 415 <>) cant:condition-http-code) (cant:parse-multipart/form-data %form-boundary-1 (bv:string->utf8 %form-data-unsupported-encoding)))) (tu:test-group "parse-cookies" (tu:test-equal '(("name" . "val")) (cant:parse-cookies "name=val;")) (tu:test-equal '(("ses sion" . "fargtrg34") ("s" . "f") ("forms-session2" . "") ("forms-sessio n3" . "f4 3433=g = 4")) (cant:parse-cookies (string-append "\t\tses sion =fargtrg34;" " s = f;" "forms-session2=\n;" " forms-sessio n3=f4 3433=g = 4"))) (tu:test-equal '() (cant:parse-cookies "")) (for-each (lambda (bad-cookies) (tu:test-error (cut condition-has-type? <> cant:&bad-request) (cant:parse-cookies bad-cookies))) '(";" "c1=v1;c2=v2;c3;c4=v4" "something" "k=v;;"))) (define (make-env) (let ((env (null-environment 5))) (module-use! env (resolve-interface '(guile))) env)) (define (test-env name) (resolve-module `(cantius-test ,name))) (define (open-socket-with-timeout uri timeout) (let ((start-time (current-time))) (let loop () (guard (cnd ((> (current-time) (+ start-time timeout)) (raise cnd)) (#t (sleep 1) (loop))) (open-socket-for-uri uri))))) (tu:test-group "nonfree-site" (tu:test-assert (or (eval '(begin (define-module (cantius-test nonfree-site) #:use-module ((srfi srfi-18) #:select (make-thread thread-terminate! thread-yield! thread-start!)) #:use-module ((ice-9 format) #:select (format)) #:use-module ((ice-9 textual-ports) #:select (get-string-all)) #:use-module (cantius)) (define sock-port #f)) (make-env)))) (tu:test-assert (or (eval '(begin (define %my-endset (endset)) (define-endpoint %my-endset about-ms ("cool-companies" "ms" "about") '() "Microsoft is my favorite company. I started using Microsoft at the age of...") (define-endpoint %my-endset broken ("cool-companies" "ms" "product-list") '() (/ 1 0)) (define-endpoint %my-endset drm-wiki ("drm-wiki" ,(string-append "dev" "ices") . some-path) '() (values (build-response*) (format #f "~@{~@?~%~^~}" "full page path1: ~a" (%current-path-string) "full page path2: ~{/~a~}" (%current-path) "device page path: ~{/~a~}" some-path))) (define-endpoint %my-endset about-google ,(list "cool-companies" "google" "about") `((,%redirect/normalize-path? . #f) (,%redirect/remove-query? . #f)) (values (build-response*) "Google provides the best services in the world...")) (define-endpoint %my-endset static-files ("static" . file-path) `((,%resource-root-path . ,(dirname (search-path %load-path "cantius" ".scm"))) (,%default-headers . ((content-type . (text/formatted (charset . "Windows-1252")))))) (call-with-input-file (find-resource-file (string-join file-path "/")) get-string-all)) (define server-sock (socket PF_INET SOCK_STREAM 0)) (bind server-sock AF_INET INADDR_LOOPBACK 0) (define sock-port (sockaddr:port (getsockname server-sock))) (define server-thread (make-thread (lambda () (run-cantius %my-endset #:socket server-sock)))) (thread-start! server-thread) (thread-yield!)) (test-env 'nonfree-site)) #t)) (define (get path) (false-if-exception (let* ((sock-port (eval 'sock-port (test-env 'nonfree-site))) (client-sock (open-socket-with-timeout (format #f "http://localhost:~a" sock-port) 5))) (dynamic-wind (const #f) (lambda () (receive (response response-body) (http-get (string-append "http://dummy" path) #:port client-sock) (list response response-body))) (lambda () (close client-sock)))))) (tu:test-group "ms-about" (define ms-result (get "/cool-companies/ms/about")) (tu:test-eqv 200 (rsp:response-code (car ms-result))) (tu:test-equal '(text/plain (charset . "utf-8")) (rsp:response-content-type (car ms-result))) (tu:test-assert (string-prefix? "Microsoft is my favorite company" (cadr ms-result)))) (for-each (lambda (path) (tu:test-group "ms-about-auto-redirect" (define ms-redirect-result (get path)) (tu:test-eqv 301 (rsp:response-code (car ms-redirect-result))) (tu:test-equal "/cool-companies/ms/about" (uri:uri-path (rsp:response-location (car ms-redirect-result)))))) '("/cool-companies//ms/about" "/cool-companies/ms/about?a=b")) (tu:test-group "error-500" (define error-500-result (get "/cool-companies/ms/product-list")) (tu:test-eqv 500 (rsp:response-code (car error-500-result))) (tu:test-assert (string-contains (cadr error-500-result) "Backtrace"))) (tu:test-group "error-404" (define error-404-result (get "/cool-companies/amazon//about?c=d")) (tu:test-eqv 404 (rsp:response-code (car error-404-result)))) (tu:test-group "drm-wiki" (define ipad-result (get "/drm-wiki/devices/apple/ipad")) (tu:test-equal (format #f "~@{~a~%~}" "full page path1: /drm-wiki/devices/apple/ipad" "full page path2: /drm-wiki/devices/apple/ipad" "device page path: /apple/ipad") (cadr ipad-result))) (tu:test-group "about-google" (define google-result (get "/cool-companies///google/about?e=f")) (tu:test-eqv 200 (rsp:response-code (car google-result))) (tu:test-assert (string-prefix? "Google provides" (cadr google-result)))) (tu:test-group "valid-static-resources" (define valid-resource-result (get "/static/cantius.scm")) (tu:test-eqv 200 (rsp:response-code (car valid-resource-result))) (tu:test-equal '(text/formatted (charset . "Windows-1252")) (rsp:response-content-type (car valid-resource-result))) (tu:test-assert (string-prefix? ";;; " (cadr valid-resource-result)))) (tu:test-group "illegal-static-resources" (define illegal-resource-result (get "/static/stylesheets/../../../../../../etc/passwd")) (tu:test-eqv 403 (rsp:response-code (car illegal-resource-result))) ;; Default handler for illegal resource accesses is expected to ignore the ;; value of the `%default-headers` parameter. (tu:test-equal '(text/plain (charset . "utf-8")) (rsp:response-content-type (car illegal-resource-result)))) (tu:test-group "missing-static-resources" (define missing-resource-result (get "/static/i'm-not-here!!!")) (tu:test-eqv 404 (rsp:response-code (car missing-resource-result))) ;; Default handler for missing resource accesses is expected to ignore the ;; value of the `%default-headers` parameter. (tu:test-equal '(text/plain (charset . "utf-8")) (rsp:response-content-type (car missing-resource-result)))) (tu:test-assert (eval '(begin (thread-terminate! server-thread) (close server-sock)) (test-env 'nonfree-site)))) (tu:test-group "normalize-path" (tu:test-equal "a/b/c/d" ((@ (cantius) normalize-path) "a//b/./c//c1/../d")) (tu:test-equal "." ((@ (cantius) normalize-path) "aa/bb/cc/../../..")) (tu:test-equal "/" ((@ (cantius) normalize-path) "/dd/ee/ff/../../..")) (tu:test-equal "/../../g/h/i" ((@ (cantius) normalize-path) "/ignored0/../ignored1/../.././ignored2/../../g/h/i")) (tu:test-equal "../j/k/l" ((@ (cantius) normalize-path) "../j/k/l")))