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.
BootstrapWanna 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.
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...
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?
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()
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.
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:
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 POST
ed 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.
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.
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
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.
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.
Various apps will need your Certificate Authority's chain to be in a single file. You can create this file by cat
ting 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):
www_kirsle_net.crt
COMODORSADomainValidationSecureServerCA.crt
COMODORSAAddTrustCA.crt
AddTrustExternalCARoot.crt
So I generated the chain as follows:
cat www_kirsle_net.crt COMODORSADomainValidationSecureServerCA.crt \
COMODORSAAddTrustCA.crt AddTrustExternalCARoot.crt > cacert.pem
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.
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
.
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.
The reasons for the switch-over are numerous:
index.cgi
on Kirsle.net was sucking up so much memory. ;)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!
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.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.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.
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: HITI 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".
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:
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.
0.0020s
.