From 9917a9882eff675567007194661df57450016c89 Mon Sep 17 00:00:00 2001 From: Jeffrey Armstrong Date: Fri, 6 May 2022 12:53:08 -0400 Subject: Login and session tracking now works via Gemini. Need to restrict operations in Gemini based on request auth levels still. --- captain/api.f90 | 2 + captain/codes.f90 | 23 ++++ captain/external.f90 | 276 +++++++++++++++++++++++++++++++++++------ captain/gemini.f90 | 39 ++++-- captain/http.f90 | 6 - captain/levitating-captain.prj | 3 + captain/requtils.f90 | 73 +++++++---- captain/response.f90 | 149 ++++++++++++++++++++-- captain/templates/index.gmi | 16 +-- captain/web.f90 | 31 ++--- 10 files changed, 500 insertions(+), 118 deletions(-) create mode 100644 captain/codes.f90 diff --git a/captain/api.f90 b/captain/api.f90 index c05b0ba..f75a257 100644 --- a/captain/api.f90 +++ b/captain/api.f90 @@ -89,6 +89,7 @@ contains use special_filenames use logging use security, only: validate_query_token + use gemini_codes implicit none type(request), intent(in)::req @@ -202,6 +203,7 @@ contains use special_filenames use logging use security, only: validate_token => validate_titan_token + use gemini_codes implicit none type(titan_request), intent(in)::req diff --git a/captain/codes.f90 b/captain/codes.f90 new file mode 100644 index 0000000..be6c07e --- /dev/null +++ b/captain/codes.f90 @@ -0,0 +1,23 @@ +module http_codes +implicit none + + integer, parameter::HTTP_CODE_SUCCESS = 200 + integer, parameter::HTTP_CODE_NOTFOUND = 404 + integer, parameter::HTTP_CODE_FAILURE = 500 + integer, parameter::HTTP_CODE_REDIRECT = 302 + integer, parameter::HTTP_CODE_UNAUTHORIZED = 401 + +end module http_codes + +module gemini_codes +implicit none + + integer, parameter::GEMINI_CODE_INPUT = 10 + integer, parameter::GEMINI_CODE_INPUT_PW = 11 + integer, parameter::GEMINI_CODE_SUCCESS = 20 + integer, parameter::GEMINI_CODE_REDIRECT = 30 + integer, parameter::GEMINI_CODE_TEMPFAIL = 40 + integer, parameter::GEMINI_CODE_PERMFAIL = 50 + integer, parameter::GEMINI_CODE_BAD_REQUEST = 59 + +end module gemini_codes \ No newline at end of file diff --git a/captain/external.f90 b/captain/external.f90 index 0cdcfdf..4c700ae 100644 --- a/captain/external.f90 +++ b/captain/external.f90 @@ -80,7 +80,8 @@ contains use server_response use special_filenames, only: get_task_result_static_filename use request_utils, only: get_status_utf8, get_player_link => player_link, & - get_instruction_link => instruction_link + get_instruction_link => instruction_link, & + build_link implicit none type(request), intent(in)::req @@ -95,7 +96,7 @@ contains type(task), dimension(:), pointer::tasks character(32)::task_text, job_text character(len=:), pointer::task_results_filename - character(len=:), pointer::player_link, instruction_link + character(len=:), pointer::player_link, instruction_link, result_link res => null() @@ -135,8 +136,11 @@ contains write(task_text, '(I8)') i - res = trim(res)//nl//"=> /results/"//task_results_filename//" "// & - trim(status)//" - Task "//trim(adjustl(task_text)) + result_link => build_link("/results/"//task_results_filename, & + trim(status)//" - Task "//trim(adjustl(task_text)), & + .true., req%token) + res = trim(res)//nl//result_link + deallocate(result_link) end do deallocate(tasks) @@ -158,6 +162,7 @@ contains use utilities use server_response use config + use request_utils, only: build_link implicit none type(request), intent(in)::req @@ -169,6 +174,7 @@ contains integer::allocation_size, i character(1)::nl = new_line(' ') character(4)::folder_icon = char(240)//char(159)//char(147)//char(129) + character(len=:), pointer::release_link if(.not. associated(req%query_string)) then public_path = "/releases" @@ -204,10 +210,15 @@ contains if(trim(public_path) /= "/releases") then i = index(req%query_string, "/", back=.true.) if(i > 0) then - res = trim(res)//nl//"=> /releases.gmi?"//req%query_string(1:(i-1))//" Up a directory" + release_link => build_link("/releases.gmi?"//req%query_string(1:(i-1)), & + " Up a directory", .true., req%token) else - res = trim(res)//nl//"=> /releases.gmi Up a directory" + release_link => build_link("/releases.gmi?"//req%query_string(1:(i-1)), & + " Up a directory", .true., req%token) + end if + res = trim(res)//nl//release_link + deallocate(release_link) end if if(associated(directories)) then @@ -219,8 +230,13 @@ contains else subpath = trim(directories(i)) end if + + release_link => build_link("/releases.gmi?"//trim(subpath), & + folder_icon//" "//trim(directories(i)), & + .true., req%token) - res = trim(res)//nl//"=> /releases.gmi?"//trim(subpath)//" "//folder_icon//" "//trim(directories(i)) + res = trim(res)//nl//release_link + deallocate(release_link) end do deallocate(directories) @@ -231,7 +247,9 @@ contains do i = 1, size(files) call combine_paths(public_path, trim(files(i)), subpath) - res = trim(res)//nl//"=> "//trim(subpath)//" "//trim(files(i)) + release_link => build_link(trim(subpath), trim(files(i)), .true., req%token) + res = trim(res)//nl//release_link + deallocate(release_link) end do deallocate(files) @@ -240,17 +258,19 @@ contains end function generate_releases_gemini - function generate_players_gemini() result(res) + function generate_players_gemini(req) result(res) use captain_db - use request_utils, only: get_player_status_utf8 + use request_utils, only: get_player_status_utf8, build_link + use server_response, only: request implicit none + type(request), intent(in)::req character(len=:), pointer::res character(len=PLAYER_NAME_LENGTH), dimension(:), pointer::players character(4)::player_status integer::n, i, nsize - character(len=3*PLAYER_NAME_LENGTH)::one_player + character(len=:), pointer::player_link n = get_player_count() if(n == 0) then @@ -274,27 +294,34 @@ contains do i = 1, n player_status = get_player_status_utf8(players(i)) - one_player = "=> /players/"//trim(players(i))//".gmi "//trim(player_status)//" "//trim(players(i)) + player_link => build_link("/players/"//trim(players(i))//".gmi", & + trim(player_status)//" "//trim(players(i)), & + .true., req%token) + if(i == 1) then - res = trim(res)//new_line(res(1:1))//new_line(res(1:1))//trim(one_player) + res = trim(res)//new_line(res(1:1))//new_line(res(1:1))//player_link else - res = trim(res)//new_line(res(1:1))//trim(one_player) + res = trim(res)//new_line(res(1:1))//player_link end if + deallocate(player_link) end do deallocate(players) end if + player_link => build_link("/players/add.gmi", "Add Player", .true., req%token) + res = trim(res)//new_line(res(1:1))//new_line(res(1:1))//"## Management"// & - new_line(res(1:1))//"=> /players/add.gmi Add Player" + new_line(res(1:1))//player_link + + deallocate(player_link) end function generate_players_gemini function generate_one_instuction_gemini(req) result(res) use captain_db use server_response - use request_utils, only: get_player_status_utf8 - use request_utils, only: render_jobs_links + use request_utils, only: get_player_status_utf8, render_jobs_links, build_link implicit none type(request)::req @@ -307,7 +334,7 @@ contains character(len=PLAYER_NAME_LENGTH), dimension(:), pointer::all_players integer::i, j, n_jobs, n_players, nsize - character(len=:), pointer::job_link_text + character(len=:), pointer::job_link_text, raw_link, launch_link, assign_link character(1)::nl = new_line(' ') character(PLAYER_NAME_LENGTH)::player_name character(4)::player_status @@ -348,7 +375,10 @@ contains res = nl//"## "//trim(instruction_name) i = index(req%location, ".gmi", back=.true.) - res = trim(res)//nl//"=> "//req%location(1:i-1)//".json View Raw" + + raw_link => build_link(req%location(1:i-1)//".json", "View Raw", .true., req%token) + res = trim(res)//nl//raw_link + deallocate(raw_link) if(n_players == 0) then res = trim(res)//nl//nl//"No players currently can run these instructions" @@ -357,8 +387,13 @@ contains do i = 1, n_players call get_player_name(players(i), player_name) player_status = get_player_status_utf8(players(i)) - res = trim(res)//nl//"=> "//trim(req%location)//"?launch="//trim(player_name)// & - " "//trim(player_status)//" "//trim(player_name) + + launch_link => build_link(trim(req%location)//"?launch="//trim(player_name), & + trim(player_status)//" "//trim(player_name), & + .true., req%token) + + res = trim(res)//nl//launch_link + deallocate(launch_link) end do end if @@ -380,8 +415,13 @@ contains cycle end if end if - res = trim(res)//nl//"=> "//trim(req%location)//"?assign="//trim(all_players(i))// & - " "//trim(all_players(i)) + + assign_link => build_link(trim(req%location)//"?assign="//trim(all_players(i)), & + trim(all_players(i)), .true., req%token) + + res = trim(res)//nl//assign_link + + deallocate(assign_link) end do deallocate(all_players) end if @@ -390,22 +430,31 @@ contains res = trim(res)//nl//nl//"### Remove"//nl//"Remove a player from these instructions" do i = 1, n_players call get_player_name(players(i), player_name) - res = trim(res)//nl//"=> "//trim(req%location)//"?remove="//trim(player_name)// & - " "//trim(player_name) + + assign_link => build_link(trim(req%location)//"?remove="//trim(player_name), & + trim(player_name), .true., req%token) + + res = trim(res)//nl//assign_link + + deallocate(assign_link) end do end if end function generate_one_instuction_gemini - function generate_instructions_gemini() result(res) + function generate_instructions_gemini(req) result(res) use captain_db + use server_response, only: request + use request_utils, only: build_link implicit none + class(request), intent(in)::req character(len=:), pointer::res character(len=PLAYER_NAME_LENGTH), dimension(:), pointer::instruction_names integer::n, i, nsize character(len=3*PLAYER_NAME_LENGTH)::one_player + character(len=:), pointer::instruction_link n = get_instructions_count() @@ -426,23 +475,73 @@ contains res = "## Instructions" do i = 1, n - one_player = "=> /instructions/"//trim(instruction_names(i))//".gmi "//trim(instruction_names(i)) + + instruction_link => build_link("/instructions/"//trim(instruction_names(i))//".gmi", & + trim(instruction_names(i)), .true., req%token) + if(i == 1) then - res = trim(res)//new_line(res(1:1))//new_line(res(1:1))//trim(one_player) + res = trim(res)//new_line(res(1:1))//new_line(res(1:1))//instruction_link else - res = trim(res)//new_line(res(1:1))//trim(one_player) + res = trim(res)//new_line(res(1:1))//instruction_link end if + + deallocate(instruction_link) end do deallocate(instruction_names) end if + instruction_link => build_link("/instructions/scan.gmi", "Scan for Instructions", & + .true., req%token) + res = trim(res)//new_line(res(1:1))//new_line(res(1:1))//"## Management"// & - new_line(res(1:1))//"=> /instructions/scan.gmi Scan for Instructions" + new_line(res(1:1))//instruction_link + + deallocate(instruction_link) end function generate_instructions_gemini + function generate_profile_gemini(req) result(res) + use server_response, only: request + use request_utils, only: build_link + use captain_db + implicit none + + class(request), intent(in)::req + + character(len=:), pointer::res + character(len=:), pointer::link + character(len=128)::username + + if(.not. associated(req%token)) then + + allocate(character(len=128)::res) + res = "Not currently logged in."//new_line(' ')//"=> /login.gmi Login" + return + + else if(.not. is_valid_session_db(req%token)) then + + call destroy_session_db(req%token) + + allocate(character(len=128)::res) + res = "You need to login again."//new_line(' ')//"=> /login.gmi Login" + return + + end if + + allocate(character(len=1024)::res) + call get_session_username_db(req%token, username) + res = "## "//trim(username)//" Profile" + + link => build_link("/logout.gmi", "Logout", .true., req%token) + res = trim(res)//new_line(' ')//link + deallocate(link) + + res = trim(res)//new_line(' ')//new_line(' ')//"More to come soon!" + + end function generate_profile_gemini + pure function is_input_provided_request(req) use server_response, only: request implicit none @@ -454,15 +553,21 @@ contains end function is_input_provided_request - pure function is_input_required_request(req) + function is_input_required_request(req) use server_response, only: request implicit none class(request), intent(in)::req logical::is_input_required_request + character(64)::first + + call req%path_component(1, first) is_input_required_request = .false. - if(req%location == "/players/add.gmi") then + if(req%location == "/players/add.gmi" .or. & + req%location == "/login.gmi" .or. & + trim(first) == "login") & + then is_input_required_request = .true. end if @@ -470,15 +575,25 @@ contains function external_input_required_gemini(req) result(resp) use server_response + use gemini_codes implicit none class(request), intent(in)::req type(response)::resp + character(64)::first + + call req%path_component(1, first) + resp%code = GEMINI_CODE_INPUT if(req%location == "/players/add.gmi") then call resp%set_message("Enter name of new player to add") + else if(req%location == "/login.gmi") then + call resp%set_message("Enter username:") + else if(trim(first) == "login") then + call resp%set_message("Enter password:") + resp%code = GEMINI_CODE_INPUT_PW end if end function external_input_required_gemini @@ -487,18 +602,22 @@ contains use server_response use captain_db use request_utils, only: handle_instruction_command + use m_uuid, only: UUID_LENGTH + use logging + use gemini_codes implicit none class(request), intent(in)::req type(response)::resp - character(64)::first + character(64)::first, second + character(len=:), pointer::session call req%path_component(1, first) if(req%location == "/players/add.gmi") then call add_player_db(req%query_string) resp%code = GEMINI_CODE_REDIRECT - call resp%set_url("/players.gmi") + call resp%set_gemini_session_url("/players.gmi", req%token) else if(req%location == "/jobs.gmi" .or. req%location == "/releases.gmi") then ! Used for paging (jobs) or subdirs (releases) - send it back @@ -511,7 +630,28 @@ contains ! Go back to the same location resp%code = GEMINI_CODE_REDIRECT call resp%set_url(req%location) + + else if(req%location == "/login.gmi" .and. req%has_query()) then + + resp%code = GEMINI_CODE_REDIRECT + call resp%set_url("/login/"//req%query_string//"/password.gmi") + + else if(trim(first) == "login") then + call req%path_component(2, second) + resp%code = GEMINI_CODE_REDIRECT + + call write_log("Attempting to validate "//trim(second)//":"//trim(req%query_string), LOG_DEBUG) + + if(validate_user_db(trim(second), trim(req%query_string))) then + allocate(character(len=UUID_LENGTH)::session) + session = create_user_session_db(trim(second)) + call resp%set_gemini_session_url("/index.gmi", session) + deallocate(session) + else + call resp%set_url("/loginfailed.gmi") + end if + end if end function external_input_request_gemini @@ -524,19 +664,52 @@ contains logical::is_redirect_action is_redirect_action = .false. - if(req%location == "/instructions/scan.gmi") then + if(req%location == "instructions/scan.gmi" .or. & + req%location == "/logout.gmi") & + then is_redirect_action = .true. end if end function is_redirect_action + subroutine handle_basic_gemini_template_components(req, page) + use server_response + use page_template + use captain_db + use config + implicit none + + type(request), intent(in)::req + type(template), intent(inout)::page + character(len=128)::username + + call page%assign('project', project) + + if(req%is_authenticated_user()) then + + call page%assign("session_flag", "/session-"//req%token) + + call page%assign('user_link_page', "/profile.gmi") + call get_session_username_db(req%token, username) + call page%assign('user_link_text', achar(240)//achar(159)//achar(152)//achar(128)//" "//trim(username)) + + else + call page%assign("session_flag", "") + call page%assign('user_link_page', "/login.gmi") + call page%assign('user_link_text', "Login") + + end if + + end subroutine handle_basic_gemini_template_components + function external_redirect_action_request_gemini(req) result(resp) use captain_db use server_response use logging + use gemini_codes implicit none - class(request), intent(in)::req + class(request), intent(inout)::req type(response)::resp resp%code = GEMINI_CODE_REDIRECT @@ -544,6 +717,15 @@ contains if(req%location == "/instructions/scan.gmi") then call scan_instructions_for_db() call resp%set_url("/instructions.gmi") + + else if(req%location == "/logout.gmi") then + + if(associated(req%token)) then + call destroy_session_db(req%token) + call req%clear_token() + end if + call resp%set_url("/index.gmi") + end if end function external_redirect_action_request_gemini @@ -555,6 +737,7 @@ contains use server_response use request_utils, only: get_job_page_title use utilities, only: build_date + use gemini_codes implicit none class(request), intent(in)::req @@ -599,7 +782,7 @@ contains else if(trim(req%location) == "/players.gmi") then call page%assign('title', 'Players') - contents => generate_players_gemini() + contents => generate_players_gemini(req) call page%assign('contents', contents) else if(req%location(1:9) == '/players/') then @@ -612,7 +795,7 @@ contains else if(trim(req%location) == "/instructions.gmi") then call page%assign('title', 'Build Instructions') - contents => generate_instructions_gemini() + contents => generate_instructions_gemini(req) call page%assign('contents', contents) else if(trim(first) == "instructions") then @@ -628,14 +811,24 @@ contains contents => generate_one_job_gemini(req) call page%assign('contents', contents) + else if(req%location == "/profile.gmi") then + + call page%assign('title', "User Profile") + contents => generate_profile_gemini(req) + call page%assign('contents', contents) + + else if(req%location == "/loginfailed.gmi") then + + call page%assign('title', "Login Failed") + call page%assign("contents", "Bad username and/or password.") + else call page%assign('title', 'Not Found') end if - call page%assign('project', project) - + call handle_basic_gemini_template_components(req, page) call write_log("Rendering page for "//req%location) call page%render() @@ -654,7 +847,7 @@ contains use request_utils, only: is_request_static, request_static implicit none - class(request), intent(in)::req + class(request), intent(inout)::req ! inout for logout actions, unfortunately... type(response)::resp if(is_redirect_action(req)) then @@ -687,6 +880,7 @@ contains use query_utilities use security use logging + use gemini_codes implicit none type(titan_request), intent(in)::req diff --git a/captain/gemini.f90 b/captain/gemini.f90 index 6070dbe..7e849d6 100644 --- a/captain/gemini.f90 +++ b/captain/gemini.f90 @@ -26,7 +26,7 @@ implicit none private public :: handle_request - + contains subroutine read_request(ssl, req) @@ -127,7 +127,7 @@ contains use iso_c_binding, only: c_ptr use jessl, only: ssl_write use logging - use server_response, only: GEMINI_CODE_SUCCESS + use gemini_codes implicit none type(c_ptr)::ssl @@ -148,7 +148,7 @@ contains subroutine write_redirect(ssl, url) use iso_c_binding, only: c_ptr - use server_response, only: GEMINI_CODE_REDIRECT + use gemini_codes implicit none type(c_ptr)::ssl @@ -158,21 +158,26 @@ contains end subroutine write_redirect - subroutine write_input_request(ssl, msg) + subroutine write_input_request(ssl, msg, code) use iso_c_binding, only: c_ptr - use server_response, only: GEMINI_CODE_INPUT + use gemini_codes implicit none type(c_ptr)::ssl character(*), intent(in)::msg + integer, intent(in), optional::code - call write_status(ssl, GEMINI_CODE_INPUT, msg) + if(present(code)) then + call write_status(ssl, code, msg) + else + call write_status(ssl, GEMINI_CODE_INPUT, msg) + end if end subroutine write_input_request subroutine write_failure(ssl) use iso_c_binding, only: c_ptr - use server_response, only: GEMINI_CODE_PERMFAIL + use gemini_codes implicit none type(c_ptr)::ssl @@ -211,7 +216,9 @@ contains use external_handling, only: external_request_gemini, external_request_titan use api_handling use logging + use m_uuid, only: UUID_LENGTH use server_response + use gemini_codes implicit none ! For our TLS connection @@ -227,7 +234,7 @@ contains ! Requested file character(1024)::text_request - character(32)::first + character(UUID_LENGTH+8)::first integer::rendered_unit, ioerror @@ -307,6 +314,18 @@ contains end if else + + ! Check for leading session + if(first(1:8) == "session-") then + + ! Set the token in the request to the leading session id + call req%set_token(first(9:len(first))) + + ! Remove the first path component + call req%remove_first_path_component() + + end if + if(req%protocol == "gemini") then resp = external_request_gemini(req) @@ -320,8 +339,8 @@ contains call write_log("Handling response", LOG_DEBUG) ! Handle the response select case(resp%code) - case(GEMINI_CODE_INPUT) - call write_input_request(ssl, resp%message) + case(GEMINI_CODE_INPUT, GEMINI_CODE_INPUT_PW) + call write_input_request(ssl, resp%message, code=resp%code) case(GEMINI_CODE_REDIRECT) call write_redirect(ssl, resp%url) diff --git a/captain/http.f90 b/captain/http.f90 index 8449ba2..61715fd 100644 --- a/captain/http.f90 +++ b/captain/http.f90 @@ -23,12 +23,6 @@ module http implicit none - integer, parameter::HTTP_CODE_SUCCESS = 200 - integer, parameter::HTTP_CODE_NOTFOUND = 404 - integer, parameter::HTTP_CODE_FAILURE = 500 - integer, parameter::HTTP_CODE_REDIRECT = 302 - integer, parameter::HTTP_CODE_UNAUTHORIZED = 401 - contains subroutine write_status(outunit, code) diff --git a/captain/levitating-captain.prj b/captain/levitating-captain.prj index f949fb2..078cccb 100644 --- a/captain/levitating-captain.prj +++ b/captain/levitating-captain.prj @@ -98,6 +98,9 @@ },{ "filename":"captain.f90", "enabled":"1" + },{ + "filename":"codes.f90", + "enabled":"1" },{ "filename":"config.f90", "enabled":"1" diff --git a/captain/requtils.f90 b/captain/requtils.f90 index 620170a..03fe869 100644 --- a/captain/requtils.f90 +++ b/captain/requtils.f90 @@ -41,8 +41,9 @@ implicit none contains pure function success_code(req) - use http, only: HTTP_SUCCESS => HTTP_CODE_SUCCESS - use server_response, only: request, GEMINI_SUCCESS => GEMINI_CODE_SUCCESS + use http_codes, only: HTTP_SUCCESS => HTTP_CODE_SUCCESS + use gemini_codes, only: GEMINI_SUCCESS => GEMINI_CODE_SUCCESS + use server_response, only: request implicit none class(request), intent(in)::req @@ -57,8 +58,9 @@ contains end function success_code pure function notfound_code(req) - use http, only: HTTP_FAIL => HTTP_CODE_NOTFOUND - use server_response, only: request, GEMINI_FAIL => GEMINI_CODE_PERMFAIL + use http_codes, only: HTTP_FAIL => HTTP_CODE_NOTFOUND + use gemini_codes, only: GEMINI_FAIL => GEMINI_CODE_PERMFAIL + use server_response, only: request implicit none class(request), intent(in)::req @@ -73,8 +75,9 @@ contains end function notfound_code pure function notpermitted_code(req) - use http, only: HTTP_UNAUTHORIZED => HTTP_CODE_UNAUTHORIZED - use server_response, only: request, GEMINI_UNAUTHORIZED => GEMINI_CODE_BAD_REQUEST + use http_codes, only: HTTP_UNAUTHORIZED => HTTP_CODE_UNAUTHORIZED + use gemini_codes, only: GEMINI_UNAUTHORIZED => GEMINI_CODE_BAD_REQUEST + use server_response, only: request implicit none class(request), intent(in)::req @@ -285,19 +288,31 @@ contains end function request_static - function build_link(link, label, gemini_mode) result(res) + function build_link(link, label, gemini_mode, session_token) result(res) + use server_response, only: gemini_session_link_url implicit none character(*), intent(in)::link, label logical, intent(in)::gemini_mode + + ! Passed in to prepend a session identifier in gemini + character(len=:), pointer, intent(in), optional::session_token character(len=:), pointer::res + character(len=:), pointer::gemlink integer::nl if(gemini_mode) then - nl = len_trim(link) + len_trim(label) + len('=> ') + if(present(session_token)) then + gemlink => gemini_session_link_url(link, session_token) + else + gemlink => gemini_session_link_url(link, null()) + end if + + nl = len_trim(gemlink) + len_trim(label) + len('=> ') + allocate(character(len=nl)::res) - res = '=> '//trim(link)//' '//trim(label) + res = '=> '//trim(gemlink)//' '//trim(label) else @@ -530,7 +545,7 @@ contains end function generate_simple_pager - function render_jobs_links(jobs, startindex, stopindex, gemini_mode, link_prefix) result(res) + function render_jobs_links(jobs, startindex, stopindex, gemini_mode, link_prefix, session_token) result(res) use captain_db implicit none @@ -538,11 +553,16 @@ contains integer, intent(in), optional::startindex, stopindex logical, intent(in)::gemini_mode character(*), intent(in), optional::link_prefix + character(len=:), intent(in), pointer, optional::session_token character(len=:), pointer::res integer::nsize, i, first, last character(len=16)::int_text character(len=(2*PLAYER_NAME_LENGTH + 64))::link + + character(len=:), pointer::link_pointer + character(len=:), pointer::internal_session_token + character(PLAYER_NAME_LENGTH)::player, instruction character(1)::nl = new_line(' ') character(64)::int_link_prefix @@ -553,6 +573,12 @@ contains int_link_prefix = " " end if + if(present(session_token)) then + internal_session_token => session_token + else + internal_session_token => null() + end if + if(.not. associated(jobs)) then allocate(character(len=32)::res) res = "None Yet" @@ -585,13 +611,17 @@ contains write(int_text, '(I8)') jobs(i)%id if(gemini_mode) then - link = "=> "//trim(int_link_prefix)//"jobs/"// & - trim(adjustl(int_text))//".gmi "// & - trim(get_status_utf8(jobs(i)%status))//" Job "// & - trim(adjustl(int_text))//" - "//trim(instruction) + + link_pointer => build_link(trim(int_link_prefix)//"jobs/"//trim(adjustl(int_text))//".gmi", & + trim(get_status_utf8(jobs(i)%status))//" Job "// & + trim(adjustl(int_text))//" - "//trim(instruction), & + .true., internal_session_token) - res = trim(res)//nl//nl//trim(link) + res = trim(res)//nl//nl//link_pointer + deallocate(link_pointer) + else + res = trim(res)//nl//'
  • ' end select - - link = '

    '// & - trim(get_status_utf8(jobs(i)%status))//" Job "// & - trim(adjustl(int_text))//" - "//trim(instruction)// & - '

    ' + link_pointer => build_link(trim(int_link_prefix)//'jobs/'//trim(adjustl(int_text))//'.html', & + trim(get_status_utf8(jobs(i)%status))//" Job "// & + trim(adjustl(int_text))//" - "//trim(instruction), & + .false., internal_session_token) - res = trim(res)//nl//trim(link)//nl//"

    " + res = trim(res)//nl//"

    "//link_pointer//'

    '//nl//"

    " + deallocate(link_pointer) endif select case(jobs(i)%status) diff --git a/captain/response.f90 b/captain/response.f90 index e172cc1..03d420e 100644 --- a/captain/response.f90 +++ b/captain/response.f90 @@ -25,13 +25,6 @@ use iso_c_binding use query_utilities implicit none - integer, parameter::GEMINI_CODE_INPUT = 10 - integer, parameter::GEMINI_CODE_SUCCESS = 20 - integer, parameter::GEMINI_CODE_REDIRECT = 30 - integer, parameter::GEMINI_CODE_TEMPFAIL = 40 - integer, parameter::GEMINI_CODE_PERMFAIL = 50 - integer, parameter::GEMINI_CODE_BAD_REQUEST = 59 - character(*), parameter::RESPONSE_JSON_OKAY = '{"status": "okay"}' type :: response @@ -55,6 +48,7 @@ implicit none procedure :: destroy => response_destroy procedure :: set_message => response_set_message procedure :: set_url => response_set_url + procedure :: set_gemini_session_url => reponse_set_gemini_session_url procedure :: set_body_contents => response_temp_file_contents procedure :: set_filename => response_set_filename_using_allocation procedure :: set_cookie_cmd => response_set_cookie_cmd @@ -90,6 +84,10 @@ implicit none procedure :: is_get => request_is_get procedure :: is_post => request_is_post procedure :: has_query => request_has_query + procedure :: is_authenticated_user => request_is_authenticated_user + procedure :: set_token => request_set_token + procedure :: remove_first_path_component => request_remove_first_path_component + procedure :: clear_token => request_clear_token end type request @@ -110,6 +108,37 @@ implicit none contains + function gemini_session_link_url(link, session_token) result(res) + implicit none + + character(*), intent(in)::link + character(len=:), pointer, intent(in), optional::session_token + character(len=:), pointer::res + logical::prepend_session + integer::nl + + nl = len_trim(link) + + prepend_session = .false. + if(link(1:1) == "/") then + if(present(session_token)) then + if(associated(session_token)) then + nl = nl + 9 + len_trim(session_token) + prepend_session = .true. + end if + end if + end if + + allocate(character(len=nl)::res) + + if(prepend_session) then + res = "/session-"//trim(session_token)//trim(link) + else + res = link + end if + + end function gemini_session_link_url + subroutine request_init(self, str, server_explicit, protocol_explicit, method, cookiestring) use logging use utilities, only: toupper @@ -269,6 +298,23 @@ contains end function request_component_start_location + function request_is_authenticated_user(self) + use captain_db + implicit none + + class(request), intent(in)::self + logical::request_is_authenticated_user + + request_is_authenticated_user = .false. + if(associated(self%token)) then + request_is_authenticated_user = is_valid_session_db(self%token) + if(request_is_authenticated_user) then + call update_session_db(self%token) + end if + end if + + end function request_is_authenticated_user + subroutine request_component(self, i_component, res) use logging implicit none @@ -414,6 +460,73 @@ contains end function request_has_query + subroutine request_set_token(self, token) + implicit none + + class(request), intent(inout)::self + character(len=*), intent(in)::token + + allocate(character(len=len_trim(token)) :: self%token) + self%token = token + + end subroutine request_set_token + + subroutine request_clear_token(self) + implicit none + + class(request), intent(inout)::self + logical::safe_to_deallocate + + if(associated(self%token)) then + safe_to_deallocate = .not. (associated(self%c%get_value("token"), self%token) .or. & + associated(self%q%get_value("token"), self%token)) + + if(safe_to_deallocate) then + deallocate(self%token) + end if + + self%token => null() + + end if + + end subroutine request_clear_token + + ! This routine is needed if we're stripping session identifiers + ! in Gemini URLs + subroutine request_remove_first_path_component(self) + implicit none + + class(request), intent(inout)::self + character(len=:), pointer::first, newloc + + integer::i, j, n + + i = 1 + if(self%location(1:1) == "/") then + i = i + 1 + end if + + n = len_trim(self%location) + j = index(self%location(i:n), "/") + (i-1) + if(j == 0) then + j = n + end if + + ! First component should be self%location(i:j) + n = n - (j-i+1) + + allocate(character(len=n)::newloc) + if(i > 1) then + newloc(1:1) = self%location(1:1) + end if + + newloc(2:n) = self%location(j+1:len_trim(self%location)) + deallocate(self%location) + self%location => newloc + newloc => null() + + end subroutine request_remove_first_path_component + subroutine request_destroy(self) implicit none @@ -443,6 +556,9 @@ contains deallocate(self%page) end if + ! Needs to happen before we destroy the cookies and query string + call self%clear_token() + call self%q%destroy() call self%c%destroy() @@ -546,6 +662,25 @@ contains end subroutine response_set_url + subroutine reponse_set_gemini_session_url(resp, str, session_token) + !use request_utils, only: gemini_session_link_url + implicit none + + class(response), intent(inout)::resp + character(*), intent(in)::str + character(len=:), pointer, intent(in)::session_token + character(len=:), pointer::session_url + + if(associated(session_token)) then + session_url => gemini_session_link_url(str, session_token) + call resp%set_url(session_url) + deallocate(session_url) + else + call resp%set_url(str) + end if + + end subroutine reponse_set_gemini_session_url + subroutine response_temp_file_contents(resp, str, mimetype) use utilities, only: generate_temporary_filename implicit none diff --git a/captain/templates/index.gmi b/captain/templates/index.gmi index bbcd8d2..4c4625a 100644 --- a/captain/templates/index.gmi +++ b/captain/templates/index.gmi @@ -1,14 +1,14 @@ - # {{ title }} - I'm Levitating! The Levitating Captain interface for the {{ project }} project. -=> /index.gmi Home -=> /releases.gmi Releases -=> /jobs.gmi Jobs -=> /players.gmi Players -=> /instructions.gmi Instructions -=> /about.gmi About +=> {{ session_flag }}/index.gmi Home +=> {{ session_flag }}/releases.gmi Releases +=> {{ session_flag }}/jobs.gmi Jobs +=> {{ session_flag }}/players.gmi Players +=> {{ session_flag }}/instructions.gmi Instructions +=> {{ session_flag }}/about.gmi About +=> {{ session_flag }}{{ user_link_page }} {{ user_link_text }} {{ contents }} @@ -17,4 +17,4 @@ The Levitating Captain interface for the {{ project }} project. ------------------------------------------------------------------------------- ``` -Copyright 2021 Approximatrix, LLC +Copyright 2022 Approximatrix, LLC diff --git a/captain/web.f90 b/captain/web.f90 index 9d8fc3c..4242083 100644 --- a/captain/web.f90 +++ b/captain/web.f90 @@ -51,25 +51,7 @@ contains end if end function method - - function request_is_authenticated(req) - use captain_db - use server_response - implicit none - - class(request), intent(in)::req - logical::request_is_authenticated - - request_is_authenticated = .false. - if(associated(req%token)) then - request_is_authenticated = is_valid_session_db(req%token) - if(request_is_authenticated) then - call update_session_db(req%token) - end if - end if - - end function request_is_authenticated - + subroutine echo_error_stdout(error_code) implicit none @@ -95,7 +77,7 @@ contains call page%assign('project', project) call page%assign('base_url', req%server) - if(request_is_authenticated(req)) then + if(req%is_authenticated_user()) then call page%assign('user_link_page', "profile") call get_session_username_db(req%token, username) @@ -1011,7 +993,7 @@ contains use config, only: template_filepath, project use logging use server_response, only:request, response - use http, only: HTTP_CODE_SUCCESS, HTTP_CODE_NOTFOUND + use http_codes, only: HTTP_CODE_SUCCESS, HTTP_CODE_NOTFOUND use request_utils, only: get_job_page_title, handle_instruction_command use captain_db, only: scan_instructions_for_db, get_session_username_db, destroy_session_db use utilities, only: build_date @@ -1031,7 +1013,7 @@ contains integer::i logical::authenticated - authenticated = request_is_authenticated(req) + authenticated = req%is_authenticated_user() if(trim(req%location) == "/" .or. & trim(req%location) == "/index.html" .or. & @@ -1211,9 +1193,9 @@ contains use config, only: template_filepath, global_permissions use logging use server_response, only:request, response - use http, only: HTTP_CODE_FAILURE, HTTP_CODE_SUCCESS use http_post_utilities use query_utilities, only: query + use http_codes implicit none type(request), intent(in)::req @@ -1300,7 +1282,8 @@ contains use server_response, only:request, response use logging use request_utils - use http + use http_codes + use http, only: write_response_headers, write_redirect use iso_fortran_env, only: output_unit use utilities, only: echo_file_stdout implicit none -- cgit v1.2.3