Kirsle.net logo Kirsle.net

Welcome to Kirsle.net!

This is the personal homepage of Noah Petherbridge, and it's where I keep my web blog and various creative projects.

I blog about anything I find interesting, and since I have a lot of varied interests, my blog entries are kind of all over the place. You can browse my tags to sort them by topic and see which ones I frequently write about, or the archive has a complete history of my posts, dating back to 2008!

Besides my blog, I have pages for my creative projects, which are linked to on the navigation bar.

I write a lot about Linux and Android, Minecraft, and I like to rant about stuff. Generally anything that makes me curious. Also check out my Bookmarks for all sorts of cool websites about various topics I'm interested in.

For the geeks: this website respects your privacy and doesn't run any third party ads or analytics. This site speaks HTTP and doesn't require any JavaScript to work.

Bootstrap
October 16, 2014 by Noah

Wanna see a Twitter Bootstrap site that doesn't look like a Bootstrap site? You're looking at one right now!

I've updated the web design for Kirsle.net to use Bootstrap, so now the site is more accessible to mobile browsers. Try resizing the browser window and see how the site reacts. On smaller screens, the left nav bumps to the bottom of the page and a button appears at the top that will scroll straight to the nav bar.

I made a few custom tweaks for my particular design though, because the fixed (non-scrolling) background image and the transparent backgrounds on the panels would cause rendering errors on Firefox for Android. So, when the screen is small enough to switch into "mobile mode", the background image becomes unfixed (so it will scroll away with the rest of the page), and the panels lose their transparent backgrounds.

I've also added a new section to my site where you can see past web designs of Kirsle.net and see how each design was typically an evolution of a prior one.

Lavender Cosmos Solar

Tags: 0 comments | Permalink
Python/Tk Experiments
October 8, 2014 by Noah

I decided to take a look at the Tk GUI framework for Python and put together a simple mockup GUI to test various things.

I'd worked with Tk before in Perl (see my Perl CyanChat Client) and if you look at some of the Linux screenshots on that page, Tk looks ugly as hell in Perl.

Python's implementation of Tk (which they call Tkinter) is more modern than Perl's, and there's other neat helper modules like ttk which provides the Tk "Tile" theming engine which makes the standard Tk widgets look more modern, and takes a CSS-style approach to theming your widgets: instead of manually specifying things like background and foreground colors in each widget you program, you keep all that stuff in one central place and refer to it by name from the widgets.

For my Python mockup test app, I put together a rough copy of my Perl CyanChat Client GUI.

At first I was trying to use the ttk/Tile versions of the widgets (such as Button, Entry, etc.), but I ran into a rather annoying roadblock: in PCCC, my text Entry widgets have black background colors, and the insertion cursor (the little flashing I-beam in a text box) is also black by default. So when clicking in the text box, you wouldn't be able to see the insertion cursor.

In the standard Tkinter Entry widget, you can use the insertbackground option to change the color of the insertion cursor. But in ttk/Tile? There is no insertbackground option. Source - it's just not supported in ttk/Tile and their theming engine.

So I decided to not use ttk and just use the standard Tk widgets. I liked ttk's centralized styling system though, so I made a central class of dictionaries full of configuration attributes that I could easily reference when setting up my widgets. So, I eventually got my GUI put together and it looked nice enough I guess...

Tk widgets with ugly scrollbar

Except for those ugly scrollbars. The "1980s 3D" look to the scrollbar and those ugly triangle arrow widgets are from the Motif GUI which Tk was originally modeled to look like. It's ancient and it's ugly. This was also one of the main reasons why my Perl CyanChat Client looks so horrible under Linux, because this is Tk and Tk is ancient.

The Tile theming engine is supposed to fix this, but I wasn't using Tile in my code because of the aforementioned text insertion cursor problem. The best I could do with the standard Tk scrollbar is color it to make it look kind of "cool" at least, so I made it all black and grey to fit the theme of the rest of my GUI.

But then I figured out I can mix and match the widgets. I could import the Scrollbar from ttk while importing all the other widgets from Tkinter. The result?

Nice scrollbars!

That's better.

I probably won't create a full CyanChat client in Python because I really don't care about CyanChat much anymore, so this was mostly just me messing around with Tk and seeing how practical it is for certain use cases. But here's the source code anyway.

There's a few interesting things in the code, like I created my own "Scrolled" class for wrapping a widget in a scrollbar (works with Text and Listbox), so it's kinda like Python's ScrolledText module, but it's really more like Perl's Tk::Scrolled module in that it can wrap arbitrary widgets, not just Text.

