zongor c220c2ec78 | ||
---|---|---|
.. | ||
app | ||
src | ||
README.md | ||
fpm.toml | ||
test.sh |
README.md
fortran-µhttpd (game website & webserver)
Fortran does not have any native way to connect to the internet. It cannot even create sockets of any kind.
This makes it an absolute nightmare to create a web server since you need to import sockets and tcp libraries from C, using the using the use iso_c_binding feature.
However, in the spirit of the challenge, I wanted to not have to resort to importing functions from another programming language as much as possible. Later when I create the client I would have to import libraries from C, but for this I could try and use "Fortran Only".
While reading this old Fortran google groups question from 2009 the group suggested that the user just use netcat and serve the Fortran program as if it were a cgi script.
This was a great idea because instead of needing to mess around with networking and making sure the functions and types imported corretly from C, I could just read and write to standard in/out and to files.
The downside of using netcat
is that it uses unidirectional sockets. You would have to create a named pipe and do a bunch of redrieting to get it to work.
mkfifo ncpipe
nc -l 8080 0<ncpipe | ./fortran-micro-httpd '../../common/html/index.html' 1>ncpipe
It just doesnt seem all that clean to me. Additionally, it refused to stay open after running one message from the web client, so I gave up on that approach.
Somthing that does work and be a lot cleaner is listen1 from plan9port. This allows for the web browser to connect to the programs stdin, stdout, & stderr, and the program can read and write as a normal command line program.
listen1 'tcp!*!8080' ./fortran-micro-httpd
I next implemented the post handling. In www.f90, it checks to see if the header returning from the
I found a library to interface with the sqlite library I will store the username, password, and other information about.
The issue is that fortran does not have a built in cryptographic hash library nor is it in the stdlib, I did find a implementation of the SM3 hash which is cryptographically sound, and have used that.
If this were to be used in the real world I would prefer to use bcrypt as it is the most common password hashing algorithm.
The implementation does not work well with existing systems since you have to convert from fortran strings to c strings and it returns an integer array of c strings which has to be converted back into fortran strings.
This is a horrible implementation from a security POV, since there is no easy way to sanitize the input, fortran certainly doesn't do that out of the box.
Also, it turned out that the library simply did not work. Perhaps it was the way I was passing in the fotran strings, but I only ever got garbage back and not a hashed value.
Speaking of strings. At first fortran luls you into a false sense of security with strings. Fortran has a fantastic way of handling "defered length arrays". Which means that you can take an array of unknown size and just keep appending arrays to each other.
For example, this is totaly valid in fortran:
character, dimension(:), allocatable :: tape
character, dimension(4) :: magic_module_header = [ achar(0), achar(97), achar(115), achar(109) ]
character, dimension(4) :: module_version = [ achar(1), achar(0), achar(0), achar(0) ]
tape = [magic_module_header]
tape = [tape, module_version]
open(unit=u, file=file_output, access='stream', status='replace', action='write', iostat=ios)
write(u, iostat=ios) tape
close(u, iostat=ios)
deallocate(tape)
Pretty cool for a language with similar performance to C!
Well, its not that simple when it comes to "strings" because there are different ways to structure a "string" in fortran.
! theres the 'suggested fortran 2023 standard way'
! this format is also the way you can use a lot of the built in string functions and the '//' append operator.
character(len=:), allocatable :: string
! this way which is useful since you can use the same interface as a normal 1d array
character, dimension(:), allocatable :: string
! this other other way which only works inside of subrotines/functions
character, intent(in) :: string(:)
! also this older way
character(*) :: string
! and this ancent '77 spec way which wont work in some compiling modes.
CHARACTER string*17
Can you tell the difference between all of them? theyre all doing almost the same thing, but they are slightly different. And some ways seem to work fine until you try to write to output. Or if you use a speicifc operator on one tyep, you cant on the other.
This is exactly what happened with the sqliteff
libarary I was trying to use.
Lets take a look at this chunk of fortran code
character, dimension(:), allocatable, intent(in) :: request
integer, intent(in) :: length
character(len=24) :: username
character(len=:), allocatable :: command
get_username: do i = 1, length
if (request(i) .eq. '=') then
s_idx = i + 1
end if
if (request(i) .eq. '&') then
e_idx = i - 1
exit get_username
end if
end do get_username
username = transfer(request(s_idx:e_idx), username)
command = 'echo ' // trim(adjustl(username))
call execute_command_line(command)
Does this seem like it should work?
Should be that we pare through the request to find the first "username" value.
then use the transfer function which "magically" converts the slice of the request into the username string.
then we call "echo" and run it on the command line. Simple enough right?
Well no, because username is a static size, it keeps it length and will export random junk from other arrays alongside of it.
So we can call trim and adjestl and cut it down to the correct length right? actually no, once again because it is a static size it will generate a bunch of 'zero length' characters which will break the sqlite call.
So we have to come up with a compleatly different way of doing this.
The only consistant way to fix this issue is by using fortran array slices.
character, dimension(:), allocatable, intent(in) :: request
integer, intent(in) :: length
character(len=24) :: username
character(len=:), allocatable :: command
j = 1
get_username: do i = 1, length
if (request(i) .eq. '=') then
s_idx = i + 1
start = .true.
end if
if (request(i) .eq. '&') then
e_idx = i - 1
start = .false.
exit get_username
end if
if (start) then
username(j:j) = request(i + 1)
j = j + 1
end if
end do get_username
username_len = j-2
command = 'echo ' // username(:username_len)
call execute_command_line(command)
This is terrible code, but it works. We keep track of the length of the string and then we can take a slice out of the total string when we are concatinating the command. So now it works fine.
Now we have fortran string weirdness out of the way we can talk a little about the actual implementation of this.
The request/response headers are pretty easily replicated.
Probably the best feature in the HTTP protocol is actually the Content-Length
header, since it allows you to exactly allocate the amount of memory needed to store the body of the request.
There isnt a whole lot left to implement. Just getting the path to the html file and the database file from the args.
We read the index.html from the file and serve it to the front end on a GET
request.
On a POST
request it will parse the body and insert the user data into the database.
This is probably the longest I have had to take for the smallest amount of actual features for anything I have created yet.