Hotel hotspot hijinks

7 minute read

Ever been staying at a hotel and gotten annoyed that you always have to open a browser to log in for wireless access? Yup, me too. A recent instance was particularly frustrating and I had to pull out my favourite Swiss Army chainsaw in order to make my life a bit easier.

Stock image of hotel hotspot login page
Stock image of hotel hotspot login page. Not where I stayed. Image credits: Hotel Hirsch, Füssen

The situation

So, the background story is that I was staying at a hotel in the mountains for a few days. As is the fortunate case these days1, the hotel had wireless access. The weird part, though, was that each room had a separate username and password. “Fair enough”, I thought and promptly opened my laptop and then Firefox to enter my login data to get the dearly-awaited connectivity. Using Firefox (or any other browser for that matter) was necessary because the login page was accessed via a captive portal. That’s the thing you get directed through when you see a login banner like this pop up in your browser:

Firefox captive portal login banner

That’s fine, I thought, and went merrily on with my day.

The problem

The problem started the following day. After getting up and waking up my laptop, I wasn’t able to read my email2, or read chat on irc3, see my messages via Signal, or use the internet at all4.

Also, ping greeted me with Destination Net Prohibited:

$ ping
PING ( 56(84) bytes of data.
From logout.hotspot.lan ( icmp_seq=1 Destination Net Prohibited
From logout.hotspot.lan ( icmp_seq=2 Destination Net Prohibited
From logout.hotspot.lan ( icmp_seq=3 Destination Net Prohibited
--- ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2002ms

Obviously, I wasn’t online.

That’s when I noticed the Firefox captive portal login banner (see image above) again. Oh, I have to log in again, that’s weird. Upon clicking on the “Open Network Login Page” button, I was logged in automatically. No need to enter the login details again. That’s also weird, I thought, because if the login is automatic, why do I have to visit the login page again at all?

I put my laptop to sleep to go for a walk around the village, get some groceries, and enjoy the mountain air5. Upon my return, I had to log in again to get wireless access. I was slowly starting to get a bit miffed. My guess is that the MAC address from the relevant end-user device is removed from the access list fairly quickly, perhaps on the order of an hour or two6, and thus network connectivity is cut rather promptly.

One issue that made my situation even worse is that I often have several browser windows open at the same time; usually because I have several trains of thought on the go at once and each window contains information relevant to each train of thought. The thing is, only one of the browser windows actually shows the (automatically appearing) captive portal login banner. Finding the window with the banner was rather time consuming.

Ok, this was starting to get silly and a bit annoying. Time to automate the annoyance away. WWW::Mechanize to the rescue!

WWW::Mechanize as a comic book super hero; generated by DALL-E
WWW::Mechanize as a comic book super hero; generated by DALL-E.

The solution

Why choose WWW::Mechanize? Well, I’ve got experience with it (I used to use a similar process to automatically log in to the ICE train in Germany when I used to commute to work before the pandemic), I know I can use it to submit data into simple HTML forms, and Perl is my go-to language for this kind of automation.

So, how to get started with automating the login process? The simple solution: quit Firefox so that all browser windows are closed, put the computer to sleep and then go for a walk for a couple of hours.

Upon my return, I just needed to use a combination of perl -de0 to start a REPL-like environment to play around in and perldoc to read the extensive WWW::Mechanize documentation.

The first attempt at trying to trigger a connection to the captive portal didn’t go well:

└> perl -de0

Loading DB routines from version 1.55
Editor support available.

Enter h or 'h h' for help, or 'man perldebug' for more help.

main::(-e:1):   0
  DB<1> use WWW::Mechanize;

  DB<2> $mech = WWW::Mechanize->new;

  DB<3> $mech->get('');

Error GETing Can't connect to (SSL
connect attempt failed) at (eval
line 2.

Ok, so we need to use HTTP and avoid HTTPS. Good to know.

Just using HTTP worked much better:

  DB<4> x $mech->get('')
0  HTTP::Response=HASH(0x55a95f5048c0)
lots of details; you really don't want to see this

That’s what we like to see! We’re at least getting stuff back now. Having a look at the page’s title, we get:

  DB<5> x $mech->title();
0  'myadvise hotspot > login'

Yup, that’s a login page. Dumping the page’s content with

  DB<5> x $mech->content();
lots of HTML content

we get to see what we’ve got to play with. The main things to note about the content (which I’m not showing because it’s too much detail and I want to protect the innocent) are:

  • we have a form called login
<form name="login" action="http://login.hotspot.lan/login" method="post">
  • we have a username field with the name username
<input style="width: 80px" name="username" type="text" value=""/>
  • and we have a password field with the name password
<input style="width: 80px" name="password" type="password"/>

This gives us enough information to be able to submit the form using the relevant login data.

Aside: interestingly enough, the fields are in English even though the site is a German one. I guess standardising the fields on English can be useful when programming.

To submit the form, we use WWW::Mechanize’s submit_form() method (the call to which I’ve formatted nicely here to make things easier to read):

    form_name => 'login',
    fields => {
        username => 'username-for-room',
        password => 'password-for-room',

We can check if the form submission was successful by asking the HTTP::Response if things went well:

  DB<7> x $mech->res->is_success;
0  1

Looking good so far. Let’s see if ping works as expected

$ ping
PING ( 56(84) bytes of data.
64 bytes from ( icmp_seq=1 ttl=247 time=20.6 ms
64 bytes from ( icmp_seq=2 ttl=247 time=13.9 ms
--- ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 13.941/17.281/20.621/3.340 ms

Yes! In other words: we’re in! That was easy :relaxed:.

Putting everything together (and cleaning up the code a bit), I ended up with this:

use strict;
use warnings;

use WWW::Mechanize;

my $mech = WWW::Mechanize->new;

# check that we have the login page and not Google or something else
# i.e. we're not logged in
if ($mech->title() =~ 'login') {
        form_name => 'login',
        fields => {
            username => '<username-for-room>',
            password => '<password-for-room>'
    if ($mech->res->is_success) {
        print "Login successful\n";
    else {
        print "Login failed\n";
else {
    print "Already logged in\n";

Now I can just run the script every time I wake my laptop back up and I’m back online. Yay!

From a security perspective it’s a bit weird that the username and password are obviously tied to the room number. In other words, I could probably just use the neighbouring room’s account just as easily (I guess: I couldn’t be bothered checking in the end).

The conclusion

Well, this script certainly saved me some time and hassle when waking up my laptop from the suspend state. Also, it was fun working out what pieces of the puzzle were necessary in order to build a solution. Perl saves the day7 again!

  1. Remember when finding a hotel with any wireless connectivity was a total mission? Sort of like the days when finding a power outlet at an airport to charge a laptop was really difficult and one ended up sitting on the floor next to a utility room where the cleaning staff would normally plug in a vacuum cleaner. Ah, those were the days :wink:. Put another way: humanity has come a looong way. 

  2. I use mutt; it’s fast and one only needs to use text for email. Right? Right? 

  3. I also use irssi for IRC. Look, I’ve been around a while, ok? 

  4. I’m one of those geeks who live on the terminal, hence I tend to use a lot of terminal-based tools to get things done. 

  5. I don’t mean this ironically: because of the forests and the distance away from any kind of metropolis, the air is much fresher. Ever notice that the air in European cities is just awful? 

  6. Later, I’d tried putting my computer to sleep and then waking it up a few minutes later. The connection was still alive, so my best guess is that the timeout to keep connections alive and keep a MAC address registered is on the order of hours, but not more than two or three hours, because even such short periods of inactivity required login again. A later test showed that the timeout was after one hour. 

  7. Well, it saved the week really. 


If you liked this post and want to see more like this, please buy me a coffee!

buy me a coffee logo