Notes to self, 2010

2010-11-30 - faxable images / asterisk pbx

Because creating images that the open source PBX Asterisk(tm) will properly fax using the SendFAX() application, was a pain in the ass, I'd like to share my findings.

The HOWTO for creating TIFF images that are laid out so the spandsp fax back-end in asterisk, is embedded in the images2fax.sh shell script, below.

In short, you need to have images of 1728x2292, in 2 colors, with the correct DPI (204x196) and the right compression.

This script automates the process of scaling, resizing, adding canvas and merging more images into a single multi-page image. It takes two or more arguments: the destination TIFF and all source images. It uses the multi-purpose ImageMagick convert utility for all the necessary operations.

#!/bin/sh
# images2fax.sh -- Walter Doekes 2010
# vim: set ts=8 sw=4 sts=4 et:

#
# Get arguments / show help
#

IMCONVERT="`which convert`"
if [ "$#" -lt 2 -o -z "$IMCONVERT" ]; then
    cat >&2 << __EOF__
Usage: $0 OUTPUT.TIFF INPUT.IMG...
Converts a collection of input images to a single multipage tiff to be
sent using the asterisk SendFAX application.

Requires ImageMagick convert(1), found at: ${IMCONVERT:-NOT FOUND}
__EOF__
    exit 1
fi
OUTPUT="$1" ; shift


#
# Utility functions
# (mktemp and mktemp_tiff return filenames safe to use without double quotes)
#

mktemp_tiff() {
    # Bah, this is ugly.
    file=`mktemp`
    mv -n $file $file.tiff
    if [ $? = 0 ]; then
        file=$file.tiff
    else
        rm $file
        file=`mktemp_tiff` # filename in use? try again
    fi
    echo $file
}

resize_image_to_1728x2292() {
    input="$1"
    output=`mktemp_tiff`
    # (-alpha: remove transparency)
    # (-resample: scale to correct proportions)
    # (-scale: resize up/down so it fits)
    # (-extent: add white canvas)
    "$IMCONVERT" "$input" -alpha off -resample 204x196 -scale 1728x2292 \
        -extent 1728x2292 $output
    if [ $? != 0 ]; then
        rm $output
        exit 1
    fi
    echo $output
}

convert_image_to_tiff() {
    input="$1"
    output=`mktemp_tiff`
    "$IMCONVERT" "$input" -compress Fax -units PixelsPerInch -density 204x196 \
        -antialias -resize '1728x2292!' -dither FloydSteinberg -monochrome \
        -dither FloydSteinberg -monochrome $output
    if [ $? != 0 ]; then
        rm $output
        exit 1
    fi
    echo $output
}


#
# Main: convert individual images and combine them
#

converted_list=""
for image in $@; do
    # Resize image
    resized=`resize_image_to_1728x2292 "$image"`
    if [ $? != 0 ]; then
        echo "Failure resizing $image. Aborting..." >&2
        [ -n "$converted_list" ] && rm $converted_list
        exit 1
    fi

    # Convert image
    converted=`convert_image_to_tiff $resized`
    if [ $? != 0 ]; then
        echo "Failure converting $image. Aborting..." >&2
        rm $converted_list $resized
        exit 1
    fi

    # Append to list
    rm $resized
    converted_list="$converted_list $converted"
done

"$IMCONVERT" '(' -coalesce $converted_list ')' "$OUTPUT"
status=$?
rm $converted_list
exit $status

Update 2010-12-06

In some cases — this has been observed when converting PDF to TIFF — the polarity can be mixed up, creating a white-on-black fax which can be quite expensive ;-) Fix this using the -define quantum:polarity=min-is-white option.

2010-11-23 - nat / switch external source port

When reproducing an issue with IP phones speaking SIP, I ran into the question of how to switch source port on my linux NAT router.

The problem the clients were having, were a result of a failing (or reset) NAT-gateway. The NAT-gateway would change the external source port mid-dialog (some SIP dialogs can persist for quite a long time).

