From 7d9d627d6efec748fd6de2a162e2cb5a0b1685e8 Mon Sep 17 00:00:00 2001 From: Jeffrey Armstrong Date: Fri, 29 May 2020 12:20:55 -0400 Subject: Initial code commit, and it actually works --- main.f90 | 551 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 551 insertions(+) create mode 100644 main.f90 (limited to 'main.f90') diff --git a/main.f90 b/main.f90 new file mode 100644 index 0000000..0a21e5a --- /dev/null +++ b/main.f90 @@ -0,0 +1,551 @@ +! Copyright (c) 2020 Jeffrey Armstrong +! +! Permission is hereby granted, free of charge, to any person obtaining a copy +! of this software and associated documentation files (the "Software"), to deal +! in the Software without restriction, including without limitation the rights +! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +! copies of the Software, and to permit persons to whom the Software is +! furnished to do so, subject to the following conditions: +! +! The above copyright notice and this permission notice shall be included in +! all copies or substantial portions of the Software. +! +! The Software shall be used for Good, not Evil. +! +! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +! SOFTWARE. + +program gnc +use jessl +use iso_c_binding +use iso_fortran_env +implicit none + + ! For our TLS connection + type(c_ptr)::ctx + type(c_ptr)::method + type(c_ptr)::ssl + integer(kind=c_long)::res + + ! Cert stuff + character(512)::cert_public, cert_private + + ! Requested file + character(1024)::request + character(512)::mimetype + integer::request_unit + + ! Argument handling + integer::i + character(512)::arg + + ! Logfile + integer, parameter::logunit = 1097 + character(512)::logfile + + ! root serving dir + character(512), target::rootdir + + logfile = "/tmp/gnc.log" + rootdir = " " + + if(command_argument_count() < 4) then + call usage() + close(logunit) + stop + end if + + i = 1 + do while(i < command_argument_count()) + call get_command_argument(i, arg) + if(trim(arg) == "-pub") then + i = i + 1 + call get_command_argument(i, cert_public) + else if(trim(arg) == "-priv") then + i = i + 1 + call get_command_argument(i, cert_private) + else if(trim(arg) == "-l") then + i = i + 1 + call get_command_argument(i, logfile) + else if(trim(arg) == "-root") then + i = i + 1 + call get_command_argument(i, rootdir) + end if + i = i + 1 + end do + + open(unit=logunit, file=trim(logfile), action="write", status="unknown", position="append") + + call write_log("Initiating program") + + call random_seed() + + call library_init() + method = tls_server_method() + ctx = ctx_new(method) + + if(.not. C_ASSOCIATED(ctx)) then + call write_log("Context failed") + close(logunit) + stop + end if + + ! Seems to be a dummy now... + !res = ctx_set_ecdh_auto(ctx, 1) + + if(.not. ctx_use_certificate_file(ctx, trim(cert_public), SSL_FILETYPE_PEM)) then + call write_log("Cert file failed") + call write_log("Public: "//trim(cert_public)) + !call print_error() + close(logunit) + stop + end if + + if(.not. ctx_use_private_key_file(ctx, trim(cert_private), SSL_FILETYPE_PEM)) then + call write_log("Cert file failed") + call write_log("Private: "//trim(cert_private)) + !call print_error() + close(logunit) + stop + end if + + ssl = ssl_new(ctx) + + call write_log("Initiating connection") + + ! So this is a GNU Extension... + res = set_read_fd(ssl, fnum(input_unit)) + if(res /= 1) then + call write_log("set rfd failed") + !call print_error() + close(logunit) + stop + end if + + res = set_write_fd(ssl, fnum(output_unit)) + if(res /= 1) then + call write_log("set wfd failed") + !call print_error() + close(logunit) + stop + end if + + res = ssl_accept(ssl) + if(res <= 0) then + call write_log("ssl_accept failed") + !call print_error() + close(logunit) + stop + end if + + call write_log("Handling read_request") + + ! Do the actual protocol nonsense + call read_request(ssl, request) + + call write_log("Request is "//trim(request)) + + ! If it ends in a slash, let's manually and silently add "index.gmi" + if(request(len_trim(request):len_trim(request)) == "/") then + request = trim(request)//"index.gmi" + end if + + res = process_request(trim(request), trim(rootdir), request_unit, mimetype) + + call write_log("Request processed") + + select case(res) + + case(2) ! Success + call write_log("Delivering") + call write_file(ssl, request_unit, mimetype) + close(request_unit) + + case(3) ! Redirect + call write_log("Redirecting") + call write_redirect(ssl, request) + + case(5) ! Not Found + call write_log("Notfound") + call write_notfound(ssl) + + end select + + call write_log("Shutdown") + + res = ssl_shutdown(ssl) + res = ssl_free(ssl) + res = ctx_free(ctx) + + close(logunit) + +contains + + subroutine write_string(ssl, string) + use jessl, only: ssl_write + use iso_c_binding, only: c_ptr + implicit none + + type(c_ptr)::ssl + character(*)::string + character, dimension(:), allocatable::buf + integer::i + + allocate(buf(len(string))) + + do i = 1, len(string) + buf(i) = string(i:i) + end do + + i = ssl_write(ssl, buf) + + deallocate(buf) + + end subroutine write_string + + subroutine write_notfound(ssl) + use iso_c_binding, only: c_ptr, c_carriage_return, c_new_line + implicit none + + type(c_ptr)::ssl + character(*), parameter::notfound_response = "50 Not Found" + + call write_string(ssl, notfound_response//c_carriage_return//c_new_line) + + end subroutine write_notfound + + function read_into_buffer(unit_number, buffer) + implicit none + + integer, intent(in)::unit_number + character, dimension(*), intent(out)::buffer + integer::read_into_buffer + + integer::i, ierr + + ierr = 0 + i = 0 + do while(ierr == 0 .and. i < len(buffer)) + i = i + 1 + read(unit_number, iostat=ierr) buffer(i) + end do + + if(ierr /= 0) then + i = i - 1 + end if + + read_into_buffer = i + + end function read_into_buffer + + subroutine write_file(ssl, unit_number, mimetype) + use iso_c_binding, only: c_ptr, c_carriage_return, c_new_line + use jessl, only: ssl_write + implicit none + + type(c_ptr)::ssl + integer, intent(in)::unit_number + character(*), intent(in)::mimetype + character, dimension(64)::buf + integer::buflen, written + + call write_string(ssl, "20 "//trim(mimetype)//c_carriage_return//c_new_line) + + buflen = read_into_buffer(unit_number, buf) + do while(buflen > 0) + written = ssl_write(ssl, buf(1:buflen)) + buflen = read_into_buffer(unit_number, buf) + end do + + end subroutine write_file + + subroutine write_redirect(ssl, req) + use iso_c_binding, only: c_ptr, c_carriage_return, c_new_line + implicit none + + type(c_ptr)::ssl + character(*), intent(in)::req + integer::i + + character(*), parameter::index_file = "index.gmi" + + i = len_trim(req) + if(req(i:i) == '/') then + call write_string(ssl, "30 "//trim(req)//index_file//c_carriage_return//c_new_line) + else + call write_string(ssl, "30 "//trim(req)//"/"//index_file//c_carriage_return//c_new_line) + end if + + end subroutine write_redirect + + function requires_redirect(req) + implicit none + + character(*), intent(in)::req + logical::requires_redirect + + integer::i + + requires_redirect = .false. + + i = len_trim(req) + if(req(i:i) == '/') then + requires_redirect = .true. + else + ! The root url without any further slashes also requires redirect + i = index(req, "://") + if(i > 0) then + requires_redirect = (index(req(i+3:len_trim(req)), "/") <= 0) + end if + end if + + end function requires_redirect + + subroutine determine_mimetype(filename, mimetype) + implicit none + + character(*), intent(in)::filename + character(*), intent(out)::mimetype + + + character(256)::tmpfile + character(1024)::cmdline + character, dimension(5)::rand_string + real, dimension(5)::rand_values + + integer::i, readback_unit + + i = index(filename, ".gmi") + if(i == (len_trim(filename)-3)) then + + mimetype = "text/gemini" + + else + + call random_number(rand_values) + rand_string = char(ichar('A') + int(25*rand_values)) + tmpfile = "/tmp/gemini_" + do i = 1, 5 + tmpfile = trim(tmpfile)//rand_string(i) + end do + + cmdline = "file -b --mime-type "//trim(filename)//" > "//trim(tmpfile) + call write_log("Executing: "//trim(cmdline)) + call execute_command_line(cmdline, wait=.true.) + + open(newunit=readback_unit, file=tmpfile, action="read", status="old") + read(readback_unit, *) mimetype + close(readback_unit) + + ! I saw this happen once... + if(trim(mimetype) == "text") then + mimetype = "text/plain" + end if + + ! GNU Extenstion... :( + call unlink(tmpfile) + + end if + + end subroutine determine_mimetype + + function process_request(request, rootdir, request_unit, mimetype) result(status_code) + implicit none + + character(*), intent(in)::request + character(*), intent(in)::rootdir + integer, intent(out)::request_unit + character(*), intent(out)::mimetype + integer::status_code + + character(1024)::filename + integer::i, j, ioerror + + if(requires_redirect(request)) then + + status_code = 3 + + else + + ! Get the file of interest + i = index(request, "://") + if(i <= 0) then + filename = request + else + j = index(request(i+3:len_trim(request)), "/") + filename = request((i+j+2):len_trim(request)) + end if + + if(len_trim(rootdir) > 0) then + + filename = rootdir//"/"//trim(filename) + call write_log("Canonicalize: "//trim(filename)) + + ! Here we'll get rid of any dots or whatever and + if(.not. canonicalize_path(filename) .or. index(filename, rootdir) /= 1) then + call write_log("File outside root requested: "//trim(filename)) + status_code = 5 + return + end if + + end if + + call write_log("Opening: "//trim(filename)) + + ! First, check if exists by opening + open(newunit=request_unit, file=trim(filename), status="old", & + form="unformatted", iostat=ioerror, access="stream") + + if(ioerror /= 0) then + + status_code = 5 + + else + + call determine_mimetype(filename, mimetype) + status_code = 2 + + end if + + end if + + end function process_request + + subroutine read_request(ssl, req) + use jessl, only: ssl_read + use iso_c_binding, only: c_ptr + implicit none + + type(c_ptr)::ssl + character(*), intent(out)::req + + character, dimension(64)::buf + integer::bufread + + integer::i, j + + req = " " + i = 1 + + bufread = ssl_read(ssl, buf) + do while(bufread > 0) + + do j = 1, bufread + if(buf(j) == c_new_line) then + exit + end if + + if(buf(j) /= c_carriage_return) then + req(i:i) = buf(j) + i = i + 1 + end if + + end do + + if(buf(j) == c_new_line) then + exit + end if + + bufread = ssl_read(ssl, buf) + end do + + end subroutine read_request + + function canonicalize_path(path) + use iso_c_binding, only: c_char, c_null_char + implicit none + + character(*), intent(inout)::path + character(kind=c_char), allocatable, dimension(:), target::srcpath + logical::canonicalize_path + + type(c_ptr)::cdst + character(kind=c_char), dimension(:), pointer::dstpath + + character(256)::errr + integer::pathlen, i + + interface + function realpath(src, dst) bind(c) + use iso_c_binding + type(c_ptr)::realpath + type(c_ptr), value::src, dst + end function realpath + end interface + + interface + subroutine c_free(p) bind(c, name="free") + use iso_c_binding + type(c_ptr), value::p + end subroutine c_free + end interface + + pathlen = (len_trim(path)+1) + + allocate(srcpath(len(path))) + + srcpath = c_null_char + + do i = 1, pathlen-1 + srcpath(i) = path(i:i) + end do + srcpath(pathlen) = c_null_char + + cdst = realpath(c_loc(srcpath), c_null_ptr) + if(c_associated(cdst)) then + + path = " " + call c_f_pointer(cdst, dstpath, [1]) + + i = 1 + do while(dstpath(i) /= c_null_char) + path(i:i) = dstpath(i) + i = i + 1 + end do + + dstpath => null() + call c_free(cdst) + + canonicalize_path = .true. + + else + + write(errr, *) "realpath error: ", ierrno() + call write_log(trim(errr)) + + canonicalize_path = .false. + + end if + + deallocate(srcpath) + + end function canonicalize_path + + subroutine usage() + implicit none + + character(256)::exe + + call get_command_argument(0, exe) + + Print *, "Usage: "//trim(exe)//" -pub -priv [-l ] [-root ]" + + end subroutine usage + + subroutine write_log(string) + implicit none + + character(*), intent(in)::string + + ! GNU Extension... :( + write(logunit, *) fdate()//" :: "//string + call flush(logunit) + + end subroutine write_log + +end program gnc \ No newline at end of file -- cgit v1.2.3