mala::notes

      ----------------------------------------
       PicoGopher Part 4: the more we are, 
                          the funnier it is.
       November 14, 2022
      ----------------------------------------
       Written on my laptop, offline, during
       a flight and a train trip
      ----------------------------------------
  
  
  Welcome to Part 4 of the PicoGopher journey! If you want to
  see how everything started, you can find the previous parts
  in my phlog at gopher://gopher.club/1/users/mala/ (also
  mirrored at gopher://gopher.3564020356.org). You can find the
  project's code at https://github.com/aittalam/PicoGopher.
  
  In the previous posts I described the steps I followed to 
  create PicoGopher from scratch. Thanks to the large amount of
  available code and documentation, it was not too hard to
  crank up some simple implementation of the Gopher protocol
  and allow anyone to serve their own contents from a RasPi
  Pico W. While this is an interesting hack, though, I think
  that requiring people to (1) have a Gopher client installed
  and (2) know your server's IP address is, perhaps, a bit of
  a stretch. And for PicoGopher to be actually useful and used,
  I think it should be accessible to a wider audience.
  
  For this reason, today's post is dedicated to extending
  PicoGopher so it can provide contents via (a very simplified
  version of) an HTTP server and automatically redirect users' 
  requests to the AP's IP address. All of these options can be
  turned on or off at will, allowing for different level of
  access - from the most esoteric one (protected WiFi, Gopher
  only, non-default IP address) to the most open (open WiFi,
  HTTP access, automatic redirection to server IP).


  =========  Project status =========
  
  Below you can see the current status of the project. The
  steps marked as "x" have been completed, while those marked
  as "+" are described in this post. You will first read about
  going async (required as soon as we start running more than
  one server listening on different ports), then a few more
  details about the new HTTP and DNS servers.
  
  
    - [x] connect to the WiFi
    - [x] run a simple HTTP server
    - [x] run a mockup Gopher server
    - [x] load/save files
    - [x] make the Gopher server not a mockup anymore:
      - [x] translate gophermaps following gopher protocol
      - [x] load any file from disk
    
    - [x] set up the pico as an access point for geolocalised
          access
  
    - [+] make the server a bit more accessible
      - [+] enable async
      - [+] enable HTTP
      - [+] captive portal with PicoDNS
  
    - [ ] powering PicoGopher
      - [ ] better understand power saving
      - [ ] playing with batteries
  
  
  =========== Going Async ===========
  
  The translation of our server code to async becomes necessary
  when we want to simultaneously run more than one server on the 
  Pico (e.g. both Gopher and HTTP). This allows our servers to
  listen to requests on different ports and answer them without
  blocking each other.
  
  The datasheet "Connecting to the Internet with Raspberry Pi
  Pico W" [1] shows an example of an (HTTP) async server. The
  main difference wrt the non-async version is that socket
  binding, listening, and accepting are all dealt with by the
  `uasyncio` micropython lib [2], and our main() code will boil
  down to starting a new server, defined by a callback method,
  and host ip + port to listen to.
  
  The callback function is passed reader and writer streams by
  default, and we can use them to communicate with clients
  connecting to our servers. This does not change the way our
  Gopher server works in a dramatic way, but for the sake of
  readability I moved all its code to a PicoGopher class and
  created a similar one for HTTP (PicoHTTP). For DNS instead, 
  I just copied/pasted the code from P Doyle's Micropython 
  DNSServer Captive Portal project [4]. Yeah that's quite ugly,
  but I was too eager to see that working! I hope you can
  understand what I mean... And if you don't, let me cite Larry
  Wall: "Laziness, Impatience, Hybris: the three virtues of the
  programmer" :-)
  
  
  =========== HTTP Server =========== 
  
  Our HTTP server is a bit of a Frankenstein's creature, but we
  love it anyway :-) It is because it's a patchwork of code
  coming from the datasheets (for the async part), from the
  Gopher listener (to parse gopherfiles) and, erm, stack
  overflow (if I remember well!) to deal with URL decoding.
  We love it because it is another great example of the 80:20
  rule, and it shows us that you can get a reasonably working
  website running on a Pico, without the need to write one line
  of HTML (well, just because I did it for you... but look at
  the source code and you will convene with me that's a really
  small amount anyway).
  
  How can we get an HTML-free website? Given our gopherhole is
  (at least for now) made only of gopherfiles and plain text
  files, all we have to do is convert a gopherfile into an HTML
  page which links to the other files. The picohttp.py file
  shows how I did it - it is still very simple and limited, but
  I think it can be expanded rather easily to support more 
  advanced features. If you look for `gophermap` in the code,
  you will find the logic behind this: at the moment I do 
  nothing more than copying plain text rows as they are, and 
  translating 2-column links into HTML <a href...> links.
  Text formatting is managed by some improvised CSS that works 
  decently on Chrome&co but screws everything on Lynx, which
  means I have already added it to the list of things that
  need to be fixed (well, yeah, the more the 80% grows in 
  features, the more the 20% of things to fix grows too...)
  
  The other main difference when compared to Gopher is that the
  HTTP server gets multiline requests (with a set of headers
  for each request, that we currently just ignore), prepends
  resource descriptors with a GET (because GET requests are the
  only ones we accept), and encodes URLs by converting many
  characters into their hex equivalent (if you ever saw a URL
  containing a `%20` whenever you had originally typed a space,
  that's URL encoding for you). This last part is taken care of 
  by the `urldecode()` function (yeah that's the one I copied
  from stack overflow). 
  
  
  === DNS server + Captive Portal ===
  
  The concept of captive portal is one I had no clear idea
  about until just a few weeks ago, when I stopped after work
  to have a drink with some colleagues and friends (back when
  it was possible to invite guests at Twitter, but also back
  when I still had a job at Twitter). After the second pint 
  I came out with the question "how can I pop up a gopherhole 
  on someone's screen the same way those login pages appear 
  when you connect to some WiFis?". 
  
  A colleague (thanks Matt!) introduced me to the fabulous
  world of captive portals and once I knew the right terms to
  search (as in +fravia's arrows [3]) I was able to find their
  Micropython implementation. The one in [4] is the simplest
  one I found - and decided to use for a quick and dirty
  experiment with PicoGopher. 
  
  The captive portal in PicoGopher is implemented as (yet)
  another server, a DNS, that basically replies to every domain
  name resolution request with the same IP address, that is the
  one of your Pico (which is 192.168.4.1 by default). When a
  Mac or an iPhone detects this, it will also try to open a
  `hotspot-detect.html` page on the captive portal, so I
  updated the HTTP server to redirect the request to the root
  of our gopherhole.
  
  There are still a few caveats to this captive portal. First
  of all, this is just one of different ways of implementing it
  and I am not sure whether this is the best one yet. Then, I
  realised that when *all* DNS requests from a laptop or a
  phone are redirected to a Pico then the device might have a
  hard time replying to all of them (and even in the best case 
  when nothing breaks, it might still take way more power). 
  Finally, I found it hard to understand how the response 
  payload was built using the code and comments alone. If you 
  are feeling the same, I found [4] a bit more detailed and 
  with better references to DNS documentation and RFC [5,6]. 
  
  
  =========== Conclusions =========== 
  
  The code in the latest commit (4d085f4) implements all the
  changes described in this post. In `main.py` there is now a
  set of variables that you can customize not just to set up
  your AP but also to enable / disable both the HTTP and the 
  DNS server.
  
  Once again I think there's still some work to do here, but I
  hope this is enough to get started with some interesting new
  functionalities.
  
  
  ============ References =========== 

[1] https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf
[2] https://docs.micropython.org/en/latest/library/uasyncio.html
[3] https://fravia.2113.ch/targets.htm
[4] https://github.com/p-doyle/Micropython-DNSServer-Captive-Portal
[5] https://github.com/jczic/MicroDNSSrv/blob/master/microDNSSrv.py
[6] https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml
[7] https://www.rfc-editor.org/rfc/rfc1035.html