From 4392961dd95582b91e173f9ae40ac510b9afe7d4 Mon Sep 17 00:00:00 2001 From: Jeffrey Armstrong Date: Mon, 11 Apr 2022 16:28:43 -0400 Subject: Added token validation to all api calls, esp. checkins. Changed status reports to use better query structure. Added query derived types to the request derived types directly. Requires testing of actual builds. --- captain/api.f90 | 127 ++++++++++++++++++++++++++++--------------------- captain/queryutils.f90 | 72 ++++++++++++++++------------ captain/response.f90 | 37 +++++++++++--- captain/security.f90 | 39 +++++++++++++++ player/endpoints.f90 | 30 +++++++++++- 5 files changed, 212 insertions(+), 93 deletions(-) diff --git a/captain/api.f90 b/captain/api.f90 index 665ff8d..c05b0ba 100644 --- a/captain/api.f90 +++ b/captain/api.f90 @@ -59,20 +59,24 @@ contains class(request)::req integer::job_i, task_i - if(associated(req%query_string)) then + character(len=:), pointer::status + + status => req%q%get_value("status") + + if(associated(status)) then job_i = req%path_component_int(5) task_i = req%path_component_int(7) - call write_log("Task Update Is "//trim(req%query_string), LOG_DEBUG) - if(req%query_string == "starting") then + call write_log("Task Update Is "//trim(status), LOG_DEBUG) + if(status == "starting") then call write_log("Inserting task", LOG_DEBUG) call insert_task(job_i, task_i) call update_job_status(job_i, JOB_STATUS_WORKING) - else if(req%query_string == "inprogress") then + else if(status == "inprogress") then call update_task_status(job_i, task_i, JOB_STATUS_WORKING) call update_job_status(job_i, JOB_STATUS_WORKING) - else if(req%query_string == "complete") then + else if(status == "complete") then call update_task_status(job_i, task_i, JOB_STATUS_SUCCESS) - else if(req%query_string == "failed") then + else if(status == "failed") then call update_task_status(job_i, task_i, JOB_STATUS_FAILURE) end if end if @@ -84,7 +88,7 @@ contains use captain_db use special_filenames use logging - use query_utilities + use security, only: validate_query_token implicit none type(request), intent(in)::req @@ -94,73 +98,88 @@ contains integer::job_i, player_i, qs_platform_index character(len=:), pointer::checkin_work_json - type(query)::q ! Complete - "/api/player/{name}/job/{jobid}/complete" ! Failed - "/api/player/{name}/job/{jobid}/failed" ! Task - "/api/player/{name}/job/{jobid}/task/{task num}" if(trim(req%component(2)) == "player" .and. trim(req%component(4)) == "job") then - job_i = req%path_component_int(5) - call write_log("Job "//trim(req%component(5))//" update arrived", LOG_INFO) - write(player, *) job_i - - if(.not. is_final_job_status(job_i)) then - if(trim(req%component(6)) == "complete") then - call update_job_status(job_i, JOB_STATUS_SUCCESS) - else if(trim(req%component(6)) == "failure") then - call update_job_status(job_i, JOB_STATUS_FAILURE) + if(validate_query_token(req%q%get_value("token"), req%component(3))) then + + job_i = req%path_component_int(5) + call write_log("Job "//trim(req%component(5))//" update arrived", LOG_INFO) + + write(player, *) job_i + + if(.not. is_final_job_status(job_i)) then + if(trim(req%component(6)) == "complete") then + call update_job_status(job_i, JOB_STATUS_SUCCESS) + else if(trim(req%component(6)) == "failure") then + call update_job_status(job_i, JOB_STATUS_FAILURE) + end if end if - end if + + if(trim(req%component(6)) == "task") then + call write_log("Task update encountered", LOG_INFO) + call handle_task_request(req) + end if + + resp%code = GEMINI_CODE_SUCCESS + call resp%set_body_contents(RESPONSE_JSON_OKAY) + resp%body_mimetype = "text/plain" + + else - if(trim(req%component(6)) == "task") then - call write_log("Task update encountered", LOG_INFO) - call handle_task_request(req) + resp%code = GEMINI_CODE_BAD_REQUEST + end if - - resp%code = GEMINI_CODE_SUCCESS - call resp%set_body_contents(RESPONSE_JSON_OKAY) - resp%body_mimetype = "text/plain" ! Checkin - /api/player/{name}/checkin.json else if(trim(req%component(2)) == "player" .and. trim(req%component(4)) == "checkin.json") then - ! Check for pending jobs - call req%path_component(3, player) - player_i = get_player_id(player) - - ! If we have a checkin, but the worker should have a job in progress, mark - ! the jobs as failed. - call mark_working_jobs_as_failed(player_i) + + if(validate_query_token(req%q%get_value("token"), req%component(3))) then - ! Acknowledge the checkin in the database - if(associated(req%query_string)) then - call q%init(req%query_string) - if(associated(q%get_value("platform"))) then - call acknowledge_checkin(player_i, q%get_value("platform")) + ! Check for pending jobs + call req%path_component(3, player) + player_i = get_player_id(player) + + ! If we have a checkin, but the worker should have a job in progress, mark + ! the jobs as failed. + call mark_working_jobs_as_failed(player_i) + + ! Acknowledge the checkin in the database + if(associated(req%query_string)) then + if(associated(req%q%get_value("platform"))) then + call acknowledge_checkin(player_i, req%q%get_value("platform")) + else + call acknowledge_checkin(player_i) + end if else call acknowledge_checkin(player_i) end if - call q%destroy() - else - call acknowledge_checkin(player_i) - end if - - job_i = get_pending_job_for_player(player_i) - if(job_i < 0) then - resp%code = GEMINI_CODE_SUCCESS - call resp%set_body_contents(RESPONSE_JSON_IDLE) - else - checkin_work_json => build_job_available_json(job_i) - if(associated(checkin_work_json)) then + + job_i = get_pending_job_for_player(player_i) + if(job_i < 0) then resp%code = GEMINI_CODE_SUCCESS - call write_log("Sending: "//trim(checkin_work_json), LOG_DEBUG) - call resp%set_body_contents(trim(checkin_work_json), "text/gemini") - deallocate(checkin_work_json) + call resp%set_body_contents(RESPONSE_JSON_IDLE) else - resp%code = GEMINI_CODE_PERMFAIL + checkin_work_json => build_job_available_json(job_i) + if(associated(checkin_work_json)) then + resp%code = GEMINI_CODE_SUCCESS + call write_log("Sending: "//trim(checkin_work_json), LOG_DEBUG) + call resp%set_body_contents(trim(checkin_work_json), "text/gemini") + deallocate(checkin_work_json) + else + resp%code = GEMINI_CODE_PERMFAIL + end if end if - end if + else + + resp%code = GEMINI_CODE_BAD_REQUEST + + end if + ! Instruction - /api/instructions/{name} else if(trim(req%component(2)) == "instruction") then diff --git a/captain/queryutils.f90 b/captain/queryutils.f90 index 00a914b..c6b612b 100644 --- a/captain/queryutils.f90 +++ b/captain/queryutils.f90 @@ -36,7 +36,7 @@ implicit none end type query_component type :: query - + character(len=:), pointer::full type(query_component), dimension(:), pointer::components @@ -133,42 +133,54 @@ contains implicit none class(query), intent(out)::self - character(len=*), intent(in)::str + character(len=*), intent(in), optional::str character(64)::msg integer::ampersands, i, i_end, i_comp, n - n = len_trim(str) - allocate(character(len=n) :: self%full) - self%full = str - - ampersands = 0 - do i = 1, len(self%full) - if(self%full(i:i) == '&') then - ampersands = ampersands + 1 - end if - end do - - allocate(self%components(ampersands + 1)) + self%components => null() + self%full => null() - ! Split and parse each component - if(ampersands == 0) then - call self%components(1)%parse(self%full) + if(present(str)) then + n = len_trim(str) else - i_comp = 1 - i = 1 - i_end = index(self%full, '&') - do while(i_comp < ampersands + 1) - call self%components(i_comp)%parse(self%full(i:i_end-1)) - i = i_end + 1 - do i_end = i, len_trim(self%full) - if(self%full(i_end:i_end) == '&') then - exit - end if - end do - i_comp = i_comp + 1 + n = 0 + end if + + if(n > 0) then + + allocate(character(len=n) :: self%full) + self%full = str + + ampersands = 0 + do i = 1, len(self%full) + if(self%full(i:i) == '&') then + ampersands = ampersands + 1 + end if end do - call self%components(i_comp)%parse(self%full(i:i_end-1)) + + allocate(self%components(ampersands + 1)) + + ! Split and parse each component + if(ampersands == 0) then + call self%components(1)%parse(self%full) + else + i_comp = 1 + i = 1 + i_end = index(self%full, '&') + do while(i_comp < ampersands + 1) + call self%components(i_comp)%parse(self%full(i:i_end-1)) + i = i_end + 1 + do i_end = i, len_trim(self%full) + if(self%full(i_end:i_end) == '&') then + exit + end if + end do + i_comp = i_comp + 1 + end do + call self%components(i_comp)%parse(self%full(i:i_end-1)) + end if + end if end subroutine query_init diff --git a/captain/response.f90 b/captain/response.f90 index 2659c31..3933eb8 100644 --- a/captain/response.f90 +++ b/captain/response.f90 @@ -22,13 +22,15 @@ module server_response 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_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"}' @@ -64,7 +66,10 @@ implicit none character(len=:), pointer::location => null() character(len=:), pointer::page => null() character(len=:), pointer::query_string => null() + character(len=:), pointer::token => null() character(len=4)::method = "GET" + + type(query)::q contains @@ -77,14 +82,14 @@ implicit none procedure :: component => request_component_func procedure :: is_get => request_is_get procedure :: is_post => request_is_post - + procedure :: has_query => request_has_query + end type request type, extends(request) :: titan_request integer(kind=8)::size character(len=:), pointer::mimetype - character(len=:), pointer::token type(c_ptr)::ssl_connection @@ -197,6 +202,12 @@ contains call toupper(self%method) end if + if(associated(self%query_string)) then + call self%q%init(self%query_string) + else + call self%q%init() + end if + end subroutine request_init function request_component_start_location(self, i_component) result(res) @@ -365,6 +376,16 @@ contains end function request_is_post + function request_has_query(self) + implicit none + + class(request)::self + logical::request_has_query + + request_has_query = associated(self%query_string) .and. self%q%component_count() > 0 + + end function request_has_query + subroutine request_destroy(self) implicit none @@ -394,6 +415,8 @@ contains deallocate(self%page) end if + call self%q%destroy() + end subroutine request_destroy subroutine response_destroy(resp) diff --git a/captain/security.f90 b/captain/security.f90 index 2f5fa4c..44d40a6 100644 --- a/captain/security.f90 +++ b/captain/security.f90 @@ -104,4 +104,43 @@ contains end function validate_titan_token + ! NOTE: A null() token can be passed, and it might even validate! + function validate_query_token(token, player) + use captain_db + implicit none + + character(len=:), pointer::token + character(len=*), intent(in)::player + + logical::validate_query_token + + character(len=:), pointer::dbtoken + + validate_query_token = .false. + + if(associated(token)) then + allocate(character(len=len(token))::dbtoken) + else + allocate(character(len=64)::dbtoken) + end if + + dbtoken = ' ' + + call get_player_token_db(player, dbtoken) + + ! If no token is provided and none is in the db, then we're okay + if((.not. associated(token)) .and. len_trim(dbtoken) == 0) then + + validate_query_token = .true. + + else if(associated(token)) then + + validate_query_token = (trim(token) == trim(dbtoken)) + + end if + + deallocate(dbtoken) + + end function validate_query_token + end module security \ No newline at end of file diff --git a/player/endpoints.f90 b/player/endpoints.f90 index 09d3faa..8708592 100644 --- a/player/endpoints.f90 +++ b/player/endpoints.f90 @@ -57,6 +57,27 @@ contains end subroutine base_url + subroutine append_query_token(url) + use config, only: token + implicit none + + character(len=*), intent(inout)::url + character::prepend + + if(len_trim(token) > 0) then + + if(index(url, "?") > 0) then + prepend = "&" + else + prepend = "?" + end if + + url = trim(url)//prepend//"token="//trim(token) + + end if + + end subroutine append_query_token + subroutine get_check_in_url(res) use config use utilities, only: replace_field @@ -66,6 +87,7 @@ contains call base_url(captain, LOCATION_CHECK_IN, .false., res) call replace_field(res, "name", identity) + call append_query_token(res) end subroutine get_check_in_url @@ -92,9 +114,11 @@ contains call replace_field(url, "step", step) if(present(status)) then - url = trim(url)//"?"//trim(status_text(status)) + url = trim(url)//"?status="//trim(status_text(status)) end if - + + call append_query_token(url) + end subroutine get_status_url subroutine get_job_report_url(job, success, res) @@ -113,6 +137,8 @@ contains end if call replace_field(res, "name", identity) call replace_field(res, "jobid", job) + + call append_query_token(res) end subroutine get_job_report_url -- cgit v1.2.3