Running a PyPi registry for Windows using Github Actions and Github Pages

“This page is not a pip package index.” https://www.lfd.uci.edu/~gohlke/pythonlibs/

python -m pip install --extra-index-url https://pypi.bartbroe.re <yourpackagehere>

When trying to build Python projects for Windows, I often end up on Christoph Gohlke’s collection of Python wheels for Windows. Most of the time, I can download the package I’m looking for, compiled for Windows, and continue my day. But wouldn’t it be nice if these packages were exposed in a proper Python package index?

The standard for a simple Python package index is really easy. You need one HTML-page with all the packages, containing HTML a-tags for each package, linking to subpages per package. Each of these subpages again should contain a-tags for each provided wheel.

To turn the web page into a package index, you would only need to scrape it, find the packages, find the wheels, and build the new set of html pages.

But… there was obfuscation of the download URLs performed with JavaScript.

function dl1(ml, mi) {
    var ot = "https://download.lfd.uci.edu/pythonlibs/";
    for (var j = 0; j < mi.length; j++) ot += String.fromCharCode(ml[mi.charCodeAt(j) - 47]);
    location.href = ot;
}

function dl(ml, mi) {
    mi = mi.replace('&lt;', '<');
    mi = mi.replace('&#62;', '>');
    mi = mi.replace('&#38;', '&');
    setTimeout(function (l) {
        dl1(ml, mi)
    }, 1500, 1);
}

dl([101,53,106,110,46,105,118,50,115,104,97,100,99,49,116,54,108,51,119,95,112,52,109,113,45,47], 
   "761FC50=H9:@G6363&lt;G;C@&#62;G;C@&#62;EGA42B9E:&#62;D3A8?");
// this triggers a download: https://download.lfd.uci.edu/pythonlibs/s2jqpv5t/ad3-2.2.1-cp36-cp36m-win_amd64.whl

This code, reconstructed in our Python scraper, looks like this:

ml = [101, 53, 106, 110, 46, 105, 118, 50, 115, 104, 97, 100, 99, 
      49, 116, 54, 108, 51, 119, 95, 112, 52, 109, 113, 45, 47]
mi = "761FC50=H9:@G6363&lt;G;C@&#62;G;C@&#62;EGA42B9E:&#62;D3A8?"


def deobfuscate_download_url(ml, mi):    
    mi = mi.replace('&lt;', '<')
    mi = mi.replace('&#62;', '>')
    mi = mi.replace('&#38;', '&')
    output = ''
    for i in range(len(mi)):
        output += chr(ml[ord(mi[i]) - 47])
    return output

print("https://download.lfd.uci.edu/pythonlibs/" + deobfuscate_download_url(ml, mi))
# https://download.lfd.uci.edu/pythonlibs/s2jqpv5t/ad3-2.2.1-cp36-cp36m-win_amd64.whl

And… the server seemed to be checking the User Agent in the request, so we tell it we are Mozilla/5.0 and not something like python-requests/{package version} {runtime}/{runtime version} {uname}/{uname -r}.

Now we have a scraper that can find all packages and wheels in this page, and we build our own package index from this.

Using Github Actions, I planned a periodic run of the scraper, committing back to its own repository. This has the advantage that we can host the package index with Github Pages, which makes this entire thing a free operation.

This is the Github Action that periodically runs:

name: Update PyPi registry
on: 
  schedule:
    - cron:  '25 */4 * * *' # daily cron
