<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="pretty-atom-feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>varun.ch</title>
  <subtitle>Varun Biniwale&#39;s blog</subtitle>
  <link href="https://varun.ch/feed/feed.xml" rel="self" />
  <link href="https://varun.ch/" />
  <updated>2025-07-24T00:00:00Z</updated>
  <id>https://varun.ch/</id>
  <author>
    <name>Varun Biniwale</name>
  </author>
  <entry>
    <title>You shouldn&#39;t need to write a scraper just to list the available electives</title>
    <link href="https://varun.ch/posts/elective-list/" />
    <updated>2025-07-24T00:00:00Z</updated>
    <id>https://varun.ch/posts/elective-list/</id>
    <content type="html">&lt;p&gt;My future alma mater is one of the best engineering schools in Canada (and in the world). But, you definitely wouldn’t think that by looking at the course selection webpage, course search page or student information system&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/elective-list/#quest-note&quot; id=&quot;quest-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[1]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/course-selection.webp 1579w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/course-selection.png&quot; alt=&quot;Waterloo&#39;s course selection list&quot; width=&quot;1579&quot; height=&quot;1015&quot;&gt;&lt;/picture&gt;
&lt;em&gt;A list of every course offered by the school, but not a particularly helpful list if you’re trying to choose an elective as a first year student&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One of my classes was missing from my schedule, so I logged into the course enrolment portal in search of answers.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/quest-classes.webp 1587w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/quest-classes.png&quot; alt=&quot;Waterloo Quest class enrolment page&quot; width=&quot;1587&quot; height=&quot;1013&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;After clicking around at the menus for a few minutes, I found out that my chosen elective’s enrolment was “unsuccessful” due to a scheduling conflict, so I needed to find a new elective, &lt;em&gt;fast&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The school publishes a “List of courses that you might be able to choose” for incoming first year students… but for some reason, they only list course codes, and expect students to visit the Academic Calendar and then individually search up each course code just to find out the course titles. This would take hours by hand!!&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/course-codes.webp 1197w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/course-codes.png&quot; alt=&quot;Waterloo&#39;s first year elective course codes&quot; width=&quot;1197&quot; height=&quot;903&quot;&gt;&lt;/picture&gt;
&lt;em&gt;How am I supposed to know which of these course codes interest me!?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Moreover if I want to see if a class conflicts with my schedule or is already full, I need to use the “Schedule of Classes” tool. It allows users to search a subject and see the classes, time and enrolment numbers, (among other information). Unfortunately, my brain simply can’t parse the information they present.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/course-search.webp 1587w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/course-search.png&quot; alt=&quot;Waterloo&#39;s course schedule search homepage&quot; width=&quot;1587&quot; height=&quot;1015&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/course-search-results.webp 1574w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/course-search-results.png&quot; alt=&quot;Waterloo&#39;s course schedule search results&quot; width=&quot;1574&quot; height=&quot;1016&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Apparently, I’m not alone. Because Waterloo publishes &lt;a href=&quot;https://uwaterloo.ca/the-centre/academics/academics-undergraduate-students/registering-courses/understanding-schedule-classes#definitions&quot;&gt;this 1,800 word long guide explaining how to read the table and understand its terminology&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’m sure there are great legacy reasons to keep around this tool, but honestly, it’s overwhelming for new students like me. I’m not sure why, but I decided to write a web app which parses the official “Schedule of Classes” and presents it in a marginally more readable format for my purposes (and the idea was that, hopefully, along the way, I’d learn what all of the numbers mean)&lt;/p&gt;
&lt;p&gt;I got relatively far with this…&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/my-search-tool.webp 1000w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/my-search-tool.png&quot; alt=&quot;My homemade search tool&quot; width=&quot;1000&quot; height=&quot;874&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;… before learning that there’s an existing unofficial web service called &lt;a href=&quot;https://uwflow.com&quot;&gt;‘UW Flow’&lt;/a&gt; that does exactly this, just way better, and with way more data (they’ve apparently been working on it &lt;a href=&quot;https://uwflow.com/about&quot;&gt;since 2012&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/uw-flow.webp 1187w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/uw-flow.png&quot; alt=&quot;&quot; width=&quot;1187&quot; height=&quot;692&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;UW Flow is great, but like the course search tool, it doesn’t solve my problems with browsing the first year appropriate electives (namely that I don’t know the &lt;em&gt;names&lt;/em&gt; of the courses, just their course codes), so I began working on a second (and much less redundant) tool.&lt;/p&gt;
&lt;p&gt;First, I grabbed the list of the course codes from the “List of courses that you might be able to choose”&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;tbody td:nth-child(2)&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// fall 2025&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;flatMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;td&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; td&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;innerText&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;,&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;code&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; code&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// SCI 206 --&gt; SCI206&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;code&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; code&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// removes empty strings&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Array(173) [ &quot;AFM101&quot;, &quot;AFM123&quot;, &quot;AFM131&quot;, &quot;ASL101R&quot;, &quot;ANTH100&quot;, &quot;ARABIC101R&quot;, &quot;ARBUS101&quot;, &quot;BIOL130&quot;, &quot;BIOL130L&quot;, &quot;BIOL225&quot;, … ]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I scraped the data from UW Flow (sorry UW Flow maintainers!)&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; courseCodes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;AFM101&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchCourseData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; query &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;operationName&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;explore&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:*&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;code_only&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
      query explore($query: String, $code_only: Boolean) {
        search_courses(args: { query: $query, code_only: $code_only }) {
          course_id
          name
          code
          useful
          terms
          terms_with_seats
          ratings
          prof_ids
          liked
          easy
          has_prereqs
          __typename
        }
        search_profs(args: { query: $query, code_only: $code_only }) {
          prof_id
          name
          code
          clear
          course_ids
          course_codes
          engaging
          liked
          ratings
          __typename
        }
      }&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://uwflow.com/graphql&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;include&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;cors&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;referrer&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://uwflow.com/explore?q=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;code&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; json &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;code&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;json&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;data&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;search_courses&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; json&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;data&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;search_courses&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchAllCourses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; courseDetails &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; code &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; courseCodes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchCourseData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        courseDetails&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;No course found for: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;code&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error fetching &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;code&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; courseDetails&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;fetchAllCourses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I had the course information, I threw together a webpage&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/elective-list/#dockn-note&quot; id=&quot;dockn-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[2]&lt;/a&gt; with a sortable table listing the course names, codes, IDs, and their ratings from UW Flow… and just for fun, I added a dice button to randomly highlight a potential elective.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://uw-firstyear-electives.varunbiniwale.com/&quot;&gt;uw-firstyear-electives.varunbiniwale.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/elective-list/my-course-list.webp 1326w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/elective-list/my-course-list.png&quot; alt=&quot;My course listing page&quot; width=&quot;1326&quot; height=&quot;1019&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;This whole thing took under half an hour, and should save me (and anyone else) much more time than that.&lt;/p&gt;
&lt;p&gt;To be honest, I’m not sure how other students were supposed to pick electives without either coding a tool or slowly losing their will to live in the Schedule of Classes. Maybe they visited each elective’s page by hand and put together spreadsheets. Maybe they picked the first thing that didn’t sound terrible. But for me, a bit of scraping, parsing, and duct-taped UI was easier than decoding the legacy interface that looks like it was designed for robots by robots with a deep hatred of undergrads.&lt;/p&gt;
&lt;p&gt;Also, I still haven’t picked an elective. But at least now I can scroll past 172 of them faster.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The world&#39;s most unhinged video wall (made out of Chromebooks)</title>
    <link href="https://varun.ch/posts/videowall/" />
    <updated>2025-03-01T00:00:00Z</updated>
    <id>https://varun.ch/posts/videowall/</id>
    <content type="html">&lt;p&gt;This is the story of our three year long journey to turn a fleet of laptops into what can only be described as the world’s most unhinged video wall.&lt;/p&gt;
&lt;p&gt;This project was a collaboration with my friend &lt;a href=&quot;https://akselsalmi.com&quot;&gt;Aksel Salmi&lt;/a&gt;. I was responsible for the software, and he designed the incredible hardware, see &lt;a href=&quot;https://akselsalmi.com/blog/videowall&quot;&gt;his blog&lt;/a&gt; to learn about the unexpectedly complex hardware needed to mount these dismantled computers&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/videowall/#common-app-note&quot; id=&quot;common-app-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[1]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/videowall-crop.webp 2048w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/videowall-crop.jpeg&quot; alt=&quot;A cropped photo of the final video wall, 10 wall mounted laptop displays in sync&quot; width=&quot;2048&quot; height=&quot;382&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/barebones.webp 1280w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/barebones.jpeg&quot; alt=&quot;Two displays plugged into two mounted motherboards making up a part of the video wall&quot; width=&quot;1280&quot; height=&quot;720&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;About three years ago, my Design teacher (The amazing Mr. Bush) came to us with an idea - our school was about to dispose of its fleet of Chromebooks, and he was wondering if we could build anything with them.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/fleet.webp 766w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/fleet.png&quot; alt=&quot;Tons of Chromebooks&quot; width=&quot;766&quot; height=&quot;536&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;meet-the-lenovo-thinkpad-11e&quot; tabindex=&quot;-1&quot;&gt;Meet the Lenovo ThinkPad 11e &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#meet-the-lenovo-thinkpad-11e&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Lenovo ThinkPad 11e could very well be the world’s worst laptop. It is also the standard-issue school laptop that reinforced eight-year-old me’s interest in computers.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/5th-grade.webp 1280w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/5th-grade.jpeg&quot; alt=&quot;Picture of me using a ThinkPad 11e in 5th grade&quot; width=&quot;1280&quot; height=&quot;853&quot;&gt;&lt;/picture&gt;
&lt;em&gt;We used this school-issued laptop through primary and the start of middle school. This is me in 5th grade using a school laptop while working on my PYP Exhibition project (a game on Scratch)&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/videowall/#scratch-note&quot; id=&quot;scratch-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[2]&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Despite my emotional connection to them, today these devices are, for all intents and purposes, junk. And for that reason, my school began the process of replacing them (with marginally less junky laptops)&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/thinkpad-product-image.webp 1500w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/thinkpad-product-image.png&quot; alt=&quot;Product photo of Lenovo ThinkPad 11e Chromebook&quot; width=&quot;1500&quot; height=&quot;1500&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;These things don’t receive software updates from Google anymore, they struggle loading most webpages and to top it off, they’re tied to some long forgotten Enterprise Enrolment system, so they can’t even be used without a school Google account.&lt;/p&gt;
&lt;h2 id=&quot;what-is-a-video-wall&quot; tabindex=&quot;-1&quot;&gt;What is a video wall? &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#what-is-a-video-wall&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A video wall is a large display made up of multiple screens arranged together to create a single, seamless display across all the screens. In the case of our project, we decided to try reusing the laptop screens to build a video wall.&lt;/p&gt;
&lt;h2 id=&quot;can-we-drive-the-screens-using-separate-hardware&quot; tabindex=&quot;-1&quot;&gt;Can we drive the screens using separate hardware? &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#can-we-drive-the-screens-using-separate-hardware&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Our first idea was to harvest just the laptop display panels and somehow drive them using a powerful computer that could power the 10 screens simultaneously. We did not go this route (due to the fact that we had no idea what we were doing, and a quick estimate of the time and costs involved scared us away).&lt;/p&gt;
&lt;h2 id=&quot;okay-before-we-try-anything-else-let-s-just-try-synchronising-a-video-across-two-devices&quot; tabindex=&quot;-1&quot;&gt;Okay, before we try anything else, let’s just try synchronising a video across two devices &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#okay-before-we-try-anything-else-let-s-just-try-synchronising-a-video-across-two-devices&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Since the screens were attached to perfectly functional laptops, it was quickly apparent that we’d probably be better off letting each screen be driven independently by their own laptop motherboards.&lt;/p&gt;
&lt;p&gt;At this point, there were still many questions (eg. how were we going to do that on &lt;em&gt;Chromebooks&lt;/em&gt;), so we put aside that challenge to focus on the new issue this brings up: Can we synchronise a single video across multiple computers?&lt;/p&gt;
&lt;p&gt;Our experiments brought us to the school’s computer lab, where we experimented with VLC’s streaming abilities to get a stream synchronised across devices on a single network, but this posed two challenges: This system is not designed for videos being &lt;em&gt;perfectly&lt;/em&gt; in sync, nor was it designed for two clients to receive different video inputs (because the whole point of the video wall is to display one loooooong video across the screens, not 10 repeat copies of the same video).&lt;/p&gt;
&lt;p&gt;We were stuck here until my ““breakthrough””.&lt;/p&gt;
&lt;p&gt;For context, the story is currently in 2022. Two years earlier, I had been locked up in my room due to the COVID lockdown, and in this time, I had loads of fun building random realtime web apps, like a chat app and multiplayer drawing game. These apps worked thanks to &lt;code&gt;socket.io&lt;/code&gt;, a (primarily) WebSocket based library that allows for low-latency, bi-directional communication.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/chat-site.webp 1920w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/chat-site.png&quot; alt=&quot;Screenshot of socket.io based chat site&quot; width=&quot;1920&quot; height=&quot;968&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Screenshot of a chat site I made to pass the time during the 2020 lockdown&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I realised that my best bet to get videos synchronised would be by using a web page that used &lt;code&gt;socket.io&lt;/code&gt; to sync the video playback across clients. Yes, there are better approaches, but simply doing something like this worked unreasonably well, all things considered.&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;video&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;video&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	  Your browser does not support the video tag.
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;video&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; socket &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;

