mmo-project/fortran/www/README.md

175 lines
8.0 KiB
Markdown
Raw Normal View History

2023-08-26 13:58:47 -04:00
# 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.
2023-09-03 13:12:45 -04:00
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](https://fortranwiki.org/fortran/show/iso_c_binding).
2023-08-26 13:58:47 -04:00
2023-09-03 13:12:45 -04:00
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".
2023-08-26 13:58:47 -04:00
While reading this [old Fortran google groups question from 2009](https://groups.google.com/g/comp.lang.fortran/c/wL1xdnB1plk) the group suggested that the user just use [netcat](https://en.wikipedia.org/wiki/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.
2023-09-03 13:12:45 -04:00
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.
2023-08-26 13:58:47 -04:00
```sh
mkfifo ncpipe
2023-09-03 13:12:45 -04:00
nc -l 8080 0<ncpipe | ./fortran-micro-httpd '../../common/html/index.html' 1>ncpipe
2023-08-26 13:58:47 -04:00
```
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](https://9fans.github.io/plan9port/man/man8/listen1.html) from [plan9port](https://9fans.github.io/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.
```sh
2023-09-03 13:12:45 -04:00
listen1 'tcp!*!8080' ./fortran-micro-httpd
2023-08-26 13:58:47 -04:00
```
2023-09-03 13:12:45 -04:00
I next implemented the post handling. In www.f90, it checks to see if the header returning from the
2023-08-26 13:58:47 -04:00
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](https://github.com/zoziha/SM3-Fortran/tree/main) 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](https://en.wikipedia.org/wiki/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.
2023-09-03 13:12:45 -04:00
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:
```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.
```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
```fortran
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.
```fortran
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.