Also, Tkinter's Text widget can't be made read-only. You can make a text box disabled, but that also prevents programmatic insertions/deletions as well. So I made a little function for inserting text that would first re-enable it, then insert text, then disable it again.

#!/usr/bin/env python

"""My test script for Python/Tk experimentation."""

import Tkinter as tk
from Tkinter import Tk, StringVar, Frame, Label, Text, Entry, Button, Listbox, END
from ttk import Scrollbar


class ChatClient(object):
    def __init__(self):
        # Styles
        self.style = MainWindowStyles()

        self.setup()

    def setup(self):
        self.mw = Tk()
        self.mw.title("Python CyanChat Client")
        resize_and_center(self.mw, 640, 480)

        # Variables
        self.nickname = StringVar(self.mw, "Kirsle")
        self.message = StringVar(self.mw, "--disabled--")

        # Top Frame (name entry box, buttons, conn status)
        self.login_frame = Frame(self.mw, **self.style.Frame)
        self.lower_frame = Frame(self.mw, **self.style.Frame)
        self.login_frame.pack(side="top", fill="x")
        self.lower_frame.pack(side="top", fill="both", expand=1)

        # The lower left (message entry, chat history) and lower right
        # (who lists)
        self.left_frame = Frame(self.lower_frame, **self.style.Frame)
        self.right_frame = Frame(self.lower_frame, **self.style.Frame)
        self.right_frame.pack(side="right", fill="y")
        self.left_frame.pack(side="right", fill="both", expand=1)

        # The message entry & chat history frames
        self.message_frame = Frame(self.left_frame, **self.style.Frame)
        self.dialogue_frame = Frame(self.left_frame, **self.style.Frame)
        self.message_frame.pack(side="top", fill="x")
        self.dialogue_frame.pack(side="top", fill="both", expand=1)

        ###
        # Top Frame Widgets
        ###

        self.name_label = Label(self.login_frame,
            text="Name:",
            **self.style.Label
        )
        self.name_entry = Entry(self.login_frame,
            textvariable=self.nickname,
            width=20,
            **self.style.DarkEntry
        )
        self.enter_exit_button = Button(self.login_frame,
            text="Enter chat",
            **self.style.Button
        )
        self.status_label = Label(self.login_frame,
            text="Connected to CyanChat",
            **self.style.ConnectedLabel
        )
        self.name_label.pack(side="left", padx=5, pady=5)
        self.name_entry.pack(side="left", pady=5)
        self.enter_exit_button.pack(side="left", padx=5, pady=5)
        self.status_label.pack(side="left")

        ###
        # Message Frame Widgets
        ###

        self.message_entry = Entry(self.message_frame,
            textvariable=self.message,
            state="disabled",
            **self.style.Entry
        )
        self.message_entry.pack(
            side="top",
            fill="x",
            padx=10,
            pady=10,
            expand=1,
        )

        ###
        # Who Frame Widgets
        ###

        self.who_label = Label(self.right_frame,
            text="Who is online:",
            anchor="w",
            **self.style.Label
        )
        self.who_label.pack(side="top", fill="x")

        self.who_list = Scrolled(self.right_frame, Listbox,
            attributes=self.style.Listbox,
            scrollbar=self.style.Scrollbar,
        )
        self.who_list.pack(side="top", fill="both", expand=1)

        for i in range(200):
            self.who_list.widget.insert(END, "Anonymous{}".format(i))

        ###
        # Dialogue Frame Widgets
        ###

        self.dialogue_text = Scrolled(self.dialogue_frame, Text,
            attributes=self.style.Dialogue,
            scrollbar=self.style.Scrollbar,
        )
        self.chat_styles(self.dialogue_text.widget)
        self.dialogue_text.pack(side="top", fill="both", padx=10, pady=0, expand=1)

        # Dummy junk
        messages = [
            [["[Kirsle]", "user"], [" Hello room!"]],
            [["\\\\\\\\\\", "server"], ["[Kirsle]", "user"], [" <links in from comcast.net Age>"], ["/////", "server"]],
            [["[ChatServer] ", "server"], ["Welcome to the Cyan Chat room."]],
            [["[ChatServer] ", "server"], ["There are only a few rules:"]],
            [["[ChatServer] ", "server"], ["   Be respectful and sensitive to others"]],
            [["[ChatServer] ", "server"], ["   And HAVE FUN!"]],
            [["[ChatServer] ", "server"], [""]],
            [["[ChatServer] ", "server"], ["Termination of use can happen without warning!"]],
            [["[ChatServer] ", "server"], [""]],
            [["[ChatServer] ", "server"], ["Server commands now available, type !\\? at the beginning of a line."]],
            [["[ChatServer] ", "server"], ["CyanChat Server version 2.12d"]],
        ]
        for i in range(80):
            messages.append([["[ChatClient]", "client"], [" Connecting..."]])
        messages.reverse()
        for line in messages:
            self.insert_readonly(self.dialogue_text, 0.0, "\n")
            line.reverse()
            for part in line:
                self.insert_readonly(self.dialogue_text, 0.0, *part)
        #self.insert_readonly(self.dialogue_text, END, "[Admin]", "admin")

    def chat_styles(self, widget):
        """Configure chat text styles."""
        # User colors
        widget.tag_configure("user", foreground="#FFFFFF")
        widget.tag_configure("guest", foreground="#FF9900")
        widget.tag_configure("admin", foreground="#00FFFF")
        widget.tag_configure("server", foreground="#00FF00")
        widget.tag_configure("client", foreground="#FF0000")

    def insert_readonly(self, widget, *args):
        """Insert text into a readonly (disabled) widget."""
        widget.widget.configure(state="normal")
        widget.widget.insert(*args)
        widget.widget.configure(state="disabled")

    def start(self):
        self.mw.mainloop()