socket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;play&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; videoElement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;video&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	videoElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;play&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I named this ExpressJS server/client system &lt;code&gt;c-sync&lt;/code&gt;&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/videowall/#c-sync-note&quot; id=&quot;c-sync-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[3]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thanks to c-sync (and tons of tinkering), after some time we had decently synchronised videos across computer screens through a webpage (or at least it seemed like it, testing on these desktop computers)&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/3-screen-sync.webp 1280w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/3-screen-sync.jpeg&quot; alt=&quot;&quot; width=&quot;1280&quot; height=&quot;720&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;As it turns out, in reality, the Chromebooks are too slow for this to be a reliable approach to synchronising playback, and tiny discrepancies in loading times + latency + system clocks etc. lead to videos not being synchronised.&lt;/p&gt;
&lt;p&gt;Now, I’m not entirely sure &lt;em&gt;why&lt;/em&gt; this works so well, but I came up with a ridiculous solution by accident. When videos reach the end of playback, each client emits the start event.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;video&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onended&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	socket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// yes seriously, this is all I needed&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means that the slowest computers hold back the fastest computers, and get the chance to load the videos. This also means looping can be a very slightly jittery process (with each screen receiving 10 ‘start’ events), but as long as the first couple frames of the video are identical, nobody would even notice.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;
Sidenote: why not schedule with timestamps
&lt;/summary&gt;
Modern computers have clocks you can rely on to be extremely precise. This plus regular NTP synchronisations means a reasonable person might just try to ensure the full video is cached, then just send a &#39;start&#39; event to each client that schedules the client to start playback at a given timestamp. Unfortunately, these Chromebooks could not reliably keep track of time within milliseconds of each other, so this method didn&#39;t work for us.
&lt;/details&gt;
&lt;p&gt;Using this method, we have nearly perfectly synchronised video playback, and can play any video on any screen (meaning we can split a wide video into 10 segments, and each computer displays its respective part, all in sync with eachother)&lt;/p&gt;
&lt;h2 id=&quot;putting-it-together&quot; tabindex=&quot;-1&quot;&gt;Putting it together &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#putting-it-together&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/chromebook-parts.webp 1016w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/chromebook-parts.jpeg&quot; alt=&quot;Bare laptop motherboard, keyboard and screen&quot; width=&quot;1016&quot; height=&quot;1280&quot;&gt;&lt;/picture&gt;
&lt;em&gt;A disassembled Chromebook open to a test video&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We reached this stage within a month or two. Believe it or not, this project still had three years of work ahead of us. The biggest issue was Chromebook software. At this point, we had a website that we could manually open on each laptop to display a fullscreen synchronised video.&lt;/p&gt;
&lt;p&gt;Ideally, we would want this to be entirely automated, so that as soon as a Chromebook receives power, it boots up automagically to the c-sync client page. Unfortunately, right now, booting the Chromebook would just take you to a Google login page (and one that was locked to our school domain &lt;em&gt;to boot&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Also, just to add insult to injury, when batteries are removed, the laptops don’t turn themselves on when they receive power (you have to hold down the power button)&lt;/p&gt;
&lt;p&gt;This meant that our next step would have to be to replace ChromeOS with something else.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.mrchromebox.tech/docs/fwscript.html#overview&quot;&gt;‘ChromeOS Firmware Recovery Script’&lt;/a&gt; is a magical piece of technology that somehow supports many different Chromebook motherboards. Ours was called ‘GLIMMER’. We just had to enter the built-in ‘Recovery Mode’, enable ‘Developer Mode’ and use the ChromeOS Shell to run the script.&lt;/p&gt;
&lt;p&gt;Now we’re basically on the home stretch. All we needed to do was pick up some stable Linux distro, write a hacky startup script that loads up Chromium and simulates the keystrokes to fullscreen the video and we’re done!&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/xfce-chromebooks.webp 1280w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/xfce-chromebooks.jpeg&quot; alt=&quot;&quot; width=&quot;1280&quot; height=&quot;960&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;We ran in to two main issues: Some Chromebooks (roughly half of our working laptops) would refuse to enter developer mode due to the enterprise enrolment, and while we were able to get the other half onto a Linux distro, video playback would consistently freeze after some time (actually they would lock up entirely).&lt;/p&gt;
&lt;p&gt;It took us several months of on-and-off experimentation to figure out what to do. Essentially, the solution was to overwrite the entire default firmware with &lt;code&gt;coreboot&lt;/code&gt; (which is also possible using MrChromebox’s script). We just needed to remove the ‘Write Protection’ screw from each laptop motherboard, and this seemed to bypass the enrolment too.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/write-protection-diagram.webp 1155w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/write-protection-diagram.png&quot; alt=&quot;Write protection screw diagram&quot; width=&quot;1155&quot; height=&quot;833&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Lenovo’s handy Write Protection screw diagram&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Doing this for 20+ computers was slow and tedious. We only really needed the WiFi, motherboard and screen in working condition, but we decided to be (mostly) gentle and keep the laptops looking like laptops so that we had a keyboard and mouse for the rest of the installation steps.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/remove-write-protection-screw-with-this-easy-trick.webp 1536w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/remove-write-protection-screw-with-this-easy-trick.jpeg&quot; alt=&quot;Efficiently removing write protection screw by bending plastic enclsoure&quot; width=&quot;1536&quot; height=&quot;2048&quot;&gt;&lt;/picture&gt;
&lt;em&gt;By the end, we got quite efficient at removing the write protection screw&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After ‘corebooting’ the Chromebooks, we were also pleasantly surprised to find out that ‘Wake on AC’ was a feature of the firmware, and that video playback no longer randomly breaks. By this point we had enough non-bricked Chromebooks left over for a line of 10 screens and a handful of spares.&lt;/p&gt;
&lt;h2 id=&quot;the-final-stretch&quot; tabindex=&quot;-1&quot;&gt;The Final Stretch &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#the-final-stretch&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now we’re really on the final stretch. Aksel worked on the mounting hardware, which you can &lt;a href=&quot;https://akselsalmi.com/blog/videowall&quot;&gt;read about on his blog&lt;/a&gt;, while I worked on figuring out a less flaky way to ‘boot to a webpage’ than the keystroke simulation and startup script I bodged together.&lt;/p&gt;
&lt;p&gt;I previously used &lt;a href=&quot;https://github.com/guysoft/FullPageOS&quot;&gt;the aptly named ‘FullPageOS’&lt;/a&gt; for a different project (which I briefly mention in &lt;a href=&quot;https://youtu.be/gzc1p_QlO2I?t=457&quot;&gt;my TED talk, which you should watch&lt;/a&gt;), but it doesn’t run on x86 hardware.&lt;/p&gt;
&lt;p&gt;I landed on using ‘Porteus Kiosk’, which is just a minimal Linux distro that opens a fullscreen Chromium browser with all the correct flags for hands-off usage (eg. allowing video playback without user interaction)&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/porteus.webp 768w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/porteus.jpeg&quot; alt=&quot;Dismantled laptop with Porteus installer&quot; width=&quot;768&quot; height=&quot;1024&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;This honestly worked totally fine, but left me unsatisfied for two reasons. Firstly, I didn’t like how we couldn’t customise the splash screen, so our project would be forever stamped with the Porteus logo on every startup (which would be every morning). And secondly, in search of a better issue to justify the extra work, I realised we can’t remotely do anything to the installations (eg. changing the page URL) without re-doing them, which would be definitely a problem once these get mounted on the wall.&lt;/p&gt;
&lt;p&gt;For those good reasons, I embarked on the journey of building ‘my own distro’ that we could install on the laptops. The system should start with something minimal (no desktop environment), and have an elegant script to autostart a kiosk mode Chromium instance.&lt;/p&gt;
&lt;p&gt;I first tried NixOS before quickly realising there was no way it would work with the tiny amount of storage on these Chromebooks (and it failed to install with every single attempt).&lt;/p&gt;
&lt;p&gt;Then I gave up, started with a Debian minimal install and just wrote a script that would provision a client (generate a ‘KIOSK_ID’, set its hostname to &lt;code&gt;csync-client-$KIOSK_ID&lt;/code&gt;, connect to the school’s WiFi, create users/permissions and set up &lt;code&gt;openbox&lt;/code&gt; to autostart a fullscreen kiosk mode Chromium).&lt;/p&gt;
&lt;p&gt;Then after attempting to repeat this on a second machine, I realised I would be wasting so much time (installing Debian is very ‘hands-on’ - you need to press lots of buttons), and I discovered ‘FAI - Fully Automatic Installation’ and the web &lt;a href=&quot;https://fai-project.org/FAIme/&quot;&gt;FAI.me&lt;/a&gt; tool. To cut a long story short&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/videowall/#fai-note&quot; id=&quot;fai-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[4]&lt;/a&gt;, after redoing everything for the millionth time, I had a single USB that I can plug in to any ‘corebooted’ Chromebook which provisions it as a c-sync client. Woohoo!&lt;/p&gt;
&lt;p&gt;I also built out a ‘controller’ for c-sync which lets us manage connected clients and assign them videos.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/c-sync-admin-tool.webp 1284w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/c-sync-admin-tool.png&quot; alt=&quot;Screenshot of admin tool for c-sync showcasing connected clients&quot; width=&quot;1284&quot; height=&quot;2778&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/c-sync-admin-tool-2.webp 1284w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/c-sync-admin-tool-2.png&quot; alt=&quot;Screenshot of admin tool for c-sync showcasing config options&quot; width=&quot;1284&quot; height=&quot;2778&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;After a successful three day stress test where the playback remained butter-smooth (and I sacrificed my ability to sleep for the greater good of testing with the backlight on), we were ready to mount these laptops on the wall.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/stress-test-no-sleep.webp 1536w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/stress-test-no-sleep.jpeg&quot; alt=&quot;Stress testing laptop video playback at night&quot; width=&quot;1536&quot; height=&quot;1417&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;mounting&quot; tabindex=&quot;-1&quot;&gt;Mounting &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#mounting&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The mounting is mostly Aksel’s thing, so &lt;a href=&quot;https://akselsalmi.com/blog/videowall&quot;&gt;I implore you to read his blog&lt;/a&gt;, but here are some cool photos from the process. (also aren’t our cable splices so pretty and not terrifying?? 😁❤️)&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/laser-cut.webp 2048w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/laser-cut.jpeg&quot; alt=&quot;A laser cut acrylic backplate&quot; width=&quot;2048&quot; height=&quot;1536&quot;&gt;&lt;/picture&gt;
&lt;em&gt;An early iteration of the mounting backplate using a laser cut acrylic piece&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/artistic-mounting-hardware.webp 2048w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/artistic-mounting-hardware.jpeg&quot; alt=&quot;&quot; width=&quot;2048&quot; height=&quot;1536&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Aksel designed a pretty awesome looking backplate to mount the motherboard, which hangs on cleats on the walls. The displays are then held in place with clampy things. This is black magic to me.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/mounting-hardware.webp 1586w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/mounting-hardware.jpeg&quot; alt=&quot;Scattered mounting hardware&quot; width=&quot;1586&quot; height=&quot;900&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Everything laid out&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/installations.webp 1280w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/installations.jpeg&quot; alt=&quot;Scattered computer components in the workshop&quot; width=&quot;1280&quot; height=&quot;960&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Preparing some displays and motherboards&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/splice.webp 2048w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/splice.jpeg&quot; alt=&quot;Soldering cable splices&quot; width=&quot;2048&quot; height=&quot;1536&quot;&gt;&lt;/picture&gt;
&lt;em&gt;We decided to splice together power cables so that each power supply could power two computers. Send any complaints to &lt;a href=&quot;https://varun.ch/contact&quot;&gt;the pager on my contact page&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;!-- We went through over 17 iterations of the mounting hardware to land on the design that we ended up using.

![Wall mounted display of design iterations we tested](iterations.jpeg)
*Part of a display about our project* --&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/drill.webp 1006w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/drill.png&quot; alt=&quot;Drilling into the wall.&quot; width=&quot;1006&quot; height=&quot;651&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/cable-routing.webp 1280w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/cable-routing.jpeg&quot; alt=&quot;Cable routed through backplate&quot; width=&quot;1280&quot; height=&quot;960&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/on-wall.webp 2048w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/on-wall.jpeg&quot; alt=&quot;Mounting hardware on wall&quot; width=&quot;2048&quot; height=&quot;1536&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/screen-clamps.webp 1750w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/screen-clamps.jpeg&quot; alt=&quot;Clamps for screens&quot; width=&quot;1750&quot; height=&quot;1080&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/mid-mount.webp 1750w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/mid-mount.jpeg&quot; alt=&quot;&quot; width=&quot;1750&quot; height=&quot;1080&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/nearly-done.webp 2048w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/nearly-done.jpeg&quot; alt=&quot;All screens mounted&quot; width=&quot;2048&quot; height=&quot;1536&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Nearly there!&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;one-last-thing&quot; tabindex=&quot;-1&quot;&gt;One last thing… &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#one-last-thing&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After we painstakingly mounted everything, I realised something sort-of important. Computers generate heat. Somewhere along the way of wiping away the firmwares, the laptop fans stopped spinning, which meant things get quite hot quite quickly. I had to figure out a way to get those working again before we could comfortably leave this up 24/7 (well, actually 12/7).&lt;/p&gt;
&lt;h3 id=&quot;embedded-controllers&quot; tabindex=&quot;-1&quot;&gt;Embedded Controllers &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#embedded-controllers&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You can apparently interface with the &lt;a href=&quot;https://chromeos.dev/en/posts/embedded-controller&quot;&gt;‘ChromeOS Embedded Controller’&lt;/a&gt; using a tool called &lt;code&gt;ectool&lt;/code&gt;, which should allow you to manually set fan speeds (among other things). The online documentation for this is lacking, and there’s apparently a slightly different &lt;code&gt;ectool&lt;/code&gt; from &lt;code&gt;coreboot&lt;/code&gt; and from Google directly. None of this made much sense at all to me, and no built &lt;code&gt;ectool&lt;/code&gt; binary I could find would work. At some point, I found a dead link, but &lt;a href=&quot;https://web.archive.org/web/*/https://tree123.org/files/utils/ectool&quot;&gt;thanks to the magic of the Wayback Machine&lt;/a&gt;, I was able to get my hands on something that wouldn’t immediately crash.&lt;/p&gt;
&lt;p&gt;By some miracle, this version of the tool actually works perfectly fine at setting fan speeds, and after some testing, I found some goldilocks values that balance noise and temperature.&lt;/p&gt;
&lt;h2 id=&quot;aside-making-videos-for-the-thing&quot; tabindex=&quot;-1&quot;&gt;Aside: Making Videos for the Thing &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#aside-making-videos-for-the-thing&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As it turns out, making such a wide video is actually not easy. Each display has a resolution of 1366× 768, and very few pieces of software will let you edit a 13660 × 768 video. Final Cut Pro and Blender are the only programs we were able to do anything this wide in.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/blender.webp 2032w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/blender.png&quot; alt=&quot;Screenshot of Blender&quot; width=&quot;2032&quot; height=&quot;1167&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Blender is one of the greatest pieces of software ever created (alongside c-sync)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Then it’s just a matter of rendering the wide video and splitting it into 10 segments.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# in case anyone ever has this insane use case again&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;input_video&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;input.mp4&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;v8&quot;&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1366&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;768&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;segments&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;((&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt;segments&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;x_offset&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; width&lt;span class=&quot;token variable&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;output_file&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${prefix}&lt;/span&gt;-&lt;span class=&quot;token variable&quot;&gt;${i}&lt;/span&gt;.mp4&quot;&lt;/span&gt;

    ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$input_video&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-vf&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;crop=&lt;span class=&quot;token variable&quot;&gt;$width&lt;/span&gt;:&lt;span class=&quot;token variable&quot;&gt;$height&lt;/span&gt;:&lt;span class=&quot;token variable&quot;&gt;$x_offset&lt;/span&gt;:0&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c:a&lt;/span&gt; copy &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$output_file&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;

&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Splitting complete!&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;in-all-its-glory&quot; tabindex=&quot;-1&quot;&gt;In all its glory &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#in-all-its-glory&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;boot-sequence-and-self-calibration&quot; tabindex=&quot;-1&quot;&gt;Boot Sequence and ‘Self Calibration’ &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#boot-sequence-and-self-calibration&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;style&gt;lite-youtube {
    background-color: #000;
    position: relative;
    display: block;
    contain: content;
    background-position: center center;
    background-size: cover;
    cursor: pointer;
    max-width: 720px;
}

/* gradient */
lite-youtube::before {
    content: attr(data-title);
    display: block;
    position: absolute;
    top: 0;
    /* Pixel-perfect port of YT&#39;s gradient PNG, using https://github.com/bluesmoon/pngtocss plus optimizations */
    background-image: linear-gradient(180deg, rgb(0 0 0 / 67%) 0%, rgb(0 0 0 / 54%) 14%, rgb(0 0 0 / 15%) 54%, rgb(0 0 0 / 5%) 72%, rgb(0 0 0 / 0%) 94%);
    height: 99px;
    width: 100%;
    font-family: &quot;YouTube Noto&quot;,Roboto,Arial,Helvetica,sans-serif;
    color: hsl(0deg 0% 93.33%);
    text-shadow: 0 0 2px rgba(0,0,0,.5);
    font-size: 18px;
    padding: 25px 20px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    box-sizing: border-box;
}

lite-youtube:hover::before {
    color: white;
}

/* responsive iframe with a 16:9 aspect ratio
    thanks https://css-tricks.com/responsive-iframes/
*/
lite-youtube::after {
    content: &quot;&quot;;
    display: block;
    padding-bottom: calc(100% / (16 / 9));
}
lite-youtube &gt; iframe {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
    border: 0;
}

