simple-learning

Simple learning web program
git clone git://git.laack.co/simple-learning.git
Log | Files | Refs | README | LICENSE

commit 4ad8279a13e5ef52372a9449c2b7a3eaa542cd48
parent 1445636cfcda30715c304ad5bb17e19d61f99e9b
Author: Andrew Laack <andrew@laack.co>
Date:   Wed,  2 Sep 2026 11:35:11 -0500

Refactoring to test server, improved course parameter handling, improved status code messaging.

Diffstat:
Mlearning/bin/dune | 2+-
Mlearning/bin/main.ml | 50+++-----------------------------------------------
Mlearning/lib/dune | 2+-
Alearning/lib/server.ml | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlearning/test/test_learning.ml | 1+
Alearning/test/test_server.ml | 38++++++++++++++++++++++++++++++++++++++
6 files changed, 137 insertions(+), 49 deletions(-)

diff --git a/learning/bin/dune b/learning/bin/dune @@ -1,4 +1,4 @@ (executable (public_name learning) (name main) - (libraries dream jingoo learning)) + (libraries learning)) diff --git a/learning/bin/main.ml b/learning/bin/main.ml @@ -1,51 +1,7 @@ open Learning -(* TODO: Make this a whitelist based on the page / course lists *) -(* TODO: Favicon *) - -let is_safe_path name = - String.length name > 0 - && name <> "." - && name <> ".." - && String.for_all (fun c -> - (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') - || (c >= '0' && c <= '9') || c = '-' || c = '_' || c = '.') name - -(* TODO: Ensure server responds correctly to invalid requests. *) -(* - Examples: Non-existent course - Examples: Non-existent page - Examples: Non-existent asset - Examples: Unknown path -*) - -let my_error_template _error debug_info suggested_response = - let status = Dream.status suggested_response in - let code = Dream.status_to_int status - and reason = Dream.status_to_string status in - Dream.set_header suggested_response "Content-Type" Dream.text_html; - Dream.set_body suggested_response ("<h1>" ^ (string_of_int code) ^ " - " ^ reason ^ "</h1>"); - Lwt.return suggested_response - - -let () = - Dream.run ~error_handler:(Dream.error_template my_error_template) +let () = + Dream.run ~error_handler:(Server.handler) @@ Dream.logger - @@ Dream.router [ - Dream.get "/course/:course_name/" (fun request -> Dream.html (Render.render_sidebar @@ Render.render_course_home - (if is_safe_path (Dream.param request "course_name") then Dream.param request "course_name" else ""))); - - Dream.get "/course/:course_name/page/:page_name" (fun request -> Dream.html @@ Render.render_sidebar @@ Render.render_page - (if is_safe_path (Dream.param request "course_name") then Dream.param request "course_name" else "") - - (if is_safe_path (Dream.param request "page_name") then (Dream.param request "page_name") else "")); - - Dream.get "/static/**" (Dream.static "./static"); - Dream.get "/" (fun _ -> Dream.html (Render.render_sidebar @@ Render.render_course_list)); - Dream.get "/robots.txt" (Dream.from_filesystem "./static" "robots.txt"); + @@ Dream.router Server.routes - Dream.get "/course/:course_name/page/assets/**" (fun request -> Dream.static - (Shared.course_dir ^ - (if is_safe_path (Dream.param request "course_name") then Dream.param request "course_name" else "") - ^ "/assets") request); - ] diff --git a/learning/lib/dune b/learning/lib/dune @@ -1,3 +1,3 @@ (library (name learning) - (libraries jingoo)) + (libraries jingoo dream)) diff --git a/learning/lib/server.ml b/learning/lib/server.ml @@ -0,0 +1,93 @@ +(* TODO: Make this a whitelist based on the page / course lists *) +(* TODO: Favicon *) + +let is_file filename = + if Sys.file_exists filename then + true + else + false + +let is_directory path = + if Sys.file_exists path then + (if Sys.is_directory path then + true + else + false) + else + false + +let is_safe_path name = + String.length name > 0 + && name <> "." + && name <> ".." + && String.for_all (fun c -> + (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') || c = '-' || c = '_' || c = '.') name + +(* TODO: Ensure server responds correctly to invalid requests. *) +(* + Examples: Non-existent course + Examples: Non-existent page + Examples: Non-existent asset + Examples: Unknown path +*) + +let my_error_template _error debug_info suggested_response = + let status = Dream.status suggested_response in + let code = Dream.status_to_int status + and reason = Dream.status_to_string status in + Dream.set_header suggested_response "Content-Type" Dream.text_html; + Dream.set_body suggested_response ("<h1>" ^ (string_of_int code) ^ " - " ^ reason ^ "</h1>"); + Lwt.return suggested_response + +let get_course_name request = + Dream.param request "course_name" + + +(* TODO: When should trailing slash be in a url? *) +(* TODO: If sticking with course_name/ then course_name ^ "/" redirect *) + +let is_valid_course name = + let pth = Shared.course_dir ^ name in + (is_safe_path name && is_directory pth) + +let routes = [ + Dream.get "/course/:course_name/" (fun request -> + let name = get_course_name request in + if is_valid_course name then + Dream.html ( + Render.render_sidebar @@ Render.render_course_home (name) + ) + else + Dream.empty `Not_Found + ); + + Dream.get "/course/:course_name/page/:page_name" (fun request -> + let name = get_course_name request in + if is_valid_course name then + Dream.html @@ Render.render_sidebar @@ Render.render_page name (if is_safe_path (Dream.param request "page_name") then (Dream.param request "page_name") else "") + + else + Dream.empty `Not_Found + ); + + + + Dream.get "/static/**" (Dream.static "./static"); + Dream.get "/" (fun _ -> Dream.html (Render.render_sidebar @@ Render.render_course_list)); + Dream.get "/robots.txt" (Dream.from_filesystem "./static" "robots.txt"); + + Dream.get "/course/:course_name/page/assets/**" (fun request -> + + let name = get_course_name request in + if is_valid_course name then + Dream.static + (Shared.course_dir ^ name + ^ "/assets") request + else + Dream.empty `Not_Found + ); + +] + +let handler = Dream.error_template my_error_template diff --git a/learning/test/test_learning.ml b/learning/test/test_learning.ml @@ -1,2 +1,3 @@ let () = Test_line_parsing.test_line_parsing () let () = Test_rendering.test_rendering () +let () = Test_server.test_server () diff --git a/learning/test/test_server.ml b/learning/test/test_server.ml @@ -0,0 +1,38 @@ +open Learning + +(*main.ml server definition: + +let () = + Dream.run ~error_handler:(Server.handler) + @@ Dream.logger + @@ Dream.router Server.routes + +*) + +let test_server = + Dream.test + @@ Dream.router Server.routes + + +let root_response = test_server (Dream.request ~target:"/" "") +let course_0_response = test_server (Dream.request ~target:"/course/course_0/" "") +let course_1_response = test_server (Dream.request ~target:"/course/course_1/" "") +let course_dne_response = test_server (Dream.request ~target:"/course/this_course_doesnt_exist/" "") + + + +let test_server () = + assert (not (Server.is_safe_path "../")); + assert (not (Server.is_safe_path "..")); + assert (not (Server.is_safe_path "smt/../../../")); + assert (not (Server.is_safe_path "smt/../")); + assert (Server.is_safe_path "smt.sl"); + assert (Server.is_safe_path "smt"); + assert (Server.is_safe_path "0_course_1.sl"); + assert (Server.is_safe_path "1_course_1.sl"); + + (* Requests to test dream server handling routes. *) + assert (Dream.status_to_int (Dream.status root_response) = 200); + assert (Dream.status_to_int (Dream.status course_0_response) = 200); + assert (Dream.status_to_int (Dream.status course_1_response) = 200); + assert (Dream.status_to_int (Dream.status course_dne_response) = 404);