class MainWindowStyles(object):
    """Simple Python class to hold style-related configurations for widgets."""
    Frame = dict(
        bg="#000000",
    )

    BaseLabel = dict(
        font="Verdana 8",
    )
    Label = dict(
        bg="#000000",
        fg="#CCCCCC",
        **BaseLabel
    )
    ConnectedLabel = dict(
        bg="#000000",
        fg="#00FF00",
        **BaseLabel
    )

    BaseFormCtrl=dict(
        highlightthickness=0, # Removes stupid border around the widget
    )

    BaseEntry = dict(
        insertwidth=1,
        selectborderwidth=0,
        selectbackground="#0099FF",
        font="Verdana 8",
        **BaseFormCtrl
    )
    Entry = dict(
        bg="#FFFFFF",
        fg="#000000",
        disabledbackground="#000000",
        disabledforeground="#666666",
        insertbackground="#000000",
        **BaseEntry
    )
    DarkEntry = dict(
        bg="#000000",
        fg="#CCCCCC",
        insertbackground="#FFFFFF", # Text insertion blinking cursor
        **BaseEntry
    )

    Listbox = dict(
        bg="#000000",
        fg="#CCCCCC",
        **BaseFormCtrl
    )

    Dialogue = dict(
        bg="#000000",
        fg="#CCCCCC",
        #disabledbackground="#000000",
        #disabledforeground="#CCCCCC",
        wrap=tk.WORD,
        state="disabled",
        **BaseEntry
    )

    Button = dict(
        bg="#000000",
        fg="#CCCCCC",
        activebackground="#000000",
        activeforeground="#0099FF",
        **BaseFormCtrl
    )

    # If using the Tkinter scrollbar, uncommon these. If using the ttk
    # scrollbar, use ttk's theming system instead.
    Scrollbar = dict(
        #relief="flat",
        #troughcolor="#000000",
        #bg="#606060",
        #activebackground="#999999",
        #borderwidth=1,
        #width=12,
        #highlightthickness=0,
    )


class Scrolled(object):
    """My own implementation for adding a scrollbar to a widget. Similar in
    principal to Python's ScrolledText module, but it works on other widgets too
    (this script uses it on Listbox too). So it's more like the Perl/Tk module
    Tk::Scrolled in that it can wrap any widget, in theory."""

    def __init__(self, master, widget_class, attributes=None, scrollbar=None):
        """
        master is the parent widget
        widget_class is the class, like Text or Listbox
        attributes are attributes for the widget
        scrollbar are attributes for the scrollbar
        """
        if attributes is None:
            attributes = []
        if scrollbar is None:
            scrollbar = []

        self.master = master

        # Parent frame to hold the widget + scrollbar
        self.frame  = Frame(master)

        # The scrollbar
        self.scrollbar = Scrollbar(self.frame, **scrollbar)

        # The widget itself
        self.widget = widget_class(self.frame,
            yscrollcommand=self.scrollbar.set,
            **attributes
        )
        self.scrollbar.configure(command=self.widget.yview)

        self.scrollbar.pack(side="right", fill="y")
        self.widget.pack(side="right", fill="both", expand=1)

    def widget(self):
        """Get at the inner widget."""
        return self.widget

    def scrollbar(self):
        """Get at the scrollbar widget."""
        return self.scrollbar

    def pack(self, **kwargs):
        """Wrapper so that pack() works as you'd expect."""
        self.frame.pack(**kwargs)