/* play button */
lite-youtube &gt; .lty-playbtn {
    display: block;
    /* Make the button element cover the whole area for a large hover/click target… */
    width: 100%;
    height: 100%;
    /* …but visually it&#39;s still the same size */
    background: no-repeat center/68px 48px;
    /* YT&#39;s actual play button svg */
    background-image: url(&#39;data:image/svg+xml;utf8,&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 68 48&quot;&gt;&lt;path d=&quot;M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z&quot; fill=&quot;red&quot;/&gt;&lt;path d=&quot;M45 24 27 14v20&quot; fill=&quot;white&quot;/&gt;&lt;/svg&gt;&#39;);
    position: absolute;
    cursor: pointer;
    z-index: 1;
    filter: grayscale(100%);
    transition: filter .1s cubic-bezier(0, 0, 0.2, 1);
    border: 0;
}

lite-youtube:hover &gt; .lty-playbtn,
lite-youtube .lty-playbtn:focus {
    filter: none;
}

/* Post-click styles */
lite-youtube.lyt-activated {
    cursor: unset;
}
lite-youtube.lyt-activated::before,
lite-youtube.lyt-activated &gt; .lty-playbtn {
    opacity: 0;
    pointer-events: none;
}

.lyt-visually-hidden {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }
&lt;/style&gt;
&lt;script&gt;/**
 * A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
 *
 * Thx to these as the inspiration
 *   https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
 *   https://autoplay-youtube-player.glitch.me/
 *
 * Once built it, I also found these:
 *   https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍)
 *   https://github.com/Daugilas/lazyYT
 *   https://github.com/vb/lazyframe
 */
class LiteYTEmbed extends HTMLElement {
    connectedCallback() {
        this.videoId = this.getAttribute(&#39;videoid&#39;);

        let playBtnEl = this.querySelector(&#39;.lty-playbtn&#39;);
        // A label for the button takes priority over a [playlabel] attribute on the custom-element
        this.playLabel = (playBtnEl &amp;&amp; playBtnEl.textContent.trim()) || this.getAttribute(&#39;playlabel&#39;) || &#39;Play&#39;;

        this.dataset.title = this.getAttribute(&#39;title&#39;) || &quot;&quot;;

        /**
         * Lo, the youtube poster image!  (aka the thumbnail, image placeholder, etc)
         *
         * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md
         */
        if (!this.style.backgroundImage) {
          this.style.backgroundImage = `url(&quot;https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg&quot;)`;
          this.upgradePosterImage();
        }

        // Set up play button, and its visually hidden label
        if (!playBtnEl) {
            playBtnEl = document.createElement(&#39;button&#39;);
            playBtnEl.type = &#39;button&#39;;
            playBtnEl.classList.add(&#39;lty-playbtn&#39;);
            this.append(playBtnEl);
        }
        if (!playBtnEl.textContent) {
            const playBtnLabelEl = document.createElement(&#39;span&#39;);
            playBtnLabelEl.className = &#39;lyt-visually-hidden&#39;;
            playBtnLabelEl.textContent = this.playLabel;
            playBtnEl.append(playBtnLabelEl);
        }

        this.addNoscriptIframe();

        // for the PE pattern, change anchor&#39;s semantics to button
        if(playBtnEl.nodeName === &#39;A&#39;){
            playBtnEl.removeAttribute(&#39;href&#39;);
            playBtnEl.setAttribute(&#39;tabindex&#39;, &#39;0&#39;);
            playBtnEl.setAttribute(&#39;role&#39;, &#39;button&#39;);
            // fake button needs keyboard help
            playBtnEl.addEventListener(&#39;keydown&#39;, e =&gt; {
                if( e.key === &#39;Enter&#39; || e.key === &#39; &#39; ){
                    e.preventDefault();
                    this.activate();
                }
            });
        }

        // On hover (or tap), warm up the TCP connections we&#39;re (likely) about to use.
        this.addEventListener(&#39;pointerover&#39;, LiteYTEmbed.warmConnections, {once: true});
        this.addEventListener(&#39;focusin&#39;, LiteYTEmbed.warmConnections, {once: true});

        // Once the user clicks, add the real iframe and drop our play button
        // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
        //   We&#39;d want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
        this.addEventListener(&#39;click&#39;, this.activate);

        // Chrome &amp; Edge desktop have no problem with the basic YouTube Embed with ?autoplay=1
        // However Safari desktop and most/all mobile browsers do not successfully track the user gesture of clicking through the creation/loading of the iframe,
        // so they don&#39;t autoplay automatically. Instead we must load an additional 2 sequential JS files (1KB + 165KB) (un-br) for the YT Player API
        // TODO: Try loading the the YT API in parallel with our iframe and then attaching/playing it. #82
        this.needsYTApi = this.hasAttribute(&quot;js-api&quot;) || navigator.vendor.includes(&#39;Apple&#39;) || navigator.userAgent.includes(&#39;Mobi&#39;);
    }

    /**
     * Add a &lt;link rel={preload | preconnect} ...&gt; to the head
     */
    static addPrefetch(kind, url, as) {
        const linkEl = document.createElement(&#39;link&#39;);
        linkEl.rel = kind;
        linkEl.href = url;
        if (as) {
            linkEl.as = as;
        }
        document.head.append(linkEl);
    }

    /**
     * Begin pre-connecting to warm up the iframe load
     * Since the embed&#39;s network requests load within its iframe,
     *   preload/prefetch&#39;ing them outside the iframe will only cause double-downloads.
     * So, the best we can do is warm up a few connections to origins that are in the critical path.
     *
     * Maybe `&lt;link rel=preload as=document&gt;` would work, but it&#39;s unsupported: http://crbug.com/593267
     * But TBH, I don&#39;t think it&#39;ll happen soon with Site Isolation and split caches adding serious complexity.
     */
    static warmConnections() {
        if (LiteYTEmbed.preconnected) return;

        // The iframe document and most of its subresources come right off youtube.com
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://www.youtube-nocookie.com&#39;);
        // The botguard script is fetched off from google.com
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://www.google.com&#39;);

        // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://googleads.g.doubleclick.net&#39;);
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://static.doubleclick.net&#39;);

        LiteYTEmbed.preconnected = true;
    }

    fetchYTPlayerApi() {
        if (window.YT || (window.YT &amp;&amp; window.YT.Player)) return;

        this.ytApiPromise = new Promise((res, rej) =&gt; {
            var el = document.createElement(&#39;script&#39;);
            el.src = &#39;https://www.youtube.com/iframe_api&#39;;
            el.async = true;
            el.onload = _ =&gt; {
                YT.ready(res);
            };
            el.onerror = rej;
            this.append(el);
        });
    }

    /** Return the YT Player API instance. (Public L-YT-E API) */
    async getYTPlayer() {
        if(!this.playerPromise) {
            await this.activate();
        }

        return this.playerPromise;
    }

    async addYTPlayerIframe() {
        this.fetchYTPlayerApi();
        await this.ytApiPromise;

        const videoPlaceholderEl = document.createElement(&#39;div&#39;)
        this.append(videoPlaceholderEl);

        const paramsObj = Object.fromEntries(this.getParams().entries());

        this.playerPromise = new Promise(resolve =&gt; {
            let player = new YT.Player(videoPlaceholderEl, {
                width: &#39;100%&#39;,
                videoId: this.videoId,
                playerVars: paramsObj,
                events: {
                    &#39;onReady&#39;: event =&gt; {
                        event.target.playVideo();
                        resolve(player);
                    }
                }
            });
        });
    }

    // Add the iframe within &lt;noscript&gt; for indexability discoverability. See https://github.com/paulirish/lite-youtube-embed/issues/105
    addNoscriptIframe() {
        const iframeEl = this.createBasicIframe();
        const noscriptEl = document.createElement(&#39;noscript&#39;);
        // Appending into noscript isn&#39;t equivalant for mysterious reasons: https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element
        noscriptEl.innerHTML = iframeEl.outerHTML;
        this.append(noscriptEl);
    }

    getParams() {
        const params = new URLSearchParams(this.getAttribute(&#39;params&#39;) || []);
        params.append(&#39;autoplay&#39;, &#39;1&#39;);
        params.append(&#39;playsinline&#39;, &#39;1&#39;);
        return params;
    }

    async activate(){
        if (this.classList.contains(&#39;lyt-activated&#39;)) return;
        this.classList.add(&#39;lyt-activated&#39;);

        if (this.needsYTApi) {
            return this.addYTPlayerIframe(this.getParams());
        }

        const iframeEl = this.createBasicIframe();
        this.append(iframeEl);

        // Set focus for a11y
        iframeEl.focus();
    }

    createBasicIframe(){
        const iframeEl = document.createElement(&#39;iframe&#39;);
        iframeEl.width = 560;
        iframeEl.height = 315;
        // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
        iframeEl.title = this.playLabel;
        iframeEl.allow = &#39;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&#39;;
        iframeEl.allowFullscreen = true;
        // AFAIK, the encoding here isn&#39;t necessary for XSS, but we&#39;ll do it only because this is a URL
        // https://stackoverflow.com/q/64959723/89484
        iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`;
        return iframeEl;
    }

    /**
     * In the spirit of the `lowsrc` attribute and progressive JPEGs, we&#39;ll upgrade the reliable
     * poster image to a higher resolution one, if it&#39;s available.
     * Interestingly this sddefault webp is often smaller in filesize, but we will still attempt it second
     * because getting _an_ image in front of the user if our first priority.
     *
     * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md for more details
     */
    upgradePosterImage() {
         // Defer to reduce network contention.
        setTimeout(() =&gt; {
            const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
            const img = new Image();
            img.fetchPriority = &#39;low&#39;; // low priority to reduce network contention
            img.referrerpolicy = &#39;origin&#39;; // Not 100% sure it&#39;s needed, but https://github.com/ampproject/amphtml/pull/3940
            img.src = webpUrl;
            img.onload = e =&gt; {
                // A pretty ugly hack since onerror won&#39;t fire on YouTube image 404. This is (probably) due to
                // Youtube&#39;s style of returning data even with a 404 status. That data is a 120x90 placeholder image.
                // … per &quot;annoying yt 404 behavior&quot; in the .md
                const noAvailablePoster = e.target.naturalHeight == 90 &amp;&amp; e.target.naturalWidth == 120;
                if (noAvailablePoster) return;

                this.style.backgroundImage = `url(&quot;${webpUrl}&quot;)`;
            }
        }, 100);
    }
}
// Register custom element
customElements.define(&#39;lite-youtube&#39;, LiteYTEmbed);
&lt;/script&gt;
&lt;div id=&quot;IUKbKAa37pI&quot; class=&quot;eleventy-plugin-youtube-embed&quot;&gt;&lt;lite-youtube videoid=&quot;IUKbKAa37pI&quot; style=&quot;background-image: url(&#39;https://i.ytimg.com/vi/IUKbKAa37pI/hqdefault.jpg&#39;);&quot; js-api&gt;&lt;div class=&quot;lty-playbtn&quot;&gt;&lt;/div&gt;&lt;/lite-youtube&gt;&lt;/div&gt;
&lt;h3 id=&quot;synced-videos&quot; tabindex=&quot;-1&quot;&gt;Synced videos! &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#synced-videos&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;div id=&quot;LtgqCIEOWx4&quot; class=&quot;eleventy-plugin-youtube-embed&quot;&gt;&lt;lite-youtube videoid=&quot;LtgqCIEOWx4&quot; style=&quot;background-image: url(&#39;https://i.ytimg.com/vi/LtgqCIEOWx4/hqdefault.jpg&#39;);&quot; js-api&gt;&lt;div class=&quot;lty-playbtn&quot;&gt;&lt;/div&gt;&lt;/lite-youtube&gt;&lt;/div&gt;
&lt;h3 id=&quot;now-there-s-an-enclosure-and-cable-routing&quot; tabindex=&quot;-1&quot;&gt;Now there’s an enclosure and cable routing! &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#now-there-s-an-enclosure-and-cable-routing&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;div id=&quot;6HSC_Q2xEgI&quot; class=&quot;eleventy-plugin-youtube-embed&quot;&gt;&lt;lite-youtube videoid=&quot;6HSC_Q2xEgI&quot; style=&quot;background-image: url(&#39;https://i.ytimg.com/vi/6HSC_Q2xEgI/hqdefault.jpg&#39;);&quot; js-api&gt;&lt;div class=&quot;lty-playbtn&quot;&gt;&lt;/div&gt;&lt;/lite-youtube&gt;&lt;/div&gt;
&lt;h2 id=&quot;yes-it-s-imperfect&quot; tabindex=&quot;-1&quot;&gt;Yes, it’s imperfect &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/videowall/#yes-it-s-imperfect&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Our video wall is imperfect. TN panel viewing angles suck, and the screens vary in colours and stuff. Yes, the synchronisation isn’t &lt;em&gt;perfect&lt;/em&gt;, and yes, I’m sure there were better alternatives for nearly every decision we made along the way.&lt;/p&gt;
&lt;p&gt;Yet I love our video wall, despite how absurdly weird it is. It’s a perfect representation of the iterative design process and a true testament to teamwork and collaboration. We turned E-Waste into something interesting. And maybe, just maybe, the real video wall was the friends we made along the way.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/videowall/videowall-crop.webp 2048w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/videowall/videowall-crop.jpeg&quot; alt=&quot;A cropped photo of the final video wall, 10 wall mounted laptop displays in sync&quot; width=&quot;2048&quot; height=&quot;382&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This project was made possible by the incredible work of so many people. Aside from my collaborator Aksel Salmi, our Design teacher Daniel Bush played a huge role in guiding us through the project.&lt;/p&gt;
&lt;p&gt;Additionally, I wanted to thank the &lt;a href=&quot;https://coreboot.org/&quot;&gt;coreboot project&lt;/a&gt; and Matt ‘MrChromebox’ DeVillier for putting together &lt;a href=&quot;https://docs.mrchromebox.tech/&quot;&gt;the firmware and tools&lt;/a&gt; that allowed any of this to work. I would also like to thank Thomas Lange of the &lt;a href=&quot;https://fai-project.org/&quot;&gt;FAI project&lt;/a&gt; for his help in building the FAI.me based automated installer that saved us so many many many hours, as well as his support over email.&lt;/p&gt;
&lt;p&gt;As silly as it sounds, this project was a backbone in my high-school experience. We hacked away at it every Monday for the past few years, and we grew up along the way too.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Browsers need to change how autofill works</title>
    <link href="https://varun.ch/posts/autofill/" />
    <updated>2025-01-18T00:00:00Z</updated>
    <id>https://varun.ch/posts/autofill/</id>
    <content type="html">&lt;p&gt;Typically, the highest impact of an &lt;a href=&quot;https://en.wikipedia.org/wiki/Cross-site_scripting&quot;&gt;XSS vulnerability&lt;/a&gt; on the web is gaining access to a victim’s account on the vulnerable website (either through a session token or by performing actions on their behalf while your code is running). If you’re particularly unlucky, an admin account could be exploited to perform more dangerous actions (like accessing additional user data) or something along those lines.&lt;/p&gt;
&lt;p&gt;However, there is a known issue with Chrome and Firefox that nobody seems to be talking about. This issue allows attackers to escalate a vulnerability into something persistent that crosses security boundaries into other parts of the victim’s digital life. Moreover, it can enable persistent access to accounts, even if a site uses &lt;code&gt;HttpOnly&lt;/code&gt; cookies or short-lived tokens.&lt;/p&gt;
&lt;h2 id=&quot;people-reuse-passwords&quot; tabindex=&quot;-1&quot;&gt;People reuse passwords. &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/autofill/#people-reuse-passwords&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;People reuse passwords across sites—it’s just a fact. Even if you set that aside, I would consider this a vulnerability (or at least undesirable behavior) that more people should be aware of.&lt;/p&gt;
&lt;p&gt;Aside from phishing attacks or compromising a site’s login flow, there should be no way for any vulnerability on a site to leak users’ passwords.&lt;/p&gt;
&lt;p&gt;Unfortunately, Chrome and Firefox currently trade security for convenience, automatically filling password fields when you visit a website—with no user interaction required. This allows malicious code on a website to exfiltrate your login credentials without any visual indication that it’s happening.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/autofill/firefox.webp 976w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/autofill/firefox.png&quot; alt=&quot;Firefox autofills passwords&quot; width=&quot;976&quot; height=&quot;558&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Firefox&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/autofill/chrome.webp 1128w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/autofill/chrome.png&quot; alt=&quot;Chrome autofills passwords&quot; width=&quot;1128&quot; height=&quot;620&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Google Chrome&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;safari-is-not-vulnerable-to-this&quot; tabindex=&quot;-1&quot;&gt;Safari is not vulnerable to this. &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/autofill/#safari-is-not-vulnerable-to-this&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/autofill/safari.webp 748w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/autofill/safari.png&quot; alt=&quot;Safari always prompts before autofilling passwords&quot; width=&quot;748&quot; height=&quot;456&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Safari: clearly requires user interaction in the form of biometrics—this is good!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It’s trivial for malicious JavaScript to insert a hidden login form and then read the values out, allowing it to exfiltrate your password with minimal (or no) visual indication or confirmation. On Firefox, my brief testing shows that this can happen on page load. Chrome, however, requires some user interaction before the input values can actually be read—although this could easily be triggered by an event on any click. Regardless, both browsers are vulnerable to this.&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/form&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;POST&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token value css language-css&quot;&gt;&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;username&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Username:&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;username&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;username&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Enter your username&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Password:&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Enter your password&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Submit&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt;
	&lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;onclick&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token value javascript language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;username: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;forms&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;elements&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;username&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;npassword: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;forms&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;elements&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	Leak my credentials
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/autofill/demo-alert.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/autofill/demo-alert.png&quot; alt=&quot;Alert showing the user&#39;s credentials&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;
Of course, this wouldn’t be as big of an issue if users didn’t reuse passwords. Although, it is a little ironic how, in this case, it’s the password manager that’s causing the problem.&lt;/p&gt;
&lt;h2 id=&quot;real-world-applicability&quot; tabindex=&quot;-1&quot;&gt;Real-world applicability &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/autofill/#real-world-applicability&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;While this may seem like a fringe or extreme issue, I’ve been able to use this bug to escalate the severity of two other issues. In both cases, I found a way to upload arbitrary HTML files to a trusted domain. However, an XSS vulnerability alone was not able to exfiltrate much (due to a tight CSP), but this autofill trick worked and allowed me to leak login credentials.&lt;/p&gt;
&lt;h2 id=&quot;solutions&quot; tabindex=&quot;-1&quot;&gt;Solutions &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/autofill/#solutions&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Perhaps browsers shouldn’t autofill inputs that aren’t directly visible to users, or they should at least prompt users before autofilling credentials (ideally with a biometric verification step). Safari does this very well.&lt;/p&gt;
&lt;p&gt;Try it out here:
&lt;a href=&quot;https://autofill-demo.varunbiniwale.com/&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to turn Firefox into a usable web browser</title>
    <link href="https://varun.ch/posts/firefox/" />
    <updated>2024-11-19T00:00:00Z</updated>
    <id>https://varun.ch/posts/firefox/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;This article is outdated! Firefox now ships a sidebar natively, and honestly, that alone is probably good enough for 99% of use cases.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Mozilla says that Firefox will be shipping better profile management and vertical tabs sometime soon. Until then, this blog post serves as a writeup about how I customised my Firefox experience to work the way I want it to.&lt;/p&gt;
&lt;p&gt;By the end of this article, you will learn how I turned Firefox into a minimal, keyboard-driven and productive browser that looks like this:
&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/minimal-firefox.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/minimal-firefox.png&quot; alt=&quot;Minimal Firefox&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/minimal-firefox-2.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/minimal-firefox-2.png&quot; alt=&quot;Minimal Firefox&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;
&lt;em&gt;Disclaimer: I have received money from Google (creators of Chrome) and The Browser Company (creators of Arc Browser), however as you may notice, this will not impact my bias.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;background&quot; tabindex=&quot;-1&quot;&gt;Background &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#background&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I hate Google Chrome. As in, I’ve been using Google Chrome since my birth, and I hate it. My first ever experiences on the web were on Google Chrome, the first laptops I used were Chromebooks at school, and when I finally got my own laptop, the first thing I installed was Google Chrome.&lt;/p&gt;
&lt;p&gt;Google Chrome is a good web browser. It’s fast, efficient and most importantly, the web is built for Google Chrome. You will never find a website that doesn’t work on Chrome. Ultimately a web browser is a tool, so I will use the one that works the best.&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/firefox/#via-note&quot; id=&quot;via-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[1]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Unfortunately, Google’s business model results in Chrome being used as a tool for Google to track you. I am uncomfortable with this. This year, I have been trying to reduce my dependence on Google products.&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/firefox/#pixel-note&quot; id=&quot;pixel-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[2]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Last year, I switched from Chrome to &lt;a href=&quot;https://arc.net&quot;&gt;Arc Browser&lt;/a&gt;, for two main reasons. Firstly, to try something new, and secondly, because I believed it was more privacy respecting than Google Chrome.&lt;/p&gt;
&lt;p&gt;Arc is a browser based on Chromium - while some may object to this, I see this as an advantage. Any webpage designed for Chrome (which is unfortunately all of them) will work perfectly fine in Arc. Again, a web browser is a tool, so I will use the best one that exists.&lt;/p&gt;
&lt;p&gt;Arc sells itself to people who want to organize their tabs. I certainly use a lot of browser tabs, but it’s never been a huge problem for me. Regardless, Arc’s solution to this works really well. Arc merges the idea of bookmarks and tabs into one sidebar, and this clicked for me. Essentially, you can use your bookmarks (“pinned tabs”) as if they were normal browser tabs, and they revert back to their pinned location when you close the page.
&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/arc-browser.webp 3644w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/arc-browser.png&quot; alt=&quot;&quot; width=&quot;3644&quot; height=&quot;2362&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;trouble-in-paradise&quot; tabindex=&quot;-1&quot;&gt;Trouble in Paradise &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#trouble-in-paradise&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I like to hack things. I was curious about the kind of technology Arc uses under the hood, so I started to reverse engineer the app’s network traffic on my Mac.&lt;/p&gt;
&lt;p&gt;It would phone home &lt;em&gt;constantly&lt;/em&gt;, which I was a little shocked by, and uncomfortable with. I also was not a huge fan of Arc’s dependence on Firebase, which is a Google product (more tracking!) and notoriously hard to secure &lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/firefox/#eva-note&quot; id=&quot;eva-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[3]&lt;/a&gt;
I also thought about how while I understood Google’s business model (more user data = more targeted ads = more revenue), I had no idea how The Browser Company makes (or intends to make) money, as a VC-funded startup. As a result, I was worried that Arc would either integrate features I don’t want, implement some kind of subscription service, or shut down entirely. None of these seemed like ideal scenarios.&lt;/p&gt;
&lt;p&gt;My suspicions came true twice recently. Once was when &lt;a href=&quot;https://kibty.town/blog/arc/&quot;&gt;xyzeva disclosed an incredible security issue in Arc and demonstrated more of the browser’s phone-home behaviour&lt;/a&gt;, and secondly, when The Browser Company announced that &lt;a href=&quot;https://www.youtube.com/watch?v=E9yZ0JusME4&quot;&gt;they were putting Arc aside to make a new browser aimed at the general population because “Arc would never reached one billion users”&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;that-other-browser&quot; tabindex=&quot;-1&quot;&gt;That Other Browser &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#that-other-browser&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In search for a better browser, I found myself on Firefox. Apart from Chrome and Safari, it’s the only independent browser that people actually use, so many (but not all!) websites actually work in Firefox. uBlock Origin also &lt;a href=&quot;https://github.com/gorhill/uBlock/wiki/uBlock-Origin-works-best-on-Firefox&quot;&gt;works best on Firefox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bloomberg.com/news/newsletters/2023-05-05/why-google-keeps-paying-mozilla-s-firefox-even-as-chrome-dominates&quot;&gt;Firefox is basically subsidised by Google&lt;/a&gt;, but as long as I can disable any tracking, I don’t see this as a major dealbreaker. I don’t think Firefox tracks data for Google anyway.&lt;/p&gt;
&lt;p&gt;It also comes with ads baked in.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/firefox-ads.webp 2110w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/firefox-ads.png&quot; alt=&quot;Ads in Firefox&quot; width=&quot;2110&quot; height=&quot;1220&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Firefox works fine. It’s a little slow to startup on my M2 MacBook Air, but once it’s running, it works fine. There are some small annoyances (I wish I could &lt;a href=&quot;https://cybercafe.dev/thisisunsafe-bypassing-chrome-security-warnings/&quot;&gt;&lt;code&gt;thisisunsafe&lt;/code&gt; my way out of HSTS errors&lt;/a&gt;, and installing homemade extensions is difficult for no good reason), but none of them come close to the big dealbreaker with Firefox.&lt;/p&gt;
&lt;p&gt;Firefox doesn’t really support browser profiles.&lt;/p&gt;
&lt;h2 id=&quot;browser-profiles&quot; tabindex=&quot;-1&quot;&gt;Browser Profiles &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#browser-profiles&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I don’t understand how people can use the web without browser profiles. Chrome’s support for separate profiles is great. Arc’s support is amazing. Firefox’s is not.&lt;/p&gt;
&lt;p&gt;In this world, there are at least three Varun Biniwales. One is a high-school student at a school in Zurich, another is a 17-year-old developer who has a personal blog, and is applying to university and the other one last worked at the Scratch Foundation. Separation between my school, personal and work profiles is an absolute must have.&lt;/p&gt;
&lt;p&gt;On Chrome, each profile acts as a separate installation (with separate cookies, bookmarks, extensions and settings), but there are some niceties like an easy profile switcher and the separate profile windows count as one application on macOS (so you can just &lt;kbd&gt;⌘&lt;/kbd&gt; + &lt;kbd&gt;`&lt;/kbd&gt; through them like normal windows)&lt;/p&gt;
&lt;p&gt;Arc makes this a little better, with spaces and profiles. The sidebar lets you select a ‘Space’, which can be assigned to a browser profile. This solves the issue of having a billion browser windows open, because there’s only ever one Arc window open, and it displays only the profile you want. This comes down to individual taste, but I found it the best to focus in, as once I closed the sidebar, I would be fullscreen in a webpage with no easy way to switch to another window to procrastinate. Arc also shares some essential settings, but again, leaves cookies, bookmarks and extensions separate across profiles.&lt;/p&gt;
&lt;p&gt;Firefox does not have profiles. Firefox has “containers,” which are simply not the same thing. Containers isolate cookies, and only cookies, which solves tracking and convenience issues, but they do not isolate bookmarks, extensions, and settings, which to me are must-haves. I use separate browser extensions (e.g., certain dev tools) in my personal account, which I would not want to risk sharing with my work or school accounts. Similarly, I never want any work- or school-related materials to leak into my personal life. Browser profiles help me focus, and so they’re a necessity. &lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/firefox/#machines-note&quot; id=&quot;machines-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[4]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I lied. Firefox does have profiles. But you could be forgiven if you didn’t know that visiting “about:profiles” in a new tab and using an interface that looks like this was the way to manage them:&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/profile-menu.webp 2946w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/profile-menu.png&quot; alt=&quot;Firefox browser profiles&quot; width=&quot;2946&quot; height=&quot;2042&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Once you have your profiles set up, you’ll find that they are even more isolated than Chrome. But there is zero integration between profiles (eg. the ability to send a tab to another profile), and they even show up as separate entries in the macOS dock and switcher. Unfortunately, there’s also no way to differentiate between profiles as they all have the same icon, and there’s no profile switcher in the interface. Until Mozilla makes profiles usable, simply install &lt;a href=&quot;https://github.com/null-dev/firefox-profile-switcher&quot;&gt;this unmaintained browser extension&lt;/a&gt;, and &lt;a href=&quot;https://github.com/null-dev/firefox-profile-switcher-connector&quot;&gt;the associated connector&lt;/a&gt;, which prompts you for your password whenever you restart your computer (no idea why). But once it’s all done, you’ll have a decent menu to control your profiles, just like in Chrome.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/profile-picker.webp 618w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/profile-picker.png&quot; alt=&quot;Profile switcher menu&quot; width=&quot;618&quot; height=&quot;770&quot;&gt;&lt;/picture&gt;
Next, I tackled the issue of the tab bar. I kind of fell in love with Arc’s vertical tabs, so I tried to replicate them in Firefox. My main reason for wanting vertical tabs is that the Y axis of a 16:9 screen is expensive, while the X axis is usually unused and wasted on page margins. As a result, moving tabs to the left of your screen makes a lot of sense. You get more space to read page titles, and more space to keep your tabs.&lt;/p&gt;
&lt;p&gt;(note that Firefox is supposedly shipping a native tab sidebar soon, but as of writing, even with the flag enabled, there is no keyboard shortcut to open and close it, so it’s useless for me right now)&lt;/p&gt;
&lt;h2 id=&quot;sidebery&quot; tabindex=&quot;-1&quot;&gt;Sidebery &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#sidebery&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Sidebery is an amazing browser extension for Firefox. It acts as a new type of sidebar (Firefox has native sidebars for eg. bookmarks), and manages tabs in a way that almost feels native. By default, it can be opened and closed with &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;E&lt;/kbd&gt;&lt;/p&gt;
&lt;p&gt;Of course, this is silly unless you can hide the default tab bar.
&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/sidebery-silly.webp 3644w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/sidebery-silly.png&quot; alt=&quot;Firefox with both a sidebar and horizontal tabs&quot; width=&quot;3644&quot; height=&quot;2362&quot;&gt;&lt;/picture&gt;
That’s when I discovered userChrome.css. userChrome.css turns Firefox from a 5 to a 10. Even if I hated Firefox, userChrome.css would make me love it. userChrome.css is the secret sauce that transforms the otherwise bland burger known as Firefox into the world’s greatest gourmet masterpiece of a burger.&lt;/p&gt;
&lt;h2 id=&quot;userchrome-css&quot; tabindex=&quot;-1&quot;&gt;userChrome.css &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#userchrome-css&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Go to &lt;code&gt;about:config&lt;/code&gt;, and enable &lt;code&gt;toolkit.legacyUserProfileCustomizations.stylesheets&lt;/code&gt;. Then go to &lt;code&gt;about:support&lt;/code&gt; and open the profile’s folder. Add a folder called &lt;code&gt;chrome&lt;/code&gt; and create a file named &lt;code&gt;userChrome.css&lt;/code&gt;. You are now free to literally style the browser’s interface to your hearts content. If you’re anything like me, this will blow your mind, because there’s now basically no limits on how your browser looks, and to some extent, how it functions. Firefox also has a &lt;a href=&quot;https://firefox-source-docs.mozilla.org/devtools-user/browser_toolbox/index.html&quot;&gt;“Browser Toolbox”&lt;/a&gt; that lets you use the Inspector on the actual browser chrome.&lt;/p&gt;
&lt;p&gt;I use the following CSS to hide the tab bar.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;#main-window[tabsintitlebar=&quot;true&quot;]:not([extradragspace=&quot;true&quot;])
	#TabsToolbar
	&gt; .toolbar-items&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;pointer-events&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;#main-window:not([tabsintitlebar=&quot;true&quot;]) #TabsToolbar&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; collapse &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/custom-1.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/custom-1.png&quot; alt=&quot;Firefox with hidden tab bar&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;
I also like to hide the Sidebar picker/header, because I only ever use Sidebery.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;#sidebar-box #sidebar-header&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/custom-2.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/custom-2.png&quot; alt=&quot;Firefox with sidebar header hidden&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;
Then I found some CSS that someone wrote to make Firefox more compact… (I can’t find the original author, but if you can, please let me know!)&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;/* Tabbar: reduce tab margin */&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--tab-block-margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4px 3px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Tab: Reduce height */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.tabbrowser-tab&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;min-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 24px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Tab: Ensure tab height doesn&#39;t augment when arrowscrollbox is visible  */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#tabbrowser-arrowscrollbox&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--tab-min-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 31px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;max-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--tab-min-height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Tab: Attention icon */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.tabbrowser-tab:is([image], [pinned])
	&gt; .tab-stack
	&gt; .tab-content[attention]:not([selected=&quot;true&quot;]),
.tabbrowser-tab
	&gt; .tab-stack
	&gt; .tab-content[pinned][titlechanged]:not([selected=&quot;true&quot;])&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background-position-x&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; left 2px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background-position-y&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bottom 12.5px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* URLBar: Fix vertical alignment */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#urlbar[breakout=&quot;true&quot;]:not([open=&quot;true&quot;])&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--urlbar-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--urlbar-toolbar-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 24px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* URLBar: Fix URL address vertical aligment when megabar is open */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#urlbar[breakout=&quot;true&quot;][open=&quot;true&quot;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--urlbar-toolbar-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* URLBar: Reduce row items padding */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.urlbarView-row-inner&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;padding-inline&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--urlbarView-item-inline-padding&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;padding-block&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* URLBar: Reduce and realign row bookmark icons */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.urlbarView-type-icon&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin-bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0 &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin-inline-start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* URLBar: Reduce &quot;This time, serach with&quot; padding */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#urlbar .search-one-offs:not([hidden])&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;padding-block&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Searchbar: Ensure toolbar height doesn&#39;t augment when searchbar is visible */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#urlbar-container,
#search-container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;padding-block&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0 &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Searchbar: Make sure the min-height of the input is the same as the popup */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#search-container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 192px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Toolbar: Reduce spacing */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#urlbar-container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--urlbar-container-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin-top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0 &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Reload Button: Fix vertical alignment */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#reload-button&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;margin-block-start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; -2px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* AppMenu: Header */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.panel-header&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4px 0 0 4px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* AppMenu: Header button */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.panel-header &gt; .subviewbutton-back&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Windows 10 context menu */&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;-moz-os-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; windows-win10&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;/* Context Menu: Reduce vertical space */&lt;/span&gt;
	&lt;span class=&quot;token selector&quot;&gt;menupopup &gt; menuitem,
	menupopup &gt; menu&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token property&quot;&gt;padding-block&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2px &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And also, since I never actually ever click them, why not get rid of the traffic light icons on the app in macOS altogether!&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* remove the &#39;native&#39; titlebar (traffic light buttons and all) */&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;#TabsToolbar&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* but it should be visible non main browsers */&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;#main-window[privatebrowsingmode=&quot;temporary&quot;] #TabsToolbar&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/custom-3.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/custom-3.png&quot; alt=&quot;Custom Firefox in compact mode and hidden toolbar&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;
Then, for fun, I also customise the font:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Iosevka&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Monaco&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Courier New&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; monospace &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;synchronising-your-userchrome-across-profiles&quot; tabindex=&quot;-1&quot;&gt;Synchronising your userChrome across profiles &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#synchronising-your-userchrome-across-profiles&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;All of this is wonderful, but you’ll have to duplicate all your work to style each browser and keep those in sync. Fortunately, Firefox supports CSS &lt;code&gt;@import&lt;/code&gt;, so you can just write one main stylesheet, and import it for each of your browser profiles.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file:///Users/varun/Documents/code/firefox/base.css&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I customise each profile’s theme with &lt;a href=&quot;https://color.firefox.com/&quot;&gt;Firefox Color&lt;/a&gt;, and I keep tiny colour tweaks per profile, like this&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--focus-outline-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #cd23b9 &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--in-content-primary-button-background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #cd23b9 &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--in-content-primary-button-background-hover&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #cd23b993 &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--in-content-primary-button-background-active&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #cd23b95c &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;--in-content-page-background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; black &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.urlbarView-row[selected]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #cd23b9 &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; black &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;other-tweaks&quot; tabindex=&quot;-1&quot;&gt;Other tweaks &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#other-tweaks&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I also fiddle with various Firefox settings to make the browser more usable. I don’t use a browser homepage, so I disable that other “Home” in &lt;code&gt;about:preferences&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/firefox-settings.webp 1354w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/firefox-settings.png&quot; alt=&quot;Firefox settings page&quot; width=&quot;1354&quot; height=&quot;972&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The unlabelled checkbox next to “Search Shortcuts” allows you to remove options from the the “This time search with:” bar. If you disable all of them, it removes the bar altogether, which makes it actually possible to tab through search suggestions with your arrow keys.&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/firefox-search-options-1.webp 1266w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/firefox-search-options-1.png&quot; alt=&quot;Firefox search options with annoying bar at the bottom&quot; width=&quot;1266&quot; height=&quot;478&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/firefox-search-options-2.webp 952w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/firefox-search-options-2.png&quot; alt=&quot;Firefox search options without annoying bar at the bottom&quot; width=&quot;952&quot; height=&quot;402&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;You can also play with Sidebery’s settings, I like to set the ‘Navigation bar’ layout to ‘hidden’. It also recommends setting &lt;code&gt;svg.context-properties.content.enabled&lt;/code&gt; in &lt;code&gt;about:config&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, which allows Sidebery to pick up on your Firefox theme. Combine this with a font defined in Sidebery’s configuration to make it feel native.&lt;/p&gt;
&lt;p&gt;Finally, in &lt;code&gt;about:config&lt;/code&gt;, I set &lt;code&gt;browser.tabs.closeWindowWithLastTab&lt;/code&gt; to false (to match Arc).&lt;/p&gt;
&lt;h3 id=&quot;bonus&quot; tabindex=&quot;-1&quot;&gt;Bonus &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#bonus&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://vimium.github.io/&quot;&gt;Vimium&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/firefox/custom-4.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/firefox/custom-4.png&quot; alt=&quot;One of my customised Firefox profiles&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;was-this-worth-it&quot; tabindex=&quot;-1&quot;&gt;Was this worth it? &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#was-this-worth-it&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Sure, I had fun. And now my web browser works the way I want it to. This is not for everyone.&lt;/p&gt;
&lt;p&gt;The user experience on Firefox is still kilometres behind Arc until they can add vertical tabs and better profile management natively (and some of Arc’s niceties like ‘Little Arc’). Even still, I would sorely miss being able to switch “Spaces” with the swipe of the sidebar, something which Firefox seems architecturally incompatible with. Putting all my Firefox profiles on separate macOS desktops gets close, but it’s just not the same.&lt;/p&gt;
&lt;p&gt;Despite all that, I still had a lot of fun customising my Firefox setup, and it’s been my daily driver for a couple months now. Ultra-custom Firefox is like the desktop Linux of web browsers.&lt;/p&gt;
&lt;h2 id=&quot;aside-zen-browser&quot; tabindex=&quot;-1&quot;&gt;Aside: Zen Browser &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#aside-zen-browser&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zen-browser/desktop&quot;&gt;Zen Browser&lt;/a&gt; is an open source fork of Firefox that aims to replicate something like the Arc user experience. It still falls short in a few ways, and I’m generally more comfortable using a reputable browser like Firefox, rather than putting the most critical software on my computer (after the OS) in the hands of someone new like I did with Arc again.&lt;/p&gt;
&lt;p&gt;Web browsers also have massive vulnerabilities all the time, and of course, it would take time for forks to catch up. I can’t trust that Zen Browser is getting these patches quickly.&lt;/p&gt;
&lt;p&gt;Also &lt;a href=&quot;https://github.com/zen-browser/desktop/pull/927&quot;&gt;Zen Browser intentionally enabled remote-debugging and intentionally disabled the warning messages, essentially leaving a backdoor for anyone on your network to exploit&lt;/a&gt;. Not cool.&lt;/p&gt;
&lt;h2 id=&quot;the-ideal-browser&quot; tabindex=&quot;-1&quot;&gt;The ideal browser &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/firefox/#the-ideal-browser&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My ideal browser would be Chromium minus the tracking, and with a far more minimal keyboard-driven interface (ideally with vertical tabs). If it could have something like userChrome.css, that would be huge.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Navigate MacOS popups using your keyboard</title>
    <link href="https://varun.ch/posts/macos-keyboard/" />
    <updated>2024-11-11T00:00:00Z</updated>
    <id>https://varun.ch/posts/macos-keyboard/</id>
    <content type="html">&lt;p&gt;I can’t believe this isn’t the default.&lt;/p&gt;
&lt;p&gt;On MacOS Sonoma onwards, under ‘System Settings’ in ‘Keyboard’, turn on ‘Keyboard navigation’. This grants you the privilege of using Tab and Shift Tab to select options in popup menus.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/macos-keyboard/setting.webp 1392w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/macos-keyboard/setting.png&quot; alt=&quot;Screenshot of MacOS settings app&quot; width=&quot;1392&quot; height=&quot;802&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;img loading=&quot;lazy&quot; src=&quot;https://varun.ch/image/macos-keyboard/demo.gif&quot;&gt;
&lt;p&gt;Enjoy the marginal productivity improvement!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Make your shell 370.52% faster with this easy trick (fix nvm startup times)</title>
    <link href="https://varun.ch/posts/slow-nvm/" />
    <updated>2024-10-04T00:00:00Z</updated>
    <id>https://varun.ch/posts/slow-nvm/</id>
    <content type="html">&lt;p&gt;&lt;code&gt;zsh&lt;/code&gt; was slow. A shell should not take nearly a full second to start. 814 milliseconds may not sound like a lot, but it’s extremely noticeable, especially when creating new terminal tabs or sshing into a server.&lt;/p&gt;
&lt;p&gt;Time how long it takes for zsh to start:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;zsh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was seeing upwards of 0.81 to 0.90 seconds on my MacBook (and worse on my spinning rust servers) for zsh.&lt;/p&gt;
&lt;p&gt;Blanking out my &lt;code&gt;.zshrc&lt;/code&gt;, my shell startup time dropped to below 0.03 seconds. Therefore I figured I was misconfiguring zsh. Commenting out lines one by one, the culprit was clear: &lt;code&gt;nvm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is because I was using nvm installed using the recommended install.sh script. From my testing, this is fine in bash, but destroys zsh startup times.&lt;/p&gt;
&lt;h2 id=&quot;fix-it&quot; tabindex=&quot;-1&quot;&gt;Fix it &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/slow-nvm/#fix-it&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To fix this, I used &lt;a href=&quot;https://github.com/lukechilds/zsh-nvm&quot;&gt;&lt;code&gt;zsh-nvm&lt;/code&gt;&lt;/a&gt;, which has the ability to enable Lazy Loading - aka, only initializing nvm when I call any node-y programs for the first time. I know that zsh plugin managers exist, but they seem excessive to me, so I just copied the &lt;a href=&quot;https://raw.githubusercontent.com/lukechilds/zsh-nvm/refs/heads/master/zsh-nvm.plugin.zsh&quot;&gt;&lt;code&gt;zsh-nvm.plugin.zsh&lt;/code&gt;&lt;/a&gt; script into a file at &lt;code&gt;~/.zsh-nvm.zsh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then, simply comment out the auto-generated nvm lines from your &lt;code&gt;.zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# export NVM_DIR=&quot;$HOME/.nvm&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# [ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; &#92;. &quot;$NVM_DIR/nvm.sh&quot;  # This loads nvm&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# [ -s &quot;$NVM_DIR/bash_completion&quot; ] &amp;amp;&amp;amp; &#92;. &quot;$NVM_DIR/bash_completion&quot;  # This loads nvm bash_completion&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add these lines (with the correct path for where you saved the &lt;code&gt;zsh-nvm&lt;/code&gt; file):&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;NVM_LAZY_LOAD&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;true
&lt;span class=&quot;token builtin class-name&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/Users/varun/.zsh-nvm.zsh&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;zsh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# --&gt; zsh -i -c echo  0.38s user 0.40s system 94% cpu 0.814 total&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;zsh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# --&gt; zsh -i -c echo  0.06s user 0.08s system 84% cpu 0.173 total&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>How to hack the Breakthrough Prize (ft. Session Confusion)</title>
    <link href="https://varun.ch/posts/breakthrough/" />
    <updated>2024-09-25T00:00:00Z</updated>
    <id>https://varun.ch/posts/breakthrough/</id>
    <content type="html">&lt;p&gt;The Breakthrough Junior Challenge is an annual, global science video competition for high-school students. It’s run by Breakthrough Initiatives, the same organization that runs the &lt;a href=&quot;https://breakthroughprize.org/&quot;&gt;Breakthrough Prize events&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In 2023, I discovered a critical vulnerability in the Breakthrough Challenge website. After over one year since it was patched, I am disclosing the bug for transparency. I believe this class of vulnerability, which I am introducing as ‘Session Confusion’, is often overlooked.&lt;/p&gt;
&lt;h2 id=&quot;set-up&quot; tabindex=&quot;-1&quot;&gt;Set up &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#set-up&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;While waiting for a video call related to an &lt;a href=&quot;https://www.youtube.com/watch?v=gzc1p_QlO2I&quot;&gt;unrelated incident&lt;/a&gt;, I got bored and clicked around at my bookmarks. I rediscovered the Breakthrough Junior Challenge website that I submitted my entry to the year before.&lt;/p&gt;
&lt;p&gt;The Breakthrough Challenge website allows participants to register accounts, which are used to submit personal details and video entries.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/breakthrough/breakthrough-signup.webp 1400w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/breakthrough/breakthrough-signup.png&quot; alt=&quot;Breakthrough signup page&quot; width=&quot;1400&quot; height=&quot;824&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;On a surface level, the website looks like it only exists for students.&lt;/p&gt;
&lt;p&gt;But surely there has to be some kind of interface for the Breakthrough board to review videos, right?&lt;/p&gt;
&lt;h2 id=&quot;subdomains&quot; tabindex=&quot;-1&quot;&gt;Subdomains &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#subdomains&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Subdomain enumeration is an extremely effective way to quickly discover the surface area of a web service. I found cp.breakthroughjuniorchallenge.org, which redirects to a login page.&lt;/p&gt;
&lt;h2 id=&quot;control-panel&quot; tabindex=&quot;-1&quot;&gt;Control… Panel…? &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#control-panel&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This was a barebones login page - these are always fun, because it suggests whatever behind it is for internal use only.&lt;/p&gt;
&lt;p&gt;Unsurprisingly, attempting to log in with my own account doesn’t work.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/breakthrough/breakthrough-cp-login.webp 610w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/breakthrough/breakthrough-cp-login.png&quot; alt=&quot;Breakthrough control panel login&quot; width=&quot;610&quot; height=&quot;382&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;After trying different URLs, I quickly discovered that pages from breakthroughjuniorchallenge.org can be reached via cp.breakthroughjuniorchallenge.org, this suggests the applications are related in some way.&lt;/p&gt;
&lt;h2 id=&quot;session-confusion&quot; tabindex=&quot;-1&quot;&gt;Session Confusion &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#session-confusion&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Exploring the cp.breakthroughjuniorchallenge.org domain further, I noticed that the login page’s structure mirrored the main site (minus styling). This raised a question: could the session management system be the same for both the public and internal sites?&lt;/p&gt;
&lt;p&gt;To test this, I logged into my regular user account on breakthroughjuniorchallenge.org and captured the session cookie. Then, I tried to use the same &lt;code&gt;laravel_session&lt;/code&gt; cookie on cp.breakthroughjuniorchallenge.org by simply changing the cookie’s domain scope to &lt;code&gt;.breakthroughjuniorchallenge.org&lt;/code&gt; so it would be sent along on subdomains too.&lt;/p&gt;
&lt;p&gt;Astonishingly, this worked. The internal control panel accepted my public session cookie, granting me access to administrative functions. This confirmed a serious vulnerability: the session tokens were not segregated between sites, leading to a case of “Session Confusion” (my own name).&lt;/p&gt;
&lt;p&gt;It occurs in systems where session tokens, such as JWTs, are shared across multiple services without proper isolation. While the tokens themselves are signed and secure, the issue comes from the same session signing mechanism (the secret key) being used across both public-facing and administrative sites. This creates a security loophole, as a token issued for one context (e.g., a public site) can be accepted by another (e.g., an internal control panel), granting unintended access. In this case, the shared secret used to sign the session tokens allowed the internal site to mistakenly trust a session created for the public site, leading to unauthorized access to administrative functions.&lt;/p&gt;
&lt;h2 id=&quot;unauthorized-access&quot; tabindex=&quot;-1&quot;&gt;Unauthorized Access &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#unauthorized-access&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Once inside the control panel, I found various administrative functionalities, including the ability to access user data, settings that alter the state of the competition and a directory of users. Fortunately, some of the interface was broken (relating to user management) - likely a side effect of my account not really existing on the admin site.&lt;/p&gt;
&lt;p&gt;I would imagine that it would be possible to bypass the errors to make full use of the admin panel, but I did not want to access any sensitive data.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/breakthrough/breakthrough-admin-panel.webp 1216w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/breakthrough/breakthrough-admin-panel.png&quot; alt=&quot;Breakthrough admin panel&quot; width=&quot;1216&quot; height=&quot;816&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;I tried to create a page on the root (expecting an error), but to my continued surprise, it went through and I had just edited the live running site. Oops.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/breakthrough/breakthrough-poc-page.webp 942w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/breakthrough/breakthrough-poc-page.png&quot; alt=&quot;Proof of concept page I added to the Breakthrough website&quot; width=&quot;942&quot; height=&quot;516&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;reporting&quot; tabindex=&quot;-1&quot;&gt;Reporting &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#reporting&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I immediately undid any changes, logged out and reported this vulnerability via email detailing each step I took and the changes I made (just the creation and deletion of one page followed by invalidating my session).&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; tabindex=&quot;-1&quot;&gt;Conclusion &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#conclusion&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This bug would have allowed an unprivileged user to access some user data, modify competition state and vandalize the Breakthrough Junior Challenge website. The Breakthrough team responded extremely quickly, and the issue was fixed just 2 hours after I initially reported the vulnerability.&lt;/p&gt;
&lt;h2 id=&quot;timeline&quot; tabindex=&quot;-1&quot;&gt;Timeline &lt;a class=&quot;header-anchor&quot; href=&quot;https://varun.ch/posts/breakthrough/#timeline&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;16:48 CET Jun 14, 2023: Email sent&lt;/li&gt;
&lt;li&gt;18:55 CET Jun 14, 2023: Vulnerability acknowledged, “We believe we have fixed the problem.”&lt;/li&gt;
&lt;li&gt;15:47 CET Jun 16, 2023: I acknowledge their reply, and confirm I can no longer reproduce the issue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I asked on 12:34 CET Jun 24, 2023 and 12:36 CET Jun 23, 2024 about publishing this writeup. I did not receive a response. I am publishing this blog post today (with some details redacted) for transparency.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>There&#39;s something fishy about the Flappy Bird revival</title>
    <link href="https://varun.ch/posts/flappy-bird/" />
    <updated>2024-09-12T00:00:00Z</updated>
    <id>https://varun.ch/posts/flappy-bird/</id>
    <content type="html">&lt;p&gt;Ten years after its sudden disappearance, Flappy Bird is making a comeback — but something about this revival doesn’t quite add up.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-article.webp 2950w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-article.png&quot; alt=&quot;An article about Flappy Bird&quot; width=&quot;2950&quot; height=&quot;2014&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Articles like this around the web published today talk about the recent ‘The Flappy Bird Foundation Group’, which supposedly “secured” the rights to the Flappy Bird trademark, and is relaunching the once acclaimed game in 2025.&lt;/p&gt;
&lt;p&gt;I’m not going to pretend to be a laywer, but &lt;a href=&quot;https://ttabvue.uspto.gov/ttabvue/v?pnam=Dong%20Nguyen%20Ha%20%20&quot;&gt;this USPTO page&lt;/a&gt; seems to suggest the trademark expired and was claimed by one “GAMETECH HOLDINGS, LLC” in 2023. &lt;a href=&quot;https://www.ign.com/articles/10-years-after-it-was-pulled-offline-viral-mobile-game-flappy-bird-is-coming-back&quot;&gt;And this IGN article&lt;/a&gt; says that the trademark was then “acquired” by “The Flappy Bird Foundation” some time later. The original developer of Flappy Bird does not seem to be involved in any of this whatsoever.&lt;/p&gt;
&lt;p&gt;Apart from the strange timeline for the re-release (the original Flappy Bird was developed in &lt;a href=&quot;https://web.archive.org/web/20140206211056/http://www.thechocolatelabapps.com/how-to-make-flappy-bird/&quot;&gt;two to three days&lt;/a&gt;), I found the new &lt;a href=&quot;https://flappybird.org&quot;&gt;flappybird.org&lt;/a&gt; website a little stranger.&lt;/p&gt;
&lt;p&gt;With its overly polished look, the art feels eerily similar to the countless clones that have flooded the market over the years. After all, Flappy Bird is famously one of the most cloned games of all time. So, what’s really different about this “official” revival?&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-1.webp 2948w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-1.png&quot; alt=&quot;The flappybird.org home page on September 12th 2024&quot; width=&quot;2948&quot; height=&quot;2008&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The flappybird.org home page on September 12th 2024… Telegram!?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It feels quite polished for what seems to be something made by a “team of passionate fans committed to sharing the game with the world”. Who is behind this, and what’s the goal? How are they making money? The trademark couldn’t have been free, so what’s the plan?&lt;/p&gt;
&lt;p&gt;A quick google search for &lt;code&gt;site:flappybird.org&lt;/code&gt; reveals some pages on the site.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-google.webp 1470w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-google.png&quot; alt=&quot;Google results for site:flappybird.org&quot; width=&quot;1470&quot; height=&quot;1752&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Some appear to be simple variations of the landing page (ranging from minor copywriting changes to a “Coming Soon” page), and one appears to be a placeholder for a future merch shop, but there are a couple pages that really stand out.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-community.webp 2958w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-community.png&quot; alt=&quot;The community page&quot; width=&quot;2958&quot; height=&quot;2016&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The community page includes some of the same information from the home page, but also includes links for “Collaborations”, “Interviews” and a Press Kit. The only link that works is the Collaborations reach out button.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-collabs.webp 2962w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-collabs.png&quot; alt=&quot;The collaborations page&quot; width=&quot;2962&quot; height=&quot;2012&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The collaborations page is barebones, but suggests there are some kind of upcoming Flappy Bird game jams. Interesting!&lt;/p&gt;
&lt;p&gt;The last page “3-$Flap” reveals what this all may really be about.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-crypto.webp 2962w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-crypto.png&quot; alt=&quot;The crypto page&quot; width=&quot;2962&quot; height=&quot;2006&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Yay! Crypto!&lt;/p&gt;
&lt;p&gt;The page states that the “legendary Flappy Bird™ is back and will fly higher than ever on Solana as it soars into Web 3.0” and that “Artists, developers and creators can build, play and earn from the legendary Flappy Bird IP” and “Flappy Bird will now be the world’s first open-source, community owned Web 2 and Web 3 game.”&lt;/p&gt;
&lt;p&gt;This page seems to suggest that the original plan of the project revolved around cryptocurrency and “Web 3.0”. Some people may refer to these kinds of projects as “grifts”.&lt;/p&gt;
&lt;p&gt;I also noticed that the website is built with WordPress. As a result, its sitemap is fully public, which allows us to enumerate the site’s pages.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-sitemap.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-sitemap.png&quot; alt=&quot;The flappybird.org sitemap.xml&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;So many pages!&lt;/p&gt;
&lt;p&gt;Using this, I found that the oldest pages seem to be from around December 2023, and many of these pages reference ‘web3’ and crypto in a few places.&lt;/p&gt;
&lt;p&gt;Furthermore, &lt;a href=&quot;https://flappybird.org/flappywebgl/&quot;&gt;/flappywebgl&lt;/a&gt; and &lt;a href=&quot;https://flappybird.org/flappywebgl2/&quot;&gt;/flappywebgl2&lt;/a&gt; actually feature some kind of prototype of the game itself, which is neat.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-loading.webp 2959w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-loading.png&quot; alt=&quot;The loading screen, which prominently alludes to a &#39;Flap Token&#39;&quot; width=&quot;2959&quot; height=&quot;2014&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The loading screen, which prominently alludes to a ‘Flap Token’ on &lt;a href=&quot;https://ton.org/&quot;&gt;‘TON’&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-webgl.webp 2966w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-webgl.png&quot; alt=&quot;Flappy Bird prototype&quot; width=&quot;2966&quot; height=&quot;2016&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;It’s just a standard Flappy Bird clone, nothing particularly special there. But the game is embedded from a Google Cloud bucket… with the directory listing enabled!&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-listing.webp 2272w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-listing.png&quot; alt=&quot;The flappybird bucket file listing&quot; width=&quot;2272&quot; height=&quot;1506&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Oops!&lt;/p&gt;
&lt;p&gt;There are two builds that are particularly interesting here. One is dated &lt;code&gt;2024-08-08&lt;/code&gt; and is titled &lt;code&gt;v1.4.2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a far more developed version of the game from the simple &lt;code&gt;flappywebgl&lt;/code&gt; prototypes embedded on the WordPress blog. It’s playable and has a polished interface. Interestingly, while the &lt;code&gt;flappywebgl&lt;/code&gt; used a distinct character design, this version uses the iconic Flappy bird. Perhaps they acquired the trademark some time in between?&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-crypto.webp 1196w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-crypto.png&quot; alt=&quot;Build v1.4.2 homescreen&quot; width=&quot;1196&quot; height=&quot;2028&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The game’s homepage.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-screenshot.webp 1180w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-screenshot.png&quot; alt=&quot;Mid-game screenshot of the v1.4.2 build&quot; width=&quot;1180&quot; height=&quot;2016&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Addictive game is addictive.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-connect-wallet.webp 1024w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-connect-wallet.png&quot; alt=&quot;Connect your wallet&quot; width=&quot;1024&quot; height=&quot;924&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Oh no&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-wallet.webp 2904w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-wallet.png&quot; alt=&quot;List of wallets you can connect&quot; width=&quot;2904&quot; height=&quot;2012&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;I don’t like this…&lt;/p&gt;
&lt;p&gt;&lt;code&gt;test19&lt;/code&gt; is dated &lt;code&gt;2024-09-12&lt;/code&gt; (today), and is a debug build of a much rougher game.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-rough.webp 1168w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-rough.png&quot; alt=&quot;Build test19&quot; width=&quot;1168&quot; height=&quot;2030&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;All the buttons that worked in the August build have been removed. What could this mean!?&lt;/p&gt;
&lt;p&gt;Both builds feature a news section that loads data from &lt;code&gt;staging-api.flappybird.org&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-news.webp 1158w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-news.png&quot; alt=&quot;Screenshot of news section in this build&quot; width=&quot;1158&quot; height=&quot;2036&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;TAP, FLAP AND WIN! What does this mean!?&lt;/p&gt;
&lt;p&gt;The game also loads in a leaderboard.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-api.webp 2166w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-game-api.png&quot; alt=&quot;Flappy Bird leaderboard from the API&quot; width=&quot;2166&quot; height=&quot;1442&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The request the web game makes to the &lt;code&gt;staging-api.flappybird.org/api/v1/leaderboard&lt;/code&gt; endpoint&lt;/p&gt;
&lt;p&gt;Naturally, the same endpoint on &lt;code&gt;api.flappybird.org&lt;/code&gt; returns a similar list, although a little longer.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-leaderboard-prod.webp 996w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-leaderboard-prod.png&quot; alt=&quot;Leaderboard from the production API&quot; width=&quot;996&quot; height=&quot;1802&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Maybe this game is already available in some kind of early access? I doubt there’s this large of a team actually developing the game.&lt;/p&gt;
&lt;p&gt;Some Google searches show that these names are the usernames of various “Crypto Influencers”. Of course, there is no way to verify these are the same people, but some of them follow either @flappy_bird or @ton_blockchain on Twitter.&lt;/p&gt;
&lt;p&gt;I was also able to find the Twitter account of the person responsible. They link to their game development company “1208 Productions”&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-twitter.webp 1208w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-twitter.png&quot; alt=&quot;The Twitter page&quot; width=&quot;1208&quot; height=&quot;1922&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;“might of”&lt;/p&gt;
&lt;p&gt;Their website states that they are “pioneering Web 3”.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-1208.webp 640w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-1208.png&quot; alt=&quot;The 1208 Productions website&quot; width=&quot;640&quot; height=&quot;430&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;“PIONEERING WEB 3”&lt;/p&gt;
&lt;p&gt;The website lists various crypto projects, including ‘Deez’, an NFT brand. They also list some notable figures they “consulted with”.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-deez.webp 1920w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/flappy-bird/flappy-bird-deez.png&quot; alt=&quot;1208 Productions&#39; Deez project and notable figures&quot; width=&quot;1920&quot; height=&quot;1305&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Wait what?? Jordan Belfort??&lt;/p&gt;
&lt;p&gt;I’m up too late looking at this. This exploration definitely helped me gain a greater understanding of what we’re looking at, but it still left me with so many questions.&lt;/p&gt;
&lt;p&gt;Does this Flappy Bird revival have ‘web3’ and cryptocurrency roots? Absolutely.&lt;/p&gt;
&lt;p&gt;Is it strange that none of the initial reporting on today’s launch mentions the web3 aspect of the game? Yes.&lt;/p&gt;
&lt;p&gt;Is this Flappy Bird revival a &lt;a href=&quot;https://x.com/dusktodusk94/status/1834244074531922069&quot;&gt;“crypto ponzi scheme grift”&lt;/a&gt;? I don’t know.&lt;/p&gt;
&lt;p&gt;Is this Flappy bird revival authorized by the original creator? Almost certainly not.&lt;/p&gt;
&lt;p&gt;Is this a somewhat shady project with the goal of capitalizing off the nostalgic appeal of a beloved game while quietly aiming to make money from cryptocurrency and Web3 integration? It sure seems like it.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Most 16-year-olds don’t have servers in their rooms</title>
    <link href="https://varun.ch/posts/server/" />
    <updated>2023-12-20T00:00:00Z</updated>
    <id>https://varun.ch/posts/server/</id>
    <content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/server/server3.webp 1440w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/server/server3.jpeg&quot; alt=&quot;Dell R720XD on an undersized coffee table, messy cables and all&quot; width=&quot;1440&quot; height=&quot;1080&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;My servers: a Dell OptiPlex on the floor and a PowerEdge R720XD on an undersized IKEA coffee table. All networked together with beautiful red and yellow ethernet cables.&lt;/p&gt;
&lt;p&gt;Hi, I’m Varun. I have a server in my room. Actually, I have two of them. And I write a ton of my own code for them. And I love it. This is a story documenting my homelab’s quiet beginnings, its spiralling evolution, and its uncertain future.&lt;/p&gt;
&lt;p&gt;Today, I self-host a ton of stuff on my servers at home, including…&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A home-made social media platform with thousands of users&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://quickz.org&quot;&gt;Quickz&lt;/a&gt;, my realtime quiz game for schools&lt;/li&gt;
&lt;li&gt;A demo instance of my MYP Personal Project, which was &lt;a href=&quot;https://varun.ch/projects/flopbook&quot;&gt;a platform for creating and sharing flipbook-style animations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tools for the &lt;a href=&quot;https://scratch.mit.edu&quot;&gt;Scratch&lt;/a&gt; community, such as a search engine&lt;/li&gt;
&lt;li&gt;A handful of game servers for my friends&lt;/li&gt;
&lt;li&gt;A few &lt;a href=&quot;https://plausible.io&quot;&gt;Plausible Analytics&lt;/a&gt; instances&lt;/li&gt;
&lt;li&gt;And a Replit-like tool for creating and hosting tons of experimental projects&lt;/li&gt;
&lt;li&gt;And so much more…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It all started during lockdown in 2020. I was 13 years old and had a little too much free time. I decided to delve into web development with static HTML websites, and before long, I was building more complex apps that required backends. I used Replit to host web apps, and random scripts (Discord bots, automation, etc.), and I ‘hosted’ Minecraft servers for my friends by simply leaving my computer on.&lt;/p&gt;
&lt;p&gt;I wanted more control and reliability so I jumped head-first into the wonderful world of self-hosting.&lt;/p&gt;
&lt;p&gt;My original rationale for choosing to host at home rather than cheap cloud options was purely out of a desire to learn (at my scale, it’s probably more cost effective to pay for The Cloud). I wanted to learn the whole stack, from user-facing frontends, to the backends and databases that support them, to the servers that actually host them. All I needed was a computer (that wasn’t my desktop) online 24/7.&lt;/p&gt;
&lt;p&gt;I got my hands on a Dell OptiPlex with an i7-3770S and 8GB of RAM from eBay. The process of getting it shipped from the US to Switzerland was actually a little frustrating&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/server/#shipping-note&quot; id=&quot;shipping-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[1]&lt;/a&gt;, with the seller initially sending the wrong amount of RAM and refusing to compromise. But after several weeks, everything was sorted out. I decided to stick with the pre-installed Windows 10 Pro (yes, as a server OS!) simply because I was familiar with it.&lt;/p&gt;
&lt;p&gt;Fast forward a few months, and this is how the desktop looked.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/server/windows-10-screenshot.webp 800w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/server/windows-10-screenshot.jpeg&quot; alt=&quot;Screenshot of my server running Windows 10. cmd windows neatly tiled.&quot; width=&quot;800&quot; height=&quot;464&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;I got very lucky with one thing: as it turns out, we have a static IPv4 address at home, making it significantly easier and more stable to host websites&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/server/#static-ip-note&quot; id=&quot;static-ip-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[2]&lt;/a&gt;. I keep everything behind Cloudflare, which sorts out SSL, DNS, and supposedly even DDoS protection too, for free.&lt;/p&gt;
&lt;p&gt;There were several benefits to using Windows 10 as a server OS (for me at the time):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I was already familiar with Windows&lt;/li&gt;
&lt;li&gt;I could test everything in a similar environment on my own desktop&lt;/li&gt;
&lt;li&gt;I could use RDP to get a fast high-quality connection to my server from home.&lt;/li&gt;
&lt;li&gt;I could use Chrome Remote Desktop to securely connect to it from school (I had to use something web-based if I wanted to access it from my school-provided Chromebook).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But after enough Windows updates forced me to deal with downtime and retile my windows to a pretty layout, I decided to make the scary jump to Ubuntu Server in November 2020. For some reason, I was entirely terrified by this. I had grown comfortable with my cozy Windows setup, and I was worried that I was going to lose control and the ability to have a quick overview of my system. But alas, it was time for change.&lt;/p&gt;
&lt;p&gt;I got going with Linux pretty fast - I had already been fairly comfortable with the command line on Windows while writing code, and it turns out, the Unix command line is quite similar.&lt;/p&gt;
&lt;p&gt;My setup looked something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nginx (serving HTTPS traffic on port 443 with self-signed certificates) exposed to the internet, rejecting requests not from Cloudflare.&lt;/li&gt;
&lt;li&gt;Lots of random projects that I wanted to keep online 24/7.&lt;/li&gt;
&lt;li&gt;One MongoDB instance running, shared across my projects.&lt;/li&gt;
&lt;li&gt;A couple Minecraft Java servers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/billchurch/webssh2&quot;&gt;webssh2&lt;/a&gt; exposed so I could still access my server from school. (this was probably so insecure!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When needed, I would install third-party software, which occasionally needed their own dependencies and apt repositories, etc. This got very messy, very quickly. That’s when I discovered Docker, which was exactly the magic I needed. I use it to simplify installing and running third party software, while all my own code still runs directly on the server.&lt;/p&gt;
&lt;p&gt;Fast forward around a year, I’m hosting some projects that actually have users (like &lt;a href=&quot;https://quickz.org&quot;&gt;Quickz&lt;/a&gt; and a social media platform I made) - and my server crashes while I’m at school. No big deal, I’ll just ssh in and see what’s wrong. Except I kept getting weird disk errors before finally getting locked out.&lt;/p&gt;
&lt;p&gt;It turns out, I had simply run out of storage, deleting a few files at home bought me some more time. Moving forward, I had two options: replace the hard drive with something bigger, or let that be a problem for future-me.&lt;/p&gt;
&lt;p&gt;I chose the third: explore getting a new server. At my current usage, the Dell OptiPlex was being pushed to its limits - 8GB of RAM and an i7-3770S weren’t really cutting it anymore.&lt;/p&gt;
&lt;p&gt;In May 2022, I finalized the crazy decision to get a real rack-mounted server - I got a great deal on used drives (3x3TB), RAM (128GB of DDR3), and relatively powerful (read: power-hungry) CPUs (2xE5-2690) in a Dell PowerEdge R720XD, which I paid for with some bug bounty money I saved up.&lt;/p&gt;
&lt;p&gt;I did not, however, consider the amount of space it would take up, the noise it would make, or the power it would draw. I solved ⅓ of those problems by using an unsupported hack to force iDRAC to limit the fan speed to a constant 10%.&lt;/p&gt;
&lt;p&gt;My first impressions went something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wow this is heavy&lt;/li&gt;
&lt;li&gt;Really heavy…&lt;/li&gt;
&lt;li&gt;There’s two of nearly everything in here. Two power supplies, two CPUs, two NICs, there are actually even two computers here! The R720XD has iDRAC, which is basically a mini computer that stays on so the server can be managed remotely, even when it’s turned off. Awesome!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/server/server1.webp 1440w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/server/server1.jpeg&quot; alt=&quot;My server resting on an IKEA coffee table next to a TV&quot; width=&quot;1440&quot; height=&quot;1080&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The server rested precariously on a coffee table next to the TV in the living room for a little bit. I’m sorry to my parents who had to put up with that&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/server/#lies-note&quot; id=&quot;lies-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[3]&lt;/a&gt;. It turns out, a rack and mounting equipment would have cost more than the server itself.&lt;/p&gt;
&lt;p&gt;This time, I decided to use a hypervisor&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/server/#hostnames-note&quot; id=&quot;hostnames-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[4]&lt;/a&gt;. The server runs Proxmox, while the majority of the resources are dedicated to an Ubuntu Server VM (where everything runs neatly in Docker now). This gives me the flexibility to easily move things around in the future without having to set up everything again.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/server/proxmox.webp 1180w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/server/proxmox.png&quot; alt=&quot;A screenshot of my Proxmox interface&quot; width=&quot;1180&quot; height=&quot;584&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;It also gives me space to run a Xubuntu VM named ‘backdoor’ that I use Chrome Remote Desktop on. It lets me access my home network in a comfortable workspace from anywhere in the world. Yes, I could set up a VPN (like OpenVPN or WireGuard), but this even works on (artificially) limited devices like the iPad and works on my school’s public WiFi. And as a bonus, I don’t have to worry about bots trying to bruteforce their way in. It might sound crazy, but this approach works really well&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://varun.ch/posts/server/#not-crazy-note&quot; id=&quot;not-crazy-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ironically after all of this, I ended up missing something about the Replit experience. I still used Replit fairly often to spin up quick isolated applications for one-off projects/experiments and demos (eg. a tool for my friends to grab their portraits from the school website). Nothing beated the workflow of writing code in the web based IDE and having it live and accessible to everyone on the web immediately.&lt;/p&gt;
&lt;p&gt;It was amazing how Replit was free, but (entirely understandably) they started to implement features to actually make money. Inactive repls get disabled if nobody uses them, and as far as I understand, &lt;a href=&quot;https://blog.replit.com/hosting-changes&quot;&gt;it’s not even possible to host public dynamic websites for free anymore&lt;/a&gt;. I also wanted to use my own hardware and domain I controlled.&lt;/p&gt;
&lt;p&gt;So I built my own self-hosted Replit clone. Built on Docker, &lt;a href=&quot;https://varun.ch/projects/dockn&quot;&gt;‘Dock’n’Roll’&lt;/a&gt; is a single-user service where I can code a web app under a private interface at dockn.varun.ch, and have it accessible to the world live under *.varunbiniwale.com. I love ‘dockn’ for its simplicity and versatility, I use it all the time.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/server/dockn1.webp 378w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/server/dockn1.png&quot; alt=&quot;Dock’n’Roll’s admin interface, listing projects&quot; width=&quot;378&quot; height=&quot;413&quot;&gt;&lt;/picture&gt; &lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/server/dockn2.webp 1178w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/server/dockn2.png&quot; alt=&quot;Dock’n’Roll’s admin interface, showing the IDE&quot; width=&quot;1178&quot; height=&quot;579&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The control panel, built in ExpressJS, has a simple web interface and Monaco Editor (from VSCode!) for me to create projects and manage their files.&lt;/li&gt;
&lt;li&gt;The interface has a button to start a project, which uses Docker’s engine API to build an image and run a container.&lt;/li&gt;
&lt;li&gt;Project metadata is stored in Redis.&lt;/li&gt;
&lt;li&gt;Port 80 in each container is automatically exposed as :project.varunbiniwale.com using OpenResty (talking to Redis with a Lua plugin) as a reverse proxy.&lt;/li&gt;
&lt;li&gt;Files can be stored under /persisted/ and are persisted across container restarts. They can be viewed/edited in the web interface.&lt;/li&gt;
&lt;li&gt;The web admin interface is protected using a &lt;a href=&quot;https://auth.varun.ch&quot;&gt;custom nginx auth proxy&lt;/a&gt; I made for all of my projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;‘Dock’n’Roll’ is also very useful for bug hunting, I use it to host random things I use to help me discover vulnerabilities like blind SSRFs and XSSs. Here’s an autogenerated image hosted at yourip.varunbiniwale.com.&lt;/p&gt;
&lt;img src=&quot;https://yourip.varunbiniwale.com/demo&quot;&gt;
&lt;p&gt;And here’s a (slightly outdated) demo video&lt;/p&gt;
&lt;style&gt;lite-youtube {
    background-color: #000;
    position: relative;
    display: block;
    contain: content;
    background-position: center center;
    background-size: cover;
    cursor: pointer;
    max-width: 720px;
}

/* gradient */
lite-youtube::before {
    content: attr(data-title);
    display: block;
    position: absolute;
    top: 0;
    /* Pixel-perfect port of YT&#39;s gradient PNG, using https://github.com/bluesmoon/pngtocss plus optimizations */
    background-image: linear-gradient(180deg, rgb(0 0 0 / 67%) 0%, rgb(0 0 0 / 54%) 14%, rgb(0 0 0 / 15%) 54%, rgb(0 0 0 / 5%) 72%, rgb(0 0 0 / 0%) 94%);
    height: 99px;
    width: 100%;
    font-family: &quot;YouTube Noto&quot;,Roboto,Arial,Helvetica,sans-serif;
    color: hsl(0deg 0% 93.33%);
    text-shadow: 0 0 2px rgba(0,0,0,.5);
    font-size: 18px;
    padding: 25px 20px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    box-sizing: border-box;
}

lite-youtube:hover::before {
    color: white;
}

/* responsive iframe with a 16:9 aspect ratio
    thanks https://css-tricks.com/responsive-iframes/
*/
lite-youtube::after {
    content: &quot;&quot;;
    display: block;
    padding-bottom: calc(100% / (16 / 9));
}
lite-youtube &gt; iframe {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
    border: 0;
}

/* play button */
lite-youtube &gt; .lty-playbtn {
    display: block;
    /* Make the button element cover the whole area for a large hover/click target… */
    width: 100%;
    height: 100%;
    /* …but visually it&#39;s still the same size */
    background: no-repeat center/68px 48px;
    /* YT&#39;s actual play button svg */
    background-image: url(&#39;data:image/svg+xml;utf8,&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 68 48&quot;&gt;&lt;path d=&quot;M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z&quot; fill=&quot;red&quot;/&gt;&lt;path d=&quot;M45 24 27 14v20&quot; fill=&quot;white&quot;/&gt;&lt;/svg&gt;&#39;);
    position: absolute;
    cursor: pointer;
    z-index: 1;
    filter: grayscale(100%);
    transition: filter .1s cubic-bezier(0, 0, 0.2, 1);
    border: 0;
}

lite-youtube:hover &gt; .lty-playbtn,
lite-youtube .lty-playbtn:focus {
    filter: none;
}

/* Post-click styles */
lite-youtube.lyt-activated {
    cursor: unset;
}
lite-youtube.lyt-activated::before,
lite-youtube.lyt-activated &gt; .lty-playbtn {
    opacity: 0;
    pointer-events: none;
}

.lyt-visually-hidden {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }
&lt;/style&gt;
&lt;script&gt;/**
 * A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
 *
 * Thx to these as the inspiration
 *   https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
 *   https://autoplay-youtube-player.glitch.me/
 *
 * Once built it, I also found these:
 *   https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍)
 *   https://github.com/Daugilas/lazyYT
 *   https://github.com/vb/lazyframe
 */
class LiteYTEmbed extends HTMLElement {
    connectedCallback() {
        this.videoId = this.getAttribute(&#39;videoid&#39;);

        let playBtnEl = this.querySelector(&#39;.lty-playbtn&#39;);
        // A label for the button takes priority over a [playlabel] attribute on the custom-element
        this.playLabel = (playBtnEl &amp;&amp; playBtnEl.textContent.trim()) || this.getAttribute(&#39;playlabel&#39;) || &#39;Play&#39;;

        this.dataset.title = this.getAttribute(&#39;title&#39;) || &quot;&quot;;

        /**
         * Lo, the youtube poster image!  (aka the thumbnail, image placeholder, etc)
         *
         * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md
         */
        if (!this.style.backgroundImage) {
          this.style.backgroundImage = `url(&quot;https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg&quot;)`;
          this.upgradePosterImage();
        }

        // Set up play button, and its visually hidden label
        if (!playBtnEl) {
            playBtnEl = document.createElement(&#39;button&#39;);
            playBtnEl.type = &#39;button&#39;;
            playBtnEl.classList.add(&#39;lty-playbtn&#39;);
            this.append(playBtnEl);
        }
        if (!playBtnEl.textContent) {
            const playBtnLabelEl = document.createElement(&#39;span&#39;);
            playBtnLabelEl.className = &#39;lyt-visually-hidden&#39;;
            playBtnLabelEl.textContent = this.playLabel;
            playBtnEl.append(playBtnLabelEl);
        }

        this.addNoscriptIframe();

        // for the PE pattern, change anchor&#39;s semantics to button
        if(playBtnEl.nodeName === &#39;A&#39;){
            playBtnEl.removeAttribute(&#39;href&#39;);
            playBtnEl.setAttribute(&#39;tabindex&#39;, &#39;0&#39;);
            playBtnEl.setAttribute(&#39;role&#39;, &#39;button&#39;);
            // fake button needs keyboard help
            playBtnEl.addEventListener(&#39;keydown&#39;, e =&gt; {
                if( e.key === &#39;Enter&#39; || e.key === &#39; &#39; ){
                    e.preventDefault();
                    this.activate();
                }
            });
        }

        // On hover (or tap), warm up the TCP connections we&#39;re (likely) about to use.
        this.addEventListener(&#39;pointerover&#39;, LiteYTEmbed.warmConnections, {once: true});
        this.addEventListener(&#39;focusin&#39;, LiteYTEmbed.warmConnections, {once: true});

        // Once the user clicks, add the real iframe and drop our play button
        // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
        //   We&#39;d want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
        this.addEventListener(&#39;click&#39;, this.activate);

        // Chrome &amp; Edge desktop have no problem with the basic YouTube Embed with ?autoplay=1
        // However Safari desktop and most/all mobile browsers do not successfully track the user gesture of clicking through the creation/loading of the iframe,
        // so they don&#39;t autoplay automatically. Instead we must load an additional 2 sequential JS files (1KB + 165KB) (un-br) for the YT Player API
        // TODO: Try loading the the YT API in parallel with our iframe and then attaching/playing it. #82
        this.needsYTApi = this.hasAttribute(&quot;js-api&quot;) || navigator.vendor.includes(&#39;Apple&#39;) || navigator.userAgent.includes(&#39;Mobi&#39;);
    }

    /**
     * Add a &lt;link rel={preload | preconnect} ...&gt; to the head
     */
    static addPrefetch(kind, url, as) {
        const linkEl = document.createElement(&#39;link&#39;);
        linkEl.rel = kind;
        linkEl.href = url;
        if (as) {
            linkEl.as = as;
        }
        document.head.append(linkEl);
    }

    /**
     * Begin pre-connecting to warm up the iframe load
     * Since the embed&#39;s network requests load within its iframe,
     *   preload/prefetch&#39;ing them outside the iframe will only cause double-downloads.
     * So, the best we can do is warm up a few connections to origins that are in the critical path.
     *
     * Maybe `&lt;link rel=preload as=document&gt;` would work, but it&#39;s unsupported: http://crbug.com/593267
     * But TBH, I don&#39;t think it&#39;ll happen soon with Site Isolation and split caches adding serious complexity.
     */
    static warmConnections() {
        if (LiteYTEmbed.preconnected) return;

        // The iframe document and most of its subresources come right off youtube.com
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://www.youtube-nocookie.com&#39;);
        // The botguard script is fetched off from google.com
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://www.google.com&#39;);

        // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://googleads.g.doubleclick.net&#39;);
        LiteYTEmbed.addPrefetch(&#39;preconnect&#39;, &#39;https://static.doubleclick.net&#39;);

        LiteYTEmbed.preconnected = true;
    }

    fetchYTPlayerApi() {
        if (window.YT || (window.YT &amp;&amp; window.YT.Player)) return;

        this.ytApiPromise = new Promise((res, rej) =&gt; {
            var el = document.createElement(&#39;script&#39;);
            el.src = &#39;https://www.youtube.com/iframe_api&#39;;
            el.async = true;
            el.onload = _ =&gt; {
                YT.ready(res);
            };
            el.onerror = rej;
            this.append(el);
        });
    }

    /** Return the YT Player API instance. (Public L-YT-E API) */
    async getYTPlayer() {
        if(!this.playerPromise) {
            await this.activate();
        }

        return this.playerPromise;
    }

    async addYTPlayerIframe() {
        this.fetchYTPlayerApi();
        await this.ytApiPromise;

        const videoPlaceholderEl = document.createElement(&#39;div&#39;)
        this.append(videoPlaceholderEl);

        const paramsObj = Object.fromEntries(this.getParams().entries());

        this.playerPromise = new Promise(resolve =&gt; {
            let player = new YT.Player(videoPlaceholderEl, {
                width: &#39;100%&#39;,
                videoId: this.videoId,
                playerVars: paramsObj,
                events: {
                    &#39;onReady&#39;: event =&gt; {
                        event.target.playVideo();
                        resolve(player);
                    }
                }
            });
        });
    }

    // Add the iframe within &lt;noscript&gt; for indexability discoverability. See https://github.com/paulirish/lite-youtube-embed/issues/105
    addNoscriptIframe() {
        const iframeEl = this.createBasicIframe();
        const noscriptEl = document.createElement(&#39;noscript&#39;);
        // Appending into noscript isn&#39;t equivalant for mysterious reasons: https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element
        noscriptEl.innerHTML = iframeEl.outerHTML;
        this.append(noscriptEl);
    }

    getParams() {
        const params = new URLSearchParams(this.getAttribute(&#39;params&#39;) || []);
        params.append(&#39;autoplay&#39;, &#39;1&#39;);
        params.append(&#39;playsinline&#39;, &#39;1&#39;);
        return params;
    }

    async activate(){
        if (this.classList.contains(&#39;lyt-activated&#39;)) return;
        this.classList.add(&#39;lyt-activated&#39;);

        if (this.needsYTApi) {
            return this.addYTPlayerIframe(this.getParams());
        }

        const iframeEl = this.createBasicIframe();
        this.append(iframeEl);

        // Set focus for a11y
        iframeEl.focus();
    }

    createBasicIframe(){
        const iframeEl = document.createElement(&#39;iframe&#39;);
        iframeEl.width = 560;
        iframeEl.height = 315;
        // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
        iframeEl.title = this.playLabel;
        iframeEl.allow = &#39;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&#39;;
        iframeEl.allowFullscreen = true;
        // AFAIK, the encoding here isn&#39;t necessary for XSS, but we&#39;ll do it only because this is a URL
        // https://stackoverflow.com/q/64959723/89484
        iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`;
        return iframeEl;
    }

    /**
     * In the spirit of the `lowsrc` attribute and progressive JPEGs, we&#39;ll upgrade the reliable
     * poster image to a higher resolution one, if it&#39;s available.
     * Interestingly this sddefault webp is often smaller in filesize, but we will still attempt it second
     * because getting _an_ image in front of the user if our first priority.
     *
     * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md for more details
     */
    upgradePosterImage() {
         // Defer to reduce network contention.
        setTimeout(() =&gt; {
            const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
            const img = new Image();
            img.fetchPriority = &#39;low&#39;; // low priority to reduce network contention
            img.referrerpolicy = &#39;origin&#39;; // Not 100% sure it&#39;s needed, but https://github.com/ampproject/amphtml/pull/3940
            img.src = webpUrl;
            img.onload = e =&gt; {
                // A pretty ugly hack since onerror won&#39;t fire on YouTube image 404. This is (probably) due to
                // Youtube&#39;s style of returning data even with a 404 status. That data is a 120x90 placeholder image.
                // … per &quot;annoying yt 404 behavior&quot; in the .md
                const noAvailablePoster = e.target.naturalHeight == 90 &amp;&amp; e.target.naturalWidth == 120;
                if (noAvailablePoster) return;

                this.style.backgroundImage = `url(&quot;${webpUrl}&quot;)`;
            }
        }, 100);
    }
}
// Register custom element
customElements.define(&#39;lite-youtube&#39;, LiteYTEmbed);
&lt;/script&gt;
&lt;div id=&quot;nITIMrND0Z0&quot; class=&quot;eleventy-plugin-youtube-embed&quot;&gt;&lt;lite-youtube videoid=&quot;nITIMrND0Z0&quot; style=&quot;background-image: url(&#39;https://i.ytimg.com/vi/nITIMrND0Z0/hqdefault.jpg&#39;);&quot; js-api&gt;&lt;div class=&quot;lty-playbtn&quot;&gt;&lt;/div&gt;&lt;/lite-youtube&gt;&lt;/div&gt;
&lt;p&gt;In conclusion, that’s my homelab — sure, it might be a little crazy, but it’s mine, and I love it.&lt;/p&gt;
&lt;p&gt;One day, I’m going to graduate and move out for university. I don’t know where I’ll go yet, but I probably won’t be taking my servers with me, and I definitely wouldn’t want to burden my parents any more by keeping all my hardware running in their home.&lt;/p&gt;
&lt;p&gt;It may not be for everyone, but if you’re a tinkerer (and willing to live with the consequences), I highly recommend trying out a little self hosting.&lt;/p&gt;
&lt;p&gt;I’ve learned so much from my servers. Being able to self-host tools and have a place under my control to run my projects is liberating. Yes, it’s stressful at times, and perhaps not the most practical setup ever, but I’ve gained tons of skills that I would have never learned otherwise. Thank you Dell PowerEdge R720XD.&lt;/p&gt;
&lt;!-- - - -

## Footnotes

0.  [^](#footnote-link-0) On further thought, surely there were used computers somewhere in Switzerland...
1.  [^](#footnote-link-1) I don’t know why. My ISP offers them under their enterprise plans, but we definitely don’t have one of those. Not complaining though!
2.  [^](#footnote-link-2) It’s now in my sister’s former room (she graduated), but that doesn’t sound as good in a blog post title
3.  [^](#footnote-link-3) I named the first server ‘box’. I named my laptop ‘rectangle’ (it’s shaped like one). I named my desktop ‘triangle’ (the strongest shape), and so naturally I named the new server ‘hypercube’
4.  [^](#footnote-link-4) Google does this too! I found hints of an internal ‘Cloudtop’ service in the JavaScript bundle for Chrome Remote Desktop. It’s [apparently a way for employees to access internal software remotely](https://www.cnbc.com/2021/04/12/google-cloudtop-virtual-desktop-tool-for-employees-only.html). Who’s the crazy one now?

- - - --&gt;
</content>
  </entry>
  <entry>
    <title>Trusted by [your company]</title>
    <link href="https://varun.ch/posts/your-company/" />
    <updated>2022-11-26T00:00:00Z</updated>
    <id>https://varun.ch/posts/your-company/</id>
    <content type="html">&lt;style&gt;
    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
      grid-gap: 1rem;
    }

    .grid&gt;.normal {
      background: rgb(31, 31, 31);
      padding: 1rem;
      display: grid;
      place-items: center;
    }

    .grid&gt;.special {
      background-color: rgb(221, 221, 221);
      padding: 1rem;
      display: grid;
      place-items: center;
      animation: 1s ease-out 0s 1 surprise;
    }

    .grid&gt;div::before {
      content: &quot;&quot;;
      display: block;
      padding-bottom: 100%;
      grid-area: 1/1/2/2;
    }

    .grid&gt;div img {
      width: 100%;
      grid-area: 1/1/2/2;
    }

    .company-logo:not(.users) {
      filter: brightness(0) invert();
    }

    .example {
      max-height: 200px;
      width: auto;
    }

    .captcha * {
      /* TODO: just apply box-sizing: border-box to the whole site? */
      box-sizing: border-box;
    }

    .captcha {
      background-color: rgb(55, 125, 206);
      color: white;
      border-radius: 8px;
      padding: 8px;
      max-width: 75%;
      margin: auto;
    }

    .captcha a {
      color: white;
    }

    .captcha h2 {
      margin: 4px;
      font-weight: bold;
    }

    .captcha h3 {
      font-weight: normal;
    }

    .captcha button {
      background-color: white;
      color: black;
      border: none;
      padding: 10px;
      margin-top: 10px;
      border-radius: 4px;
      font-weight: bold;
      letter-spacing: 0.05em;
      transition: background-color 200ms;
    }

    .captcha button:hover {
      background-color: rgb(231, 231, 231);
    }

    .captcha .game {
      padding: 20px;
      background-color: white;
      display: flex;
      flex-direction: column;
      border-radius: 4px;
    }

    .captcha img {
      margin-left: auto;
      margin-right: auto;
      margin-bottom: 20px;
      width: 100%;
      width: 50%;
    }

    .captcha input {
      width: 100%;
      padding: 10px;
      margin-bottom: 10px;
      border: 1px solid #ccc;
      border-radius: 3px;
    }

		img {
      width: 100%;
      height: auto;
      max-width: 100%;
      margin: 15px auto;
    }

    @keyframes surprise {
      0% {
        transform: scale(0%);
      }

      100% {
        transform: scale(100%);
      }
    }
  &lt;/style&gt;
&lt;p&gt;&lt;em&gt;Make sure you are logged into a Google Workspace account (such as a school or work provided Google account) to experience this post fully. Might not work on reasonable browsers that block third party cookies! (like Safari with “Prevent cross-site tracking” enabled)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This blog is trusted by…&lt;/p&gt;
&lt;div class=&quot;grid&quot;&gt;
    &lt;div class=&quot;normal&quot;&gt;
      &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg&quot; alt=&quot;Google&quot; class=&quot;company-logo&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;normal&quot;&gt;
      &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/9/96/Microsoft_logo_%282012%29.svg&quot; alt=&quot;Microsoft&quot; class=&quot;company-logo&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;normal&quot;&gt;
      &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/6/6e/Adobe_Corporate_logo.svg&quot; alt=&quot;Adobe&quot; class=&quot;company-logo&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;normal&quot;&gt;
      &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/a/a9/Amazon_logo.svg&quot; alt=&quot;Amazon&quot; class=&quot;company-logo&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;normal&quot;&gt;
      &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/b/b8/Lenovo_logo_2015.svg&quot; alt=&quot;Lenovo&quot; class=&quot;company-logo&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;special&quot;&gt;
      &lt;img src=&quot;https://www.google.com/u/0/ac/images/logo.gif?uid=&amp;amp;service=google_gsuite&quot; alt=&quot;Your company&quot; class=&quot;company-logo users&quot;&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;p&gt;I noticed that Google Workspace &lt;a href=&quot;https://support.google.com/a/answer/96474&quot;&gt;gives admins the ability to set a custom logo&lt;/a&gt; that appears in the header of some products, including Gmail and Google Drive.&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/your-company/MxgDsHIixfeBrXsqcMKXzOnFDdMSIZ1AI08b.webp 320w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/your-company/MxgDsHIixfeBrXsqcMKXzOnFDdMSIZ1AI08b.svg&quot; alt=&quot;Example from their website&quot; width=&quot;320&quot; height=&quot;64&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;The image is loaded from the URL &lt;code&gt;https://www.google.com/u/0/ac/images/logo.gif?uid=[a large integer]&amp;amp;service=google_gsuite&lt;/code&gt;, and the value of uid appears to be ignored. This means that the logo.gif image can be loaded cross origin, and additionally the cookies on www.google.com also allow for cross origin images with authentication (so therefore, even when loaded on a non Google webpage, browsers will show the logo specific to your Google Workspace organization!).&lt;/p&gt;
&lt;img src=&quot;https://www.google.com/u/0/ac/images/logo.gif?uid=&amp;amp;service=google_gsuite&quot; alt=&quot;Your company&quot; class=&quot;company-logo users&quot;&gt;
&lt;p&gt;&lt;em&gt;(The 320x132 image probably doesn’t look great when blown up this large.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If your company does not have a logo set up, or you don’t use Google Workspace, it will show “G Suite” as a default image. This is based on the &lt;code&gt;service&lt;/code&gt; parameter. For example, here are the default logos for a few services:&lt;/p&gt;
&lt;h2 id=&quot;service-google-gsuite&quot;&gt;&lt;code&gt;?service=google_gsuite&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/your-company/google_gsuite.webp 320w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/your-company/google_gsuite.png&quot; alt=&quot;service=google_gsuite&quot; width=&quot;320&quot; height=&quot;132&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;service-mail&quot;&gt;&lt;code&gt;?service=mail&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/your-company/mail.webp 143w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/your-company/mail.gif&quot; alt=&quot;service=mail&quot; width=&quot;143&quot; height=&quot;59&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;service-google&quot;&gt;&lt;code&gt;?service=google_*&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/your-company/google_search.webp 116w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/your-company/google_search.png&quot; alt=&quot;service=google_search&quot; width=&quot;116&quot; height=&quot;41&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;service-writely&quot;&gt;&lt;code&gt;?service=writely&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/your-company/writely.webp 143w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/your-company/writely.gif&quot; alt=&quot;service=writely&quot; width=&quot;143&quot; height=&quot;59&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2 id=&quot;service-jotspot&quot;&gt;&lt;code&gt;?service=jotspot&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://varun.ch/posts/your-company/jotspot.webp 143w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://varun.ch/posts/your-company/jotspot.png&quot; alt=&quot;service=jotspot&quot; width=&quot;143&quot; height=&quot;59&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Those images will display for users who are not signed into a Google Workspace account, or don’t have a logo set up.&lt;/p&gt;
&lt;p&gt;This trick has some limitations: Most notably, the image &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#security_and_tainted_canvases&quot;&gt;cannot be read through JavaScript directly&lt;/a&gt; (eg. by using a canvas). The cross origin image is only “visible” to visitors, which severely limits the impact. However, it is still possible to extract some information by relying on the user, similarly to my last experiment, &lt;a href=&quot;https://varun.ch/history&quot;&gt;“Retrieving your browsing history through a CAPTCHA”&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After publishing that post &lt;a href=&quot;https://news.ycombinator.com/item?id=30569396&quot;&gt;on Hacker News&lt;/a&gt;, I was shared the post, &lt;a href=&quot;https://ronmasas.com/posts/the-human-side-channel&quot;&gt;“The Human Side Channel”&lt;/a&gt; by Ron Masas. It is a great collection of similar attacks.&lt;/p&gt;
&lt;p&gt;This requires some very unlikely user interaction, but here is a demo game that leaks the visitor’s organization (Requires JavaScript).&lt;/p&gt;
  &lt;div class=&quot;captcha&quot; id=&quot;captcha&quot;&gt;
    &lt;h1 id=&quot;are-you-a-robot&quot;&gt;Are you a robot?&lt;/h1&gt;
    &lt;h2 id=&quot;name-the-brands-from-their-logos&quot;&gt;Name the brands from their logos&lt;/h2&gt;
    &lt;h3 id=&quot;press-done-when-you-finish&quot;&gt;Press &quot;DONE&quot; when you finish.&lt;/h3&gt;
    &lt;div class=&quot;game&quot;&gt;
      &lt;img id=&quot;img&quot; src=&quot;https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg&quot;&gt;
      &lt;form id=&quot;form&quot;&gt;
        &lt;input id=&quot;input&quot; type=&quot;text&quot; placeholder=&quot;ACME Corporation&quot;&gt;
      &lt;/form&gt;
    &lt;/div&gt;
    &lt;button id=&quot;done&quot;&gt;DONE&lt;/button&gt;
  &lt;/div&gt;
&lt;p&gt;In conclusion, this is a very low impact issue, but maybe something fun to consider when building a phishing page for a pentest or something similar.&lt;/p&gt;
&lt;script&gt;
  /*
    slightly dystopian demo game
    &quot;Say McDonald&#39;s to end commercial&quot;
  */
  let captcha = document.getElementById(&#39;captcha&#39;)
  let button = document.getElementById(&#39;done&#39;)
  let img = document.getElementById(&#39;img&#39;)
  let input = document.getElementById(&#39;input&#39;)

  function shuffle(a) {
    for (let i = a.length - 1; i &gt; 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [a[i], a[j]] = [a[j], a[i]];
    }

    if (a[0].yours) return shuffle(a) // try again 
    // TODO: technically this will make the site slower. prepend it instead?
    return a;
  }

  function capitalize(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
  }

  let logosRaw = [
    {
      url: &quot;https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg&quot;,
      name: &quot;google&quot;
    },
    {
      url: &quot;https://upload.wikimedia.org/wikipedia/commons/9/96/Microsoft_logo_%282012%29.svg&quot;,
      name: &quot;microsoft&quot;
    },
    {
      url: &quot;https://upload.wikimedia.org/wikipedia/commons/6/6e/Adobe_Corporate_logo.svg&quot;,
      name: &quot;adobe&quot;
    },
    {
      url: &quot;https://upload.wikimedia.org/wikipedia/commons/a/a9/Amazon_logo.svg&quot;,
      name: &quot;amazon&quot;
    },
    {
      url: &quot;https://upload.wikimedia.org/wikipedia/commons/b/b8/Lenovo_logo_2015.svg&quot;,
      name: &quot;lenovo&quot;
    },
    {
      url: &quot;https://www.google.com/u/0/ac/images/logo.gif?uid=&amp;service=google_gsuite&quot;,
      name: &quot;*&quot;,
      yours: true
    }
  ]

  const logos = shuffle(logosRaw)

  img.src = logos[0].url

  let current = 0;
  let yours = &quot;&quot;;

  function next(e) {
    e.preventDefault()
    if (logos[current].yours) {
      yours = input.value
    }

    if (current == logos.length - 1) {
      captcha.innerHTML = &#39;&lt;h2&gt;CAPTCHA Complete!&lt;/h2&gt;&#39;
      if (yours.includes(&#39;suite&#39;)) {
        captcha.innerHTML += &#39;&lt;p&gt;You are either not logged into a Google Workspace account or your organization has not set up a logo.&lt;/p&gt;&#39;
      } else {
        captcha.innerHTML += `&lt;p&gt;Your organization is &lt;b&gt;${capitalize(yours.replace(/[^a-z0-9]/gi, &#39;&#39;))}.&lt;/b&gt;&lt;/p&gt;`
      }
      captcha.innerHTML += `&lt;p&gt;&lt;a id=&quot;reload&quot; href=&quot;/your-company&quot;&gt;Try again&lt;/a&gt;&lt;/p&gt;`
      return;
    }
    if (!input.value.toLowerCase().includes(logos[current].name) &amp;&amp; !logos[current].yours) return alert(&quot;Try again&quot;)

    current++
    img.src = &quot;&quot;
    img.src = logos[current].url
    input.value = &quot;&quot;
  }

  button.addEventListener(&#39;click&#39;, next)
  form.addEventListener(&#39;submit&#39;, next)
&lt;/script&gt;
</content>
  </entry>
</feed>