So, how do you go about switching source port on an UDP connection on your Linux NAT router without resetting it (or disturbing anyone else using it)? The answer: conntrack(8)

Step (1): add a static SNAT mapping for a custom port, before the standard SNAT rules, for your specific application. In my case the phone is at 192.168.1.123, the destination at 123.123.123.123 and my external IP is 5.4.3.2.

# iptables -t nat -nvL POSTROUTING
Chain POSTROUTING (policy ACCEPT 25M packets, 1534M bytes)
 pkts bytes target     prot opt in     out     source               destination         
 348K   31M SNAT       0    --  *      eth2    0.0.0.0/0            0.0.0.0/0           to:5.4.3.2 
# iptables -t nat -I POSTROUTING -o eth2 -s 192.168.1.123 -d 123.123.123.123 -p udp --dport 5060 -j SNAT --to 5.4.3.2:7060

Step (2): start the dialog. It is now connection tracked:

# conntrack -L -s 192.168.1.123 -p udp --reply-port-dst 7060
udp      17 165 src=192.168.1.123 dst=123.123.123.123 sport=5060 dport=5060 packets=15 bytes=7670 src=123.123.123.123 dst=5.4.3.2 sport=5060 dport=7060 packets=37 bytes=10179 [ASSURED] mark=0 use=1

Step (3): remove static NAT mapping, optionally replacing it with a new one and kill the tracked connection.

# iptables -t nat -D POSTROUTING -o eth2 -s 192.168.1.123 -d 123.123.123.123 -p udp --dport 5060 -j SNAT --to 5.4.3.2:7060
# iptables -t nat -I POSTROUTING -o eth2 -s 192.168.1.123 -d 123.123.123.123 -p udp --dport 5060 -j SNAT --to 5.4.3.2:7061
# conntrack -D -s 192.168.1.123 -d 123.123.123.123 -p udp --orig-port-src 5060 --orig-port-dst 5060

Step (4): watch how the dialog continues on the new external source port. You're done.

# conntrack -L -s 192.168.1.123 -p udp --reply-port-dst 7061
udp      17 64 src=192.168.1.123 dst=123.123.123.123 sport=5060 dport=5060 packets=5 bytes=3208 src=123.123.123.123 dst=5.4.3.2 sport=5060 dport=7061 packets=6 bytes=2618 [ASSURED] mark=0 use=1

2010-08-30 - sip totag / grandstream / register

SIP Question: The Grandstream GXP2000 1.1.2.23 sends SIP REGISTER requests with a To tag. Is my proxy wrong in refusing the request?

REGISTER sip:server SIP/2.0
Via: SIP/2.0/UDP 1.2.3.4:5074;branch=z9hG4bK0b90873d634698eb
From: "phone 123" <sip:123@server>;tag=c29eb9104c6a5a86
To: <sip:123@server>;tag=as77984b6
Contact: <sip:123@1.2.3.4:5074;transport=udp>
Supported: path
Authorization: Digest username="123", realm="server", algorithm=MD5, uri="sip:server", nonce="0997652c", response="3b91afb768c11ae0a0405e1bed41bc23"
Call-ID: 3033205eb0b5d203@192.168.4.117
CSeq: 56349 REGISTER
Expires: 3600
User-Agent: Grandstream GXP2000 1.1.2.23
Max-Forwards: 70
Allow: INVITE,ACK,CANCEL,BYE,NOTIFY,REFER,OPTIONS,INFO,SUBSCRIBE,UPDATE,PRACK,MESSAGE
Content-Length: 0

Answer: no, the proxy is right. RFC 3261 says this about it.

8.1.1.2 To

   A request outside of a dialog MUST NOT contain a To tag; the tag in
   the To field of a request identifies the peer of the dialog.  Since
   no dialog is established, no tag is present.