def resize_and_center(win, width, height):
    """Resize a window and center it on the screen."""
    screen_w = win.winfo_screenwidth()
    screen_h = win.winfo_screenheight()
    geometry = "{}x{}+{}+{}".format(
        width,
        height,
        screen_w / 2 - width / 2,
        screen_h / 2 - height / 2,
    )
    win.geometry(geometry)


if __name__ == "__main__":
    app = ChatClient()
    app.start()

Tags: 2 comments | Permalink
Android can apparently delete your Camera folder without warning
August 11, 2014 by Noah

So I took a few pics with my Nexus 5 this morning, and when viewing them in the Gallery app they appeared to have not saved correctly (there was a solid dark grey placeholder image for each one I took). If I opened the Camera app and swiped over to see them, there they were. But the Camera app also popped up a toast saying "Insert an SD card to save photos."

The Nexus 5 has no SD card.

And then I probably shouldn't have rebooted my phone (the Camera was being an idiot, so I thought rebooting would fix it), cuz when the phone came back up, the Camera folder was entirely deleted. Like, it didn't show up in the Gallery, and swiping in the Camera app did nothing. /mnt/sdcard/DCIM/Camera didn't exist anymore. Take a new picture, and the Camera folder was remade but only had the one new picture in it.

So, Life Pro Tip: Android can do this sometimes. If you get those kind of symptoms, hopefully try making a backup of the Camera folder (using like Astro or Root Browser), before rebooting the phone. You'll probably still lose the newly taken/corrupted pictures but at least Android shouldn't delete all your other camera pictures.

I think the Camera app was still able to see the pictures even though they weren't saved because they were kept in some sort of temporary in-memory cache of the Camera app. Probably the best way to save them would've been to take screenshots of them from within the Camera app.

All the other folders that had pictures in them were left alone, i.e. pictures saved through Snapchat and other photo albums.

Update:

I used a recovery program to restore all the deleted photos/videos on my phone. Site I used. Some of the pictures were corrupted but I was able to get all the ones I cared about back. :) Most of the deleted files were Snapchats that I already had non-deleted copies of.

Also, the selfies that I took this morning that "caused" this whole entire mess were recovered as well, so that's double-good-news. :P One of the pics that literally broke my camera:

Pic that literally broke my camera

Tags: 0 comments | Permalink
HTML5 Multi-File Uploader w/ Progress Bar
June 20, 2014 by Noah

The most recent feature I added to my website's CMS: multi-file uploads for the photo albums. I've been wanting to get around to this for a while so I can actually upload photo albums in bulk and make better use of that feature on my site. ;)

So I did some research and found some bits of example code here and there, and put together a pure HTML5 multiple-file uploader with progress bar. No Flash, no Java, no Internet Explorer 9 or lower. ;)

A lot of the existing bits of code I found out there weren't quite written in a way that was useful for my purposes. Their code tended to run the upload immediately after getting ahold of the files, i.e. they'd set up an HTML5 drag-and-drop spot and/or a multiple-file <input> box, and as soon as the user drops their pictures or selects them, the JavaScript would go right to work uploading them one by one to the back-end.

On my CMS I wanted to hold off on the uploading, because there's other form elements to take care of too, i.e. what album to put the pictures into or to apply a caption to them all. So I set up handlers for my file input box and drag-drop site to just put all the File objects into an array and wait for the submit button to be pushed.

So in my implementation, all the pictures are uploaded at once to the back-end, and there's only one progress bar (for the entire upload). It's possible to have one upload event per individual file, and therefore get progress bars on a file-by-file basis, but this didn't fit into my existing code structure.

Something I think is cool though is, on the back-end I'm using the exact same endpoint to handle uploads using Ajax (for those with JavaScript turned on) and when being POSTed to directly, i.e. for users with NoScript enabled. In both cases, they hit the /photos/upload on the server to send the form and images.

When the Ajax is the one doing it, it adds an extra __ajax form parameter. In this case, the back-end responds with a JSON response telling what the next URL is, and the JavaScript initiates a redirect to that URL. In case the user has JavaScript turned off, and the form POSTs to the back-end directly, the web server sends an HTTP redirect to the next URL.

Anyway, I threw together a quick Python/Flask app to mess with this stuff and figure it all out so I didn't have to worry about trying to wrangle existing code into doing something new. I have it hosted on Github here: https://github.com/kirsle/flask-multi-upload

The real interesting part is in the JavaScript source - only 184 lines of code, including comments and whitespace. Pretty straightforward. The same basic front-end code could be used regardless of the back-end, i.e. it could be uploading to a PHP script or something instead of a Python app. The Python part of the source is pretty short and sweet too.