jobs:
  build:
    name: Update registry
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Set up Python 3.7
      uses: actions/setup-python@v1
      with:
        python-version: 3.7
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install requests
    - name: Remove old package index
      run: |
        mv docs/CNAME ./CNAME
        rm -rf docs/*
        mv ./CNAME docs/CNAME
    - name: Scrape Christoph Gohlke
      run: |
        python scrape.py
    - name: Commit files
      run: |
        git config --local user.email "[email protected]"
        git config --local user.name "PyPi updater"
        git add *
        git commit -m "Update PyPi registry" -a
    - name: Push changes
      uses: ad-m/github-push-action@master
      with:
        github_token: $

I’m hosting this on pypi.bartbroe.re, untill it eventually breaks, so it’s usable with:

python -m pip install --extra-index-url https://pypi.bartbroe.re <yourpackagehere>

Decompiling and deobfuscating a Zend Guard protected code base

Up untill PHP version 5.6 Zend Guard could be used to obfuscate / compile your PHP source code. It is possible to deobfuscate these code bases.

The easiest way to achieve deobfuscation is by using a PHP runtime that caches opcodes, translating these cache entries back to source code. The repository Zend-Decoder by Tools2 on Github hooks into the lighttpd xcache opcode cacher, and does exactly this.

For my own convenience, I have written a Dockerfile that sets up this workflow. Combined with some bash one liners it’s possible to deobfuscate an entire code base.

Steps to get this running are:

  1. Get the codebase with the Dockerfile: git clone https://github.com/bartbroere/zend-decoder
  2. Obtain a copy of ZendGuardLoader.so and place it in the cloned repository
  3. Build the container docker build -t zenddecoder .
  4. Run the container, with the code base as a bind mount, and drop into a shell docker run -v /path/to/your/codebase:/src -it zenddecoder /bin/bash
  5. Now it’s possible to deobfuscate your entire code base with one-liners like this:
    for f in $(find /src/ -name '*.php'); do php index.php $f > ${f::-4}".dec.php"; done"
    

[Download] De Nederlandse familienamenbank

Hier staat een download van de Nederlandse familienamenbank. De dataset is gedownload door de webpagina’s voor alle achternamen te structureren. De familienamenbank verbergt het precieze aantal als dat onder de vijf ligt. Voor zulke gevallen is een poging gedaan het precieze aantal (of een interval) af te leiden, op basis van de kleur van gemeenten op de overzichtskaarten van Nederland. De code voor het downloaden en structureren van de familienamenbank staat in de Github-repository bartbroere/namenlijsten.

[Drone footage] Scheveningen, maar dan met trekkers

Adding a teleport to a SOF2 map: all the key-value pairs

The process of modding old games is mostly dead links. Some of these dead links are behind logins or paywalls. For an upcoming lanparty, we needed teleports in our map for Soldier of Fortune 2. After some googling and reading documentation, this post summarizes how we did it.

SOF2 runs the Quake 3 engine, meaning it can be modded using GTKradiant. Radiant’s file format is .map.

Here’s what we needed to add to our .map-file. Replace the two occurrences of 487 655 30, with the x y z coordinates of the target of your teleport. The other lines (after brush 0) are the six planes of the triggering area (the source of the teleport). It’s probably easier to move this area around using GTKradiant.

The three entities are numbered. The syntax may suggest that this is a comment, but I think it couldn’t hurt to make it a logical sequence with the rest of your file.

// entity 199
{
"classname" "target_teleporter"
"origin" "487 655 30"
"target" "1"
"targetname" "teleport"
}
// entity 200
{
"classname" "target_location"
"origin" "487 655 30"
"targetname" "1"
}
// entity 201
{
"classname" "trigger_multiple"
"target" "teleport"
// brush 0
{
( -728 504 56 ) ( -728 472 56 ) ( -728 472 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -672 504 56 ) ( -728 504 56 ) ( -728 504 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -672 472 56 ) ( -672 504 56 ) ( -672 504 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -728 472 56 ) ( -672 472 56 ) ( -672 472 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -728 472 56 ) ( -728 504 56 ) ( -672 504 56 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -672 504 48 ) ( -728 504 48 ) ( -728 472 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
}
}

Of course, it’s not necessary to edit the map in a text editor. You could add the same key-value pairs using the entities menu. This can be opened by pressing N in GTKradiant, while having an object selected in the interface.

For the source of the teleport, it’s important to use the _trigger texture. Give this the following key-value pairs:

"classname" "trigger_multiple"
"target" "teleport"

After compiling your map, with these lines added, you should have a working teleport.