10.2 Constructing the REGISTER Request

   A REGISTER request does not establish a dialog.

Grandstream has already fixed this bug long ago (in version 1.1.2.27, beginning of 2007) as you can see in the release notes on www.grandstreamsucks.com. You should upgrade the firmware.

2010-08-04 - nxclient / locale passing

So, I achieved victory on getting the compose key to work in the NX session. On to get a proper English language setting on our Terminal Server.

The configuration suffers from two problems:
(1) /etc/environment had values set (LANG=nl_NL.UTF-8 and LANGUAGE=nl_NL:nl). (2) nxssh does not pass the LANG/LC_* environment variables.

If I were to remove the /etc/environment variables and configure everything like in a previous post of mine, everyone gets the POSIX locale (nxssh doesn't pass anything). That's not good. I can't find a way to get nxssh(1) to send the needed environment, so we'll have to look elsewhere.

The fix: configure sshd(1) on the server to PermitUserEnvironment yes and create a custom ~/.ssh/environment in my homedir on the server with the following settings:

LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_PAPER=en_GB.UTF-8
LC_TIME=en_GB.UTF-8

My environment gets loaded after the global environment, so I get my custom locale. At the same time I haven't broken it for everyone else. (Observe that the PermitUserEnvironment can have security implications in some cases.)

2010-08-03 - altgr / nxclient / compose key

Like various reports on the internet suggest, the AltGr compose key doesn't work properly or not at all from an NXClient connected to an NXServer (FreeNX in my case).

Note that this is a different issue from the one where Alt_R (and Super_L, Super_R, Ctrl_R en Menu) remains pressed after which no normal typing is possible. That issue is described in Alt Gr keeps stuck and involves a new int sendKey = 0; in nxagent that should be reverted.

I run Ubuntu Lucid Lynx on my desktop with the NXClient, and we run the same on the terminal server. Normally getting the Compose Key to work is simple: go to the System menu, then Preferences, then Keyboard. Choose Layouts, Options and set the Compose Key to Right Alt:

Keyboard Preferences: compose key

However, on the server this has no effect. When comparing output of the xev(1) program, we see the following when pressing AltGr, single-quote, e (to get é):

$ diff -uw client_xev server_xev
--- client_xev 2010-08-02 12:38:23.923838872 +0200
+++ server_xev 2010-08-02 12:44:50.843839010 +0200
@@ -1,11 +1,11 @@
 KeyPress event, serial NN, synthetic NO, window 0xXXX,
-    state 0x10, keycode 108 (keysym 0xff20, Multi_key), same_screen YES,
+    state 0x10, keycode 113 (keysym 0xffea, Alt_R), same_screen YES,
     XLookupString gives 0 bytes: 
     XmbLookupString gives 0 bytes: 
-    XFilterEvent returns: True
+    XFilterEvent returns: False
 
 KeyRelease event, serial NN, synthetic NO, window 0xXXX,
-    state 0x10, keycode 108 (keysym 0xff20, Multi_key), same_screen YES,
+    state 0x18, keycode 113 (keysym 0xffea, Alt_R), same_screen YES,
     XLookupString gives 0 bytes: 
     XFilterEvent returns: False
 
@@ -13,7 +13,7 @@
     state 0x10, keycode 48 (keysym 0x27, apostrophe), same_screen YES,
     XLookupString gives 1 bytes: (27) "'"
     XmbLookupString gives 1 bytes: (27) "'"
-    XFilterEvent returns: True
+    XFilterEvent returns: False
 
 KeyRelease event, serial NN, synthetic NO, window 0xXXX,
     state 0x10, keycode 48 (keysym 0x27, apostrophe), same_screen YES,
@@ -24,12 +24,6 @@
     state 0x10, keycode 26 (keysym 0x65, e), same_screen YES,
     XLookupString gives 1 bytes: (65) "e"
     XmbLookupString gives 1 bytes: (65) "e"
-    XFilterEvent returns: True
-
-KeyPress event, serial NN, synthetic NO, window 0xXXX,
-    state 0x10, keycode 0 (keysym 0xe9, eacute), same_screen YES,
-    XLookupString gives 0 bytes: 
-    XmbLookupString gives 2 bytes: (c3 a9) "é"
     XFilterEvent returns: False
 
 KeyRelease event, serial NN, synthetic NO, window 0xXXX,

Two things stand out here:
(1) The right alt press is seen as Alt_R and not Multi_key and XFilterEvent returns False (meaning that the receiving application should take action on the keypress).
(2) The keycode observed is 113 instead of 108.

This information is enough to fix the issue. First get the Multi_key setting for xmodmap(1):

$ xmodmap -pke | grep Multi_key
keycode 108 = Multi_key Multi_key Multi_key Multi_key

Put that line in ~/.xmodmaprc on the server, changing the 108 to 113. You test if it works by running xmodmap(1) manually with the .xmodmaprc as argument. If everything works like it should, you should now have a working compose key again: ¡Vìctørý!

2010-06-20 - python2.6 features / python2.5

Today I'll show you some quick and dirty python2.5 compatibility fixes. Of course you're developing on python2.6 or even python3.x, but your customer still lives in the dark ages. Here are two fixes that might come in handy.

ImportError: No module named ssl

Falling back to python2.5 socket.SSL if there is no python2.6 ssl through a small wrap_socket replacement:

import socket
try:
    from ssl import wrap_socket
except ImportError:
    class wrap_socket:
        def __init__(self, socket):
            self.socket = socket
        def __getattr__(self, name):
            return getattr(self.socket, name)
        def connect(self, *args, **kwargs):
            self.socket.connect(*args, **kwargs)
            self.ssl = socket.ssl(self.socket)
        def read(self, *args, **kwargs):
            return self.ssl.read(*args, **kwargs)
        recv = read
        def write(self, *args, **kwargs):
            return self.ssl.write(*args, **kwargs)
        send = write

AttributeError: 'unicode' object has no attribute 'format'

Falling back to python2.5 Template if there is no python2.6 string.format. It does not do any fancy formatting, but in many cases you're not using that either. (Yes, for template syntax, {variable} wins in readability over $variable or, even worse, %(variable)s.)

if not hasattr('', 'format'):
    from string import Template
    import re
    def str_format(string, *args, **kwargs):
        tpl = Template(string)
        tpl.pattern = str_format.re
        return tpl.substitute(*args, **kwargs)
    str_format.re = re.compile(r'\{(?P<named>[A-Za-z0-9_]+)\}')

Usage:

try:
    value = template.format(**values)
except AttributeError:
    value = str_format(template, **values)

2010-06-17 - uninitialized globals / C language

As per the C language spec., uninitialized globals are initialized to zero (0). Nandu310 tells us why on his blog about the memory areas in the C language.

Data segment: the data segment is to hold the value of those variables that need to be available throughout the life time of the program. [...]

There are two parts in this segment. The initialized data segment and uninitialized data segment. When variables are initialized to some value (other than 0 or which is different value), they are allocated in the initialized segment (.data). When the variables are uninitialized they get allocated in the uninitialized data segment (.bss). [...] [The] job of initialization of variables to zero is left to the operating system to take care of. This bulk initialization can greatly reduce the time required to load.

When we want to run an executable program, the OS starts a program known as loader. When this loads the file into memory, it takes the BSS segment and initializes the whole thing to zeros. That is why the uninitialized global data and static data always get the default value of zero.

Nandu310 explains the segments in more detail and uses the nm tool. Using objdump you can get a more verbose view.

--- uninited.c  2010-06-17 09:14:59.364090777 +0200
+++ initone.c 2010-06-17 09:15:57.144090752 +0200
@@ -1,5 +1,5 @@
 int my_int;
-int my_int2;
+int my_int2 = 3;
 int main() {
     return my_int + my_int2;
 }

Calling objdump on the compiled C programs shows you how my_int2 moves from the .bss section to the .data section between the two binaries.

$ objdump -zD uninited | sed -ne '/<main>/,/^$/p;/of section \.data/,/of section \.comment/p' 
...
Disassembly of section .bss:
...
000000000060102c <my_int2>:
  60102c: 00 00                 add    %al,(%rax)
  60102e: 00 00                 add    %al,(%rax)
...
$ objdump -zD initone | sed -ne '/<main>/,/^$/p;/of section \.data/,/of section \.comment/p'
...
Disassembly of section .data:
...
0000000000601018 <my_int2>:
  601018: 03 00                 add    (%rax),%eax
  60101a: 00 00                 add    %al,(%rax)
...

The moral of the story: globals in C are always initialized.

And, for the record, when using gcc — even without optimizations — there is no difference between initializing to 0 and leaving the variable uninitialized: they both go in the .bss. This means that you get no benefit from leaving a variable uninitialized. For readability, it is often better to explicitly initialize it anyway.

2010-05-07 - unexpanded tabs / mercurial web / diff

The hgweb mercurial web interface on current Debian/Squeeze (mercurial-common 1.5.1-2) lists tab characters as-is in the diff view.

Every line is prefixed not only by a plus or a minus (unified diff), but also by file and line numbers. This can cause a tab (0x9) character to appear as a single space. This does not look nice.

The following patch can be applied to expand the tab character so the intentation looks right again.

--- /usr/share/pyshared/mercurial/hgweb/webutil.py.orig 2010-05-07 09:41:17.000000000 +0200
+++ /usr/share/pyshared/mercurial/hgweb/webutil.py      2010-05-07 09:47:53.000000000 +0200
@@ -177,6 +177,7 @@ def diffs(repo, tmpl, ctx, files, parity
                 ltype = "difflineat"
             else:
                 ltype = "diffline"
+            l = l.expandtabs()
             yield tmpl(ltype,
                        line=l,
                        lineid="l%s" % lineno,

2010-04-27 - asian site visits / trend micro

Today I noticed some odd traffic from a couple of chinese and japanese IP addresses on a website that I maintain.

A known visitor visited the site with (anonymized) IP address 1.2.3.4 and a minute later the chinese and japanese IP addresses started following suit:
netblock 150.26.0.0 - 150.100.255.255 "Japan Network Information Center" and
netblock 216.104.0.0 - 216.104.31.255 "TREND MICRO INCORPORATED".

1.2.3.4 - -        [25/Apr/2010:09:14:32 +0200] GET /oper/waitingroom/ 
1.2.3.4 - -        [25/Apr/2010:09:14:24 +0200] GET /media/252/js/jquery-1.3.2.min.js 
1.2.3.4 - -        [25/Apr/2010:09:14:24 +0200] GET /media/252/js/userchat.js 
1.2.3.4 - -        [25/Apr/2010:09:14:24 +0200] GET /media/252/js/jquery.cookie.js
1.2.3.4 - -        [25/Apr/2010:09:14:33 +0200] GET /poll?_=1272179685670 
...
216.104.15.138 - - [25/Apr/2010:09:15:42 +0200] GET /oper/waitingroom/
216.104.15.134 - - [25/Apr/2010:09:16:28 +0200] GET /media/252/js/userchat.js 
216.104.15.130 - - [25/Apr/2010:09:16:30 +0200] GET /media/252/js/jquery-1.3.2.min.js 
216.104.15.130 - - [25/Apr/2010:09:16:31 +0200] GET /media/252/js/jquery.cookie.js 
150.70.84.48 - -   [25/Apr/2010:09:16:33 +0200] GET /poll?_=1272179685670

I contacted the known visitor and indeed she was running some kind of Trend Micro security-all-in-one-tool. So, this probably is "legal" traffic designed to detect malicious websites.

The asian web requests did not supply the session cookies, so these foreign requests do not present an immediate security concern. What does concern me however is that (1) the traffic from 1.2.3.4 is now doubled — or at least the request counts are — and (2) googling for these netblocks or IP-addresses does not turn up any info that these requests are legitimate.

FYI, the User-Agent string reported by these bots is Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1).

2010-04-09 - mysql utf8 collation / conversion

On a clean MySQL install — on a Debian or Ubuntu system at least — the MySQL server gets the latin1_swedish_ci with latin1 character set by default. Every time you set up a new machine, you must remember to either fix the defaults in my.cnf config file or to supply character set and collation options when creating databases.

Of course you'll opt to set this by default in my.cnf first:

[client]
default-character-set = utf8

[mysqld]
init_connect = 'SET collation_connection = utf8_unicode_ci'
default-character-set = utf8
character-set-server = utf8
collation-server = utf8_unicode_ci

Unfortunately, sometimes you forget to do this, and now you have a database in some legacy (non-utf-8) character set. Well. Here is the fix. Create this stored procedure and run it supplying the the schema_name. It converts the database(*) and the tables to utf-8 with the utf8_unicode_ci collation.

delimiter //

DROP PROCEDURE IF EXISTS convert_to_utf8;
//

CREATE PROCEDURE convert_to_utf8 (schema_name VARCHAR(64))
BEGIN
  DECLARE done INT DEFAULT 0;
  DECLARE table_name VARCHAR(64);
  DECLARE schema_cur CURSOR FOR
    SELECT t.table_name FROM information_schema.tables t
    WHERE CAST(table_schema AS CHAR CHARACTER SET utf8) COLLATE utf8_unicode_ci
        = CAST(schema_name AS CHAR CHARACTER SET utf8) COLLATE utf8_unicode_ci
      AND table_collation NOT LIKE 'utf8%';
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

  -- ALTER DATABASE
  SET @statement = CONCAT('ALTER DATABASE ', schema_name,
    ' CHARACTER SET utf8 COLLATE utf8_unicode_ci');
  -- "This command is not supported in the prepared statement protocol yet"
  -- PREPARE executable FROM @statement;
  -- EXECUTE executable;
  -- DEALLOCATE PREPARE executable;
  SELECT @statement AS `Run this by hand!`;

  -- ALTER TABLES
  OPEN schema_cur;
  REPEAT
    FETCH schema_cur INTO table_name;
    IF NOT done THEN
      SET @statement = CONCAT('ALTER TABLE ', schema_name, '.', table_name,
        ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci');
      PREPARE executable FROM @statement;
      EXECUTE executable;
      DEALLOCATE PREPARE executable;
    END IF;
  UNTIL done END REPEAT;
END;
//

After switching back the delimiter to ';', you can call this stored procedure like this:

CALL convert_to_utf8('meetbees_db');

This converts the meetbees_db database to the utf-8 characters set with the utf8_unicode_ci collation.

(*) The ALTER DATABASE command must be run by hand as the MySQL version that I've tried it on reports that “[this] command is not supported in the prepared statement protocol yet”. That's one command to copy-paste, so you should be able to manage that.

2010-03-06 - anwb rijopleiding cdrom / linux

Regelmatig wordt er niet aan de Linux-gebruikers gedacht als ergens een custom applicatie wordt gemaakt. Zo is het natuurlijk ook bij de Examentraining Rijbewijs B. De 2005 versie bevat een binary voor Windows en MAC OS9. De CD-ROM uit 2008 bevat eentje voor Windows en voor MAC-OSX.

Waar dit keer echter wel aan gedacht is, is het niet nodeloos moeilijk verpakken van de data op de CD.

$ ls /media/cdrom
anwb.ico                       ANWB_Rijopleiding_PC.exe  Desktop DB  Fdata
ANWB_Rijopleiding_MAC_OSX.osx  AUTORUN.INF               Desktop DF  xtras
$ ls /media/cdrom/Fdata/Data/
DATA10.CFG  DATA2.CFG  DATA4.CFG  DATA6.CFG  DATA8.CFG
DATA1.CFG   DATA3.CFG  DATA5.CFG  DATA7.CFG  DATA9.CFG
$ head -n4 /media/cdrom/Fdata/Data/DATA1.CFG
001 11 03 --------------------------
De maximumsnelheid voor een motorvoertuig bedraagt op deze rijbaan:
km/h
30 30,0 30,00
$ ls /media/cdrom/Fdata/Gdata/ | head -n3
000.jpg
001.jpg
002.jpg

Mooi! Als het zo makkelijk is, kunnen we daar net zo goed een scriptje omheen vouwen. Dan hoeven we niet een Windows VM te regelen.

Zo gezegd, zo gedaan:
ANWB_Rijopleiding_Linux.py (view)

Het gebruik ziet er als volgt uit:

Linux versie van ANWB Examentraining

Het script neemt 3 of 4 argumenten:
- het pad naar de ANWB CD-ROM
- de eerste vraag (bijvoorbeeld 1 of 451)
- de laatste vraag (bijvoorbeeld 50 of 500)
- optioneel of de vragen in een willekeurige volgorde gesteld moeten

$ ./ANWB_Rijopleiding_Linux.py /media/cdrom 1 2
Attempting to parse ANWB files in /media/cdrom...
Parsing DATA1.CFG (offset 0)...
Parsing DATA10.CFG (offset 450)...
<snip>
Parsing DATA8.CFG (offset 350)...
Parsing DATA9.CFG (offset 400)...
Found and parsed 500 questions...
Found 500 corresponding images...
<... beantwoord de eerste twee vragen ...>
Right answers: 2
Wrong answers: 0

Succes!

Het script ook de geluidsfragmenten met de vraag laten afspelen — ja, er is ook een Fdata/Sdata met 500 sounds — laat ik over als exercitie voor de lezer.

2010-01-30 - dutch public broadcasting teletext

Laying around for almost a year, gathering dust, for your enjoyment here is:
a parsable plain text version of the Dutch Public Broadcasting teletext pages.

As the format hardly ever changes, it can be used as an data aggregation source. For example, for fetching the current Eredivisie ranking or the scores for latest Eredivisie games.

Example

 819     Teletekst za 30 jan           
    OETBAL -----------------------------
 +-+      eredivisie,stand per 29/01    
 ---------------------------------------
                                        
  1. PSV          19 16  3  0  51  47-13
  2. FC Twente    19 15  4  0  49  38-15
  3. Ajax         19 13  3  3  42  55-16
  4. Feyenoord    19 11  5  3  38  30-16
  5. Heracles Al. 19 10  2  7  32  27-26
  6. FC Utrecht   19  8  7  4  31  23-18
  7. AZ           19  9  1  9  28  32-22
  8. NAC Breda    19  7  5  7  26  26-29
  9. VVV-Venlo    19  4  9  6  21  26-27
 10. FC Groningen 19  5  6  8  21  22-28
 11. Vitesse      19  6  3 10  21  23-33
 12. Roda JC      19  5  5  9  20  25-34
 13. Heerenveen   19  6  2 11  20  21-34
 14. Sparta R'dam 19  5  4 10  19  19-35
 15. Willem II    20  5  2 13  17  24-39
 16. NEC          19  3  7  9  16  21-35
 17. ADO Den Haag 19  3  6 10  15  18-35
 18. RKC Waalwijk 20  4  0 16  12  17-39
   programma op 818 / topscorers op 832 
 volgende   nieuws   sport   voetbal    

The teletekst viewer also features colored (html) versions. The source — the website uses the images from the NOS pages as data source — is freely available.