Tags: 0 comments | Permalink
Limbo in Vanilla Minecraft
June 3, 2014 by Noah

Time for another Minecraft tutorial using command blocks to do something pretty neat using the vanilla version of the game (no plugins necessary). I used Minecraft version 1.7.9.

In this mini tutorial, I'm showing off how I created a "Limbo Dimension" that players are sent to after death, where they must remain for 60 minutes before being respawned in the overworld again.

Read more...

Tags: 2 comments | Permalink
Markdown in Rophako
April 18, 2014 by Noah

I've made an update to my Rophako CMS that powers Kirsle.net: it now supports Markdown!

Blog posts can be either written in Markdown, or direct HTML (the old way). In Markdown mode, HTML is currently escaped from the post. I may change this and make it only escape HTML in comments if I find it to be a PITA in the future. ;) Oh, and emoticons still work (they're rendered after Markdown is done).

Markdown works in comments, too, but with some limitations, like I don't allow embedding images. Also, you can write entire pages on your site in Markdown. Just create a page named with a .md file extension that contains Markdown code and it "just works" (there's a built-in markdown.inc.html file in the default site that wraps your rendered document in your web design's layout).

Best of all, I'm using a handful of extensions to the Python Markdown module to enable a lot of Github style Markdown features, such as code highlighting. Here's a Python example:

def render_markdown(body, html_escape=True):
    """Render a block of Markdown text.

    This will default to escaping literal HTML characters. Set
    `html_escape=False` to trust HTML."""

    args = dict(
        lazy_ol=False, # If a numbered list starts at e.g. 4, show the <ol> there
        extensions=[
            "fenced_code",  # GitHub style code blocks
            "tables",       # http://michelf.ca/projects/php-markdown/extra/#table
            "smart_strong", # Handles double__underscore better.
            "codehilite",   # Code highlighting with Pygment!
            "nl2br",        # Line breaks inside a paragraph become <br>
            "sane_lists",   # Make lists less surprising
        ],
        extension_configs={
            "codehilite": {
                "linenums": False,
            }
        }
    )
    if html_escape:
        args["safe_mode"] = "escape"

    return markdown.markdown(body, **args)

I'll be revisiting all my old blog posts that have code pasted in them and reformatting them in Markdown for easier maintenance. My old way of pasting code in a blog post was to open the code in vim, and then run commands like,

:let html_use_css=1
:TOhtml

... which would output an HTML file that syntax highlights the code, and then I'd cut and paste all that junk into my blog. Blech. Markdown will be much cleaner. :D

Tags: 0 comments | Permalink
OpenSSL for Kirsle.net!
April 18, 2014 by Noah

A while after the Heartbleed SSL vulnerability made headlines, Wired.com ran an article titled "It's Time to Encrypt the Entire Internet" urging everyone to deploy SSL/TLS encryption on their sites.

SSL certificates tend to be pretty expensive, though, which is one reason I hadn't looked into it that closely in the past. In a Reddit comment thread about that Wired article some people mentioned Namecheap as a good option for simple SSL certs. So, I got a simple domain-level certificate for $9 for Kirsle.net. :) So all kirsle.net URLs are now running over https! This blog post is about the experience of setting up SSL and wrestling with various applications in the process.

Generating the Certificate

The simplest guide I found that I followed to make a certificate was Generate CSR - Apache OpenSSL. One command creates a passphrase-protected key file, the next one generates the signing request:

openssl genrsa –des3 –out kirsle.key 2048​
openssl req -new -key kirsle.key -out kirsle.csr

You apparently need a 2048-bit RSA key these days before a Certificate Authority will consider your signing request. I pasted in my CSR file and filled out some forms, got an e-mail verification sent to the address on my WHOIS record for my domain, and before I knew it I was e-mailed a zip file containing my certificate and the Comodo CA certificates.

Certificate Chain File

Various apps will need your Certificate Authority's chain to be in a single file. You can create this file by catting the certificates into one file in "reverse" order, with your site's certificate on top, and the root certificate on bottom. Comodo gave me these files (and this is also the order for the chain file):

  • Kirsle.net certificate: www_kirsle_net.crt
  • Intermediate CA certificate: COMODORSADomainValidationSecureServerCA.crt
  • Intermediate CA certificate: COMODORSAAddTrustCA.crt
  • Root CA certificate: AddTrustExternalCARoot.crt

So I generated the chain as follows:

cat www_kirsle_net.crt COMODORSADomainValidationSecureServerCA.crt \
    COMODORSAAddTrustCA.crt AddTrustExternalCARoot.crt > cacert.pem

Apache2 mod_ssl

I'm running a Debian server, so I just symlinked the ssl.load and ssl.conf files from my /etc/apache2/mods-available into my mods-enabled, and then edited the ssl.conf. All I changed in it was to uncomment the SSLHonorCipherOrder on line.

I removed the sites-enabled/default-ssl and then edited my Kirsle.net config file to add a <VirtualHost *:443> version. I had to look at the default-ssl file to get an idea which options were needed (if I missed any, Apache would fail to start!)

Relevant SSL options for my VirtualHost:

    # SSL
    SSLEngine on
    SSLCertificateChainFile /etc/ssl/crt/cacert.pem
    SSLCertificateFile /etc/ssl/crt/www_kirsle_net.crt
    SSLCertificateKeyFile /etc/ssl/crt/kirsle.key
    SSLOptions +StdEnvVars
    BrowserMatch "MSIE [2-6]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0
    BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown

Note: if you leave out the chain file, web browsers will still behave fine (because they're smart enough to download the intermediary certificates themselves), but other things will break. For example, the Python requests module will throw an SSL exception if the server doesn't give it the intermediary certificates!

After making sure https://www.kirsle.net/ was working, I made an update to my Rophako CMS to support SSL sites better and then made the switch-over. Any requests going to my HTTP Kirsle.net are redirected to the SSL version and given a Strict Transport Security header.

As a fun side note, Apache supports Perfect Forward Secrecy by default (using the default SSLCipherSuite option of HIGH:MEDIUM:!aNULL:!MD5).

Starting or restarting Apache requires you to enter the SSL key's passphrase at the command line. For simple config updates, service apache2 graceful will reload them without needing a full restart, so you don't need to enter the passphrase then.

Dovecot IMAP

I use Dovecot for my IMAP mail server on Kirsle.net, and I wanted it to use my shiny new SSL certificate. Before this, I was using a self-signed certificate, and apparently Thunderbird doesn't even warn you if that self-signed certificate changes at any point. After the Heartbleed vulnerability was fixed, I re-generated new self-signed certs and was shocked that Thunderbird happily accepted the new certificate without even telling me. It would've been extremely easy to Man-in-the-Middle my e-mail server. (I had since then installed an extension in Thunderbird to police SSL certificates for me as a workaround).

So, configuration is pretty simple, just edit /etc/dovecot/conf.d/10-ssl.conf and enter in the new paths to your chain file and private key. Note that if you use just your domain's certificate, clients like Thunderbird that support SSL properly will complain about the certificate being insecure, and unlike web browsers, Thunderbird doesn't bother downloading the intermediary certificates itself.

One catch with Dovecot is that if your private key file is encrypted with a passphrase like mine is, doing service dovecot restart won't work. Dovecot will start in a way where it won't support TLS but will otherwise appear to function normally.

To start Dovecot with a passphrase, you need to run dovecot -p (as root) to start the service. It will prompt for your passphrase at the command line and then start up. The service can be stopped normally using service dovecot stop.

Postfix SMTP Server

This one I'm a bit upset about. Postfix has absolutely NO support for using a passphrase protected TLS key file! Even their official documentation states that the key file must not be encrypted.

That is so full of wtf. Postfix is a widely deployed SMTP server for Linux, and it has to use insecure, unprotected TLS key files. So, I'm still using a self-signed certificate for Postfix (and my Thunderbird add-on will tell me if this certificate ever changes, so don't get any ideas!). I don't send outgoing mail very often, anyway, and if I care enough I'll PGP encrypt. But, I'll be looking into an alternative SMTP server sometime soon.

Tags: 4 comments | Permalink
Goodbye, PerlSiikir!
April 6, 2014 by Noah
As of a few minutes ago, Kirsle.net is no longer powered by Perl. Instead, I've been working on a new content management system written in Python to replace it.

The reasons for the switch-over are numerous:

  1. The old Perl code was originally written for my previous version of my other project, Siikir, and the code was for an entire social networking type of site with lots of features, among which were Blogs, Photo Albums, and Comments (the three that Kirsle.net made use of). Kirsle.net didn't make use of the other features.
  2. The Perl code also had some memory leaks, which I tried for a while to eliminate but wasn't making much progress with. It was running as a FastCGI script, and the most notable side effects of the memory leaks were that my web server would randomly kill off unrelated processes, such as Minecraft servers or my XMPP server, because my index.cgi on Kirsle.net was sucking up so much memory. ;)
  3. Setting up PerlSiikir on a brand new server was an hours-long task. It needed a recent version of Perl, which needed a perlbrew installation done, and then a lot of modules needed manual installation. Seriously, look at my install notes. The new Python web app takes only minutes to set up.
  4. I like Python better nowadays than Perl. :)
And the best news of all is that my new Python CMS is open source!

I named the new project Rophako, because I was sitting at the Github "new repository" screen for a half hour trying to think of a name, and ended up just making use of my Azulian Translator to come up with a name. So, Rophako is Azulian for "Website." I'm a clever genius, I know. ;)

You can check out Rophako on Github: https://github.com/kirsle/rophako. The "default website" that comes with it isn't very polished yet; I literally just finished writing the code to support Kirsle.net. So, sometime later I'll tidy up the default website and have a working copy of it running on some subdomain like rophako.kirsle.net.

Anyway, this is the new CMS. I ported over all my old blog posts, comments, comment subscriptions, and things of the sort. All the old URLs should work too, due to my kirsle_legacy.py module in Rophako. If anybody finds any broken links or issues with the site, let me know. :)

Update (4/9/14):

I've polished up Rophako's default site and have an example running here: http://rophako.kirsle.net/

That's the site you'd get if you download and install Rophako (minus the blog posts and photos ;) ). So... the project is officially in "beta" status now and is usable!

Tags: 0 comments | Permalink
Exploring Grindr's Photo Cache
April 4, 2014 (updated June 13, 2022) by Noah
UPDATE (June 12 2022): I wrote this blog post originally in 2014 and it's highly likely that Grindr's data folder on Android doesn't look anything like this anymore. I haven't rooted my Android phones in a long time and haven't run Grindr in several years either. I sometimes get emails from people asking for help getting into their Grindr cache -- I can't help you, and don't ask me about this. This blog post is very outdated, Grindr has gone thru several complete re-designs in the last 8 years and they've probably changed everything about their data folder layout.

What you can do if you want to look into this yourself is: root your Android phone, look in your /data/data folder for Grindr's app data, copy it out to your PC and go thru it yourself. The file command on a macOS or Linux terminal can tell what type of file something is regardless of its extension (in this post, many JPEG photos were named with ".1" file extensions instead of .jpeg and Linux easily ferreted this out).

Also note that rooting your Android phone may require a factory reset of the phone, so if it's your existing Grindr cache you want to get into, you may be out of luck. Google Pixel phones for example officially support unlocking the bootloader (necessary for flashing custom ROMs and root), but this action forces a factory reset for privacy (e.g. a thief stole your phone and wants to root it, which would let them bypass your lock screen, Google wants the phone to mandatorily reset to factory defaults before it can be rooted). Some phones with shadier root exploits may not need the factory reset. You're on your own either way. Don't ask me for support in rooting your phone or helping you hack Grindr.

A long time ago, the Grindr for Android app used to store its photo cache on your SD card, but lately they hid them away in the app's private space to make them slightly more difficult to get to. I decided to go exploring using Root Browser and see what I could find out.

When I say "photo cache" I mean the place where Grindr downloads pictures locally so that it doesn't need to keep redownloading everyone's pictures all the time and consuming a lot of unnecessary bandwidth. Grindr caches both profile pictures and pictures received over chat messages. They both go into the same place. So if you have access to that place, you can get high resolution copies of all pictures received over chat and have them on your PC. :)

First of all, you'll need a rooted Android device for this, because the Android OS normally doesn't allow apps to get into each other's private data folders. The Root Browser app is a file browser that's root-aware (so it will prompt for root permission when you attempt to open a folder that ordinarily you can't open without root).

So, without further ado, Grindr's photo cache is located at /data/data/com.grindrapp.android/cache/picasso-cache/. This folder may contain a lot of files, mine had 3,458 and so Root Browser took a while to load that folder. You can copy it somewhere under /mnt/sdcard and then get to your files from a PC that way. Make sure the files are no longer owned by "root" when put in the SD card part, or you may run into issues when accessing them from your PC.

Most of the files in this folder have hexadecimal names that appear to be hashes of some sort, and the names usually come in pairs, one with a ".0" file extension and the other with a ".1", for example one I found on my phone was 4e21d675447678d0493bc8cb41a56e8d.0.

The ".0" file is a plain text file, and most of the ".1" files are the JPEG images. I use Linux, and my file browser automatically identified the types of all the files and showed thumbnail images for all the ".1" files. So, most of the time if you rename one of the ".1" files to have a ".jpg" extension you can see the images under Windows.

Some of the .1 files aren't images though. Some are more text files, and I peeked inside one to see what it was:

$ cat c1749deee81d4fece16d836e177c5852.1
[{"messageId":16970,"title":"Calling All DJs & Bartenders","body":"Are you one of the sexiest DJs or bartenders and able to work a paid event on the afternoon of April 27th in Palm Springs? If so, send us your information and a link to your website to palmsprings@grindr.com or simply tap 'More' to email us directly. ","actionTitle":"More", "dismissTitle":null, "expirationDate":1396853940000, "url":"mailto:palmsprings@grindr.com"}]

These appear to be the broadcasted pop-up messages shown in the app sometimes.

Now, the other interesting files are the ones with the ".0" extensions. These appear to be debug information, and they're basically the full HTTP request dump used to download the ".1" file. Here's what the one looked like for my profile picture (in case the Grindr CDN link stops working and you're curious, it's this picture):

$ cat 4e21d675447678d0493bc8cb41a56e8d.0
http://cdns.grindr.com:80/images/profile/1024x1024/d8dfd4eb2abd9c4d29653587cc87912b393bac97
GET
0
HTTP/1.1 200 OK
14
Accept-Ranges: bytes
Content-Length: 72057
Content-Type: image/jpg
Date: Fri, 04 Apr 2014 20:09:05 GMT
Etag: "98af07f8697f854734874296a90c640f"
Last-Modified: Sat, 01 Mar 2014 22:05:22 GMT
Server: ECS (lax/2851)
x-amz-id-2: [REDACTED]
x-amz-request-id: [REDACTED]
X-Android-Received-Millis: 1396642144430
X-Android-Response-Source: CONDITIONAL_CACHE 200
X-Android-Selected-Transport: http/1.1
X-Android-Sent-Millis: 1396642144347
X-Cache: HIT
I edited-out the "x-amz" headers because I'm not sure how secret those are supposed to be.

When browsing through my cache folder I also saw some pictures that weren't profile pics, but were sent over chat messages. These always seem to be the full resolution of the original pic sent, i.e. not thumbnails or anything. The ".0" file looks the same as for a profile picture, except the URL downloaded begins with "http://cdns.grindr.com:80/grindr/chat/" and the server headers respond with a "Content-Type: binary/octet-stream" (which causes a web browser to download the picture to disk instead of displaying it in the browser).

Some of the ".1" files are actually empty (0 bytes), and their .0 files indicate that these are the ad banners (requesting a URL from googleads.g.doubleclick.net). So it looks like whatever system in Grindr is responsible for downloading pictures also sorta deals with downloading ad banners, except it doesn't actually save the banner into the cache folder.

The last somewhat not-very-interesting file in the cache folder is called "journal", and it's a text file. By reading the first couple lines, it appears to be part of libcore.io.DiskLruCache, a bit of Java code that provides a rotating offline cache. This probably means that, if Grindr's cache folder fills up, it will automatically remove old files to make room for new ones, so it can keep its overall disk usage under control automatically. The journal file appears to list the hash names of all the other files in the folder, along with words like "CLEAN", "DIRTY", and "REMOVE".

Tags: 27 comments | Permalink
Skype and Windows Live Messenger
February 28, 2014 by Noah
Back in the day, I ran a couple of chatbots on Windows Live Messenger (although it was called MSN Messenger then), so I'm reasonably familiar with how the Microsoft Notification Protocol (MSNP) works. We had a Perl module called MSN.pm which works with the MSNP10 version of the protocol, and it probably still works today.

That's right, the Windows Live Messenger protocol is still perfectly alive and well today. A while back, I booted my Windows OS on my PC where I still had Pidgin set up to sign me into MSN, and surprisingly it still worked. One of my Skype contacts sent me a message over Pidgin, and their "MSN e-mail address" had an "@SkypeDomain.fakedomain" domain part. It seems that now, though, while the MSN servers are still up, they at least block non-Chinese users from authenticating (Pidgin says "invalid response from server").

The Skype/MSN merger was done in a pretty half-assed way by Microsoft:

  • If you sign in with your Skype username, you only see Skype contacts.
  • If you sign in with your MSN username, you see both MSN and Skype contacts.
  • Even though you can use your old MSN username, and chat with existing MSN contacts, there doesn't seem to be a way to add a new MSN contact through Skype.
It appears that the Skype client actually acts like a "mini Pidgin": when you sign in with your old MSN account, Skype actually signs you in separately to the Skype and MSN servers. And, on the MSN side of things, the "@SkypeDomain.fakedomain" extension was probably implemented similarly to what happened when MSN and Yahoo joined forces, and your Yahoo contacts on MSN would have "@yahoo.com" domain extensions.

I don't get why Microsoft doesn't just pull the plug on MSNP completely, and force everyone to get a Skype name if they don't already have one linked with their MSN accounts.

Tags: 4 comments | Permalink