Category Archives: Computing
The KeymanWeb Hello World app
A couple of days ago I helped some developers put together a very basic web page that uses KeymanWeb to showcase their keyboard layout. The idea is that a visitor to the page will immediately be able to try out their keyboard layout without any fiddling around.
Another requirement was that the page work well on desktop and mobile devices. This requires a few little tricks, mostly because KeymanWeb has some special requirements to work smoothly on mobiles and tablets.
I’ve presented snippets of the code, in the order they appear in the document.
And here are screenshots just for posterity.
The usual HTML 5 boilerplate is thrown in at the top of the page. Our first trick is to put the page into an appropriate zoom and scroll mode for mobile and tablet devices with a viewport
meta tag. This is done with the following code:
<!-- at present, KMW requires this for mobile -->
<meta name="viewport" content="width=device-width, user-scalable=no">
At the time of writing, KeymanWeb (build 408) requires the viewport meta tag in order to render correctly on a variety of devices. Both clauses in the content attribute are required, and the keyboard will not scale correctly without them.
Next, I add a link to the stylesheet for the on screen keyboard. This is optional, because KeymanWeb will inject the link itself anyway, but it does prevent a Flash Of Unstyled Content.
<!-- this is optional but eliminates a flash of unstyled content -->
<link href='https://s.keyman.com/kmw/engine/408/src/osk/kmwosk.css' rel='stylesheet' type='text/css'>
I throw in some basic styling for the desktop and touch devices. You’ll see a selector of body.is-desktop
on a couple of the style selectors. I use this instead of a @media
query because this is differentiating between viewport-controlled devices and plain old boring desktop browsers. There may be a better way of doing this now, but I haven’t found it yet. The code for setting the is-desktop
class is found further on in this post.
/* Styles for the page */
* {
font-family: Tahoma, sans-serif;
}
body.is-desktop {
padding: 22px 2px 0 2px;
width: 900px;
margin: auto;
}
h1 {
font-size: 24pt;
font-weight: bold;
margin: 0 0 24px 0;
padding: 0;
}
body.is-desktop h1 {
font-size: 48pt;
}
/* Style the text area */
textarea {
width: 100%;
height: 250px;
font-family: "Khmer OS";
font-size: 24px;
}
I need to talk a bit more about the On Screen Keyboard (OSK) styling. While I could use the default styling (as shown below), this looks a little dated and I wanted to show how the keyboard could be styled as you like. There is some complexity to the OSK styling as it has many moving parts, around sizing, scaling and positioning of each individual key. I don’t want to throw away that baby, so instead I keep all the bathwater and just add some bubbles to jazz up the display the way I want it. That’s a mixed metaphor, but it was late when I wrote this.
First, this CSS rule-set removes the thick red border and dark background, and adjusts the padding around the sides to compensate.
/* Style the on screen keyboard */
body .desktop .kmw-osk-inner-frame {
background: white;
border: solid 1px #404040;
padding-bottom: 2px;
padding-top: 8px;
}
Next, I hide the header and footer for the OSK. I don’t need them for this demo.
body .desktop .kmw-title-bar,
body .desktop .kmw-footer {
display: none;
}
I tweak the spacing between keys and rows and make the keys slightly less rounded.
body .desktop .kmw-key-square {
border-radius: 2px;
padding-bottom: 8px;
}
body .desktop .kmw-key {
border-radius: 2px;
border: solid 1px #404040;
background: #f2f4f6;
}
body .desktop .kmw-key:hover {
background: #c0c8cf;
}
And that’s it for the CSS changes. It’s really pretty easy to restyle the keyboard without losing the benefits of a scalable, cross-platform keyboard. You’ll note that sneaky .desktop selector creeping in there. That’s because I’ve opted to style only the desktop keyboard; the touch layouts are already pretty nice and I’ll keep them as is.
I load the KeymanWeb code from the KeymanWeb CDN. You can load from the KeymanWeb CDN (running on Azure) or keep a locally hosted copy, of course. I’ve commented out the source version and because we have only a single keyboard, I have elected not to include a menu for switching languages.
<!-- Uncomment these lines if you want the source version of KeymanWeb (and remove the keymanweb.js reference)
<script src="https://s.keyman.com/kmw/engine/408/src/kmwstring.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/kmwbase.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/keymanweb.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/kmwosk.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/kmwnative.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/kmwcallback.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/kmwkeymaps.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/kmwlayout.js"></script>
<script src="https://s.keyman.com/kmw/engine/408/src/kmwinit.js"></script>
-->
<script src="https://s.keyman.com/kmw/engine/408/keymanweb.js"></script>
<!-- Uncomment if you want the user to be able to switch keyboards with a toggle popup ...
or you can create a custom switch if you prefer
<script src='https://s.keyman.com/kmw/engine/408/kmwuitoggle.js'></script>
-->
Now, no self-respecting web developer is going to include inline script, except in a Hello World demo. So let’s call this the KeymanWeb Hello World demo, so I can keep my self respect. Or something.
I use a simple page load listener. You could use an earlier listener, such as DOMContentLoaded
, but window.onload
has a rich and nearly honourable history.
// This should really be in a separate file.
// But for now in one file is easier to understand
// After everything has loaded
window.addEventListener('load', function() {
Next some voodoo. Allows me to style the touch and desktop versions differently, as I touched on earlier. Why? Because KeymanWeb works very differently on touch devices. If you think about this, it must be so. A touch device has its own soft keyboard, which KeymanWeb must, by slightly convoluted means, hide, and replace with its own soft keyboard. Whereas, on a desktop device, KeymanWeb can show a utility soft keyboard, but does most of its interaction by getting in there between the hardware keyboard and the input fields.
Don’t ask me about Windows touch devices. Does anyone actually use those? (Okay, I’m sure they do, but it’s a rocky path for us poor keyboard devs to tread!)
if(!tavultesoft.keymanweb.util.isTouchDevice()) {
document.body.className += 'is-desktop';
}
Next, make sure KeymanWeb is initialised. If I don’t do this myself, KeymanWeb will do so when things are ready, but I need KeymanWeb to be ready in order to load keyboards and attach events, and so on.
tavultesoft.keymanweb.init();
I could add another keyboard, or a stock keyboard from the repository. I haven’t, but this shows you how.
//tavultesoft.keymanweb.addKeyboards('@eng'); // Loads default English keyboard from Keyman Cloud (CDN) if you want it
In this case, I have a custom keyboard, developed by Lyheng Phuoy. This is an early beta version of the keyboard but already it’s very impressive.
// Add our custom keyboard
tavultesoft.keymanweb.addKeyboards({
name: 'Khmerism', // Display name of the keyboard
id: 'lyhengkeyboard', // ID of the keyboard for reference in code
filename: './lyhengkeyboard-1.0.js', // source of the keyboard for dynamic load
version: '1.0', // version of the keyboard, optional
language: [{
name: 'Khmer', // language name for UI elements
id: 'khm', // language ID for tagging text
region: 'as' // region of the language, for UI elements, optional
}]
});
In this section, I want to control the size, position and flexibility of the keyboard. I don’t want it to be resizable or movable. So I set nomove
and nosize
accordingly.
var ta = document.getElementsByTagName('textarea')[0];
// Watch for the keyboard being shown and set its position
tavultesoft.keymanweb.osk.addEventListener('show', function() {
var rectParams = {
left: ta.offsetLeft,
top: ta.offsetTop + ta.offsetHeight + 8, // a small gap below the text area
width: ta.offsetWidth,
height: ta.offsetWidth * 0.4, // a pleasing aspect ratio
nomove: true,
nosize: true
};
tavultesoft.keymanweb.osk.setRect(rectParams);
});
// Focus text area after everything loads
ta.focus();
Finally, in the body element, I set a special magic class, osk-always-visible
, so that KeymanWeb doesn’t hide its on screen keyboard after displaying it the first time. And then we have basically the world’s simplest page with a textarea
.
<!-- The osk-always-visible class tells KeymanWeb not to hide the osk on blur, after it is
shown the first time -->
<body class="osk-always-visible">
<h1>Typing Khmer</h1>
<textarea rows="6" cols="60"></textarea>
</body>
And that’s it! I’d love to see what you do with KeymanWeb on your own sites.
Here’s the full keyboard source, and that link to the demo page again, including Lyheng’s keyboard (with his permission).
When ញ៉ាំ meets ញ៉ំា
The Khmer script was added to the Unicode standard in September 1999. Today, nearly 18 years later, operating system renderers still get it wrong.
This is a quick post to document the difference in how several Khmer words are wrongly rendered on different current operating systems. I ran these tests on Windows 10 (10.0.14393), Android 7.1.1 Nougat, iOS 10.2.1, Mac OS X Sierra <> and Ubuntu 16.04 LTS with Firefox 47. The good news is that Windows 10 and Ubuntu passed all the tests (bar a font style issue with Leelawadee UI). Android passed nearly everything, except the bad encoding test.
Now, admittedly, the rules around triisap (U+17CA) and muusikatoan (U+17C9) are very complex. The Unicode standard description covers most of the difficulties, but not all of them.
Muusiaktoan is also sometimes called ធ្មេញកណ្ដុរ /tmɨɲ kɑndao/ – rat’s teeth, which is a fun name.
On to the words. In every case, the DauhPenh rendering is correct.
ញ៉ាំ /ɲam/ To eat
ញ | ៉ | ា | ំ |
U+1789 | U+17C9 | U+17B6 | U+17C6 |
ស៊ី /sii/ To eat (for young)
ស | ៊ | ី |
U+179F | U+17CA | U+17B8 |
As of Mac OS X Sierra, /sii/ now displays correctly. But contrast with /ʔum/, /ʔom/ below.
អ៊ំ /ʔum/, /ʔom/ Uncle, aunt
អ | ៊ | ំ |
U+17A2 | U+17CA | U+17C6 |
Note how Leelawadee UI renders this wrongly; but that is a font rather than a renderer bug.
ប៊ី /bii/ A type of egg roll
ប | ៊ | ី |
U+1794 | U+17CA | U+17B8 |
ប៉ី /pəy/ A type of wind instrument
ប | ៉ | ី |
U+1794 | U+17C9 | U+17B8 |
As of Mac OS X Sierra, /pəy/ now displays correctly. But contrast with /bii/ above!
Yum yum yum
ញ៉ាំ /ɲam/ To eat
I’d like to pull out the word ញ៉ាំ for further analysis. Every operating system has some trouble with this word, because it could be encoded in several different ways. The correct way works on everything except iOS and Mac OS X. The incorrect encodings should really display wrongly, but none of the renderers complain about both invalid forms!
Correct order (ញ៉ាំ)
ញ | ៉ | ា | ំ |
U+1789 | U+17C9 | U+17B6 | U+17C6 |
Incorrect order (ញ៉ំា)
ញ | ៉ | ំ | ា |
U+1789 | U+17C9 | U+17C6 | U+17B6 |
Incorrect vowel (ញុំា)
ញ | ុ | ំ | ា |
U+1789 | U+17BB | U+17C6 | U+17B6 |
In this instance, The DauhPenh rendering is appropriate for the first and second lines; the Apple rendering is ironically most appropriate for the third line!
Many thanks to Makara for his suggestion on the second incorrect rendering; I updated this post shortly after initial posting to include the extra example. There are other possible letter orders which may or may not display “correctly”; I will leave finding these as an exercise for the reader.
ZWNJ FTW
Here’s one I’ll examine in detail another time. Some words can be written in two different ways, neither really incorrect. The Unicode standard caters for these by allowing for insertion of a Zero Width Non Joiner (U+200C) to force the superscripted form of triisap (៊) or muusikatoan (៉). Windows 10’s Leelawadee UI font gets this one wrong (but its DauhPenh font doesn’t).
អ៊ី or អ៊ី /ʔii/ An exclamation of surprise
អ | ៊ | ី |
U+17A2 | U+17CA | U+17B8 |
អ | ZWNJ | ៊ | ី |
U+17A2 | U+200C | U+17CA | U+17B8 |
Note: table ZWNJ character order corrected as per comment by Olivier Berten.
Let’s Encrypt on Windows, redux
Jan 2020, please note: This approach is now deprecated. Let’s Encrypt will stop accepting ACMEv1 requests in June 2020. Have a look at https://letsencrypt.org/docs/client-options/ for alternatives to this process.
A couple of months ago I wrote a script to automatically renew Let’s Encrypt certificates in PowerShell on Windows. The renewal process works really well, however, there is one wrinkle that I did not cover. In this blog post I smooth out that wrinkle!
The wrinkle
When it came time to renew my certificate, a few days ago (well within the 90 day limit for the certificate, mind you), I discovered that the identifier for the certificate had expired. Why was this a problem? Well, I had originally used a manual DNS challenge to validate the identifier with Let’s Encrypt. This worked fine, but of course, manually creating a new identifier and challenge every 90 days completely undoes the benefit of automatic certificate renewal.
Smoothing out the wrinkle
In order to resolve this, I needed to automatically generate and validate an identifier at the same time as I generated a new certificate. The only automatic challenge provider at this time with ACMESharp is the http-01 provider with the IIS handler.
So I updated the script to generate the identifier automatically and validate with the http-01 challenge provider. However, the http-01 challenge provider requires a HTTP request, not a HTTPS request, to the hostname in question, because, and I quote the IETF draft here:
… Because many webservers allocate a default HTTPS virtual host to a particular low-privilege tenant user in a subtle and non-intuitive manner, the challenge must be completed over HTTP, not HTTPS.
I love subtle and non-intuitive computing!
But this was a problem for me, because the secure site I was working on did not have a http endpoint, only a https endpoint, which is the whole reason I used a DNS challenge in the first place.
I finally threw in the towel and I decided to setup a http endpoint, and configure automatic redirection to https (yes, I could go further here). You may find you need to setup an exception for automatic redirection for the .well-known/ root folder (where http challenges are kept as static files). I’ll leave that tweak for you to figure out (it’s just another line or two in your site root web.config).
A deeper wrinkle
The wrinkles get a little deeper, when you look at what happens if you already have a valid challenge response for an identifier. This could happen if you had manually validated another alias for the same identifier using e.g. dns, as I had in the past, or if you are renewing before your existing identifier expires. Because the challenge responses are matched to the hostname, and not the aliases, the previously valid responses continue to be acceptable. In this situation, the existing challenge response was used by the Let’s Encrypt server, and it never checked my shiny new web server challenge endpoint. This meant we needed to see if any existing challenge responses were considered valid, rather than relying on checking the challenge we’d just setup. The script changes handle this scenario.
Just read me the script
The full PowerShell script is shown below; the changes begin at the start of the Try
block, and finish after the New-ACMECertificate
call. More detail on how the script works is provided in the previous blog post.
import-module ACMESharp
#
# Script parameters
#
$domain = "my.example.com"
$iissitename = "my.example.com"
$certname = "my.example.com-$(get-date -format yyyy-MM-dd--HH-mm)"
#
# Environmental variables
#
$PSEmailServer = "localhost"
$LocalEmailAddress = "[email protected]"
$OwnerEmailAddress = "[email protected]"
$pfxfile = "c:\Admin\Certs\$certname.pfx"
$CertificatePassword = "PASSWORD!"
#
# Script setup - should be no need to change things below this point
#
$ErrorActionPreference = "Stop"
$EmailLog = @()
#
# Utility functions
#
function Write-Log {
Write-Host $args[0]
$script:EmailLog += $args[0]
}
Try {
Write-Log "Generating a new identifier for $domain"
New-ACMEIdentifier -Dns $domain -Alias $certname
Write-Log "Completing a challenge via http"
Complete-ACMEChallenge $certname -ChallengeType http-01 -Handler iis -HandlerParameters @{ WebSiteRef = $iissitename }
Write-Log "Submitting the challenge"
Submit-ACMEChallenge $certname -ChallengeType http-01
# Check the status of the identifier every 6 seconds until we have an answer; fail after a minute
$i = 0
do {
$identinfo = (Update-ACMEIdentifier $certname -ChallengeType http-01).Challenges | Where-Object {$_.Status -eq "valid"}
if($identinfo -eq $null) {
Start-Sleep 6
$i++
}
} until($identinfo -ne $null -or $i -gt 10)
if($identinfo -eq $null) {
Write-Log "We did not receive a completed identifier after 60 seconds"
$Body = $EmailLog | out-string
Send-MailMessage -SmtpServer $PSEmailServer -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Attempting to renew Let's Encrypt certificate for $domain" -Body $Body
Exit
}
# We now have a new identifier... so, let's create a certificate
Write-Log "Attempting to renew Let's Encrypt certificate for $domain"
# Generate a certificate
Write-Log "Generating certificate for $domain"
New-ACMECertificate $certname -Generate -Alias $certname
# Submit the certificate
Submit-ACMECertificate $certname
# Check the status of the certificate every 6 seconds until we have an answer; fail after a minute
$i = 0
do {
$certinfo = Update-AcmeCertificate $certname
if($certinfo.SerialNumber -eq "") {
Start-Sleep 6
$i++
}
} until($certinfo.SerialNumber -ne "" -or $i -gt 10)
if($i -gt 10) {
Write-Log "We did not receive a completed certificate after 60 seconds"
$Body = $EmailLog | out-string
Send-MailMessage -SmtpServer $PSEmailServer -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Attempting to renew Let's Encrypt certificate for $domain" -Body $Body
Exit
}
# Export Certificate to PFX file
Get-ACMECertificate $certname -ExportPkcs12 $pfxfile -CertificatePassword $CertificatePassword
# Import the certificate to the local machine certificate store
Write-Log "Import pfx certificate $pfxfile"
$certRootStore = "LocalMachine"
$certStore = "My"
$pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$pfx.Import($pfxfile,$CertificatePassword,"Exportable,PersistKeySet,MachineKeySet")
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($certStore,$certRootStore)
$store.Open('ReadWrite')
$store.Add($pfx)
$store.Close()
$certThumbprint = $pfx.Thumbprint
# Bind the certificate to the requested IIS site (all https bindings)
Write-Log "Bind certificate with Thumbprint $certThumbprint"
$obj = get-webconfiguration "//sites/site[@name='$iissitename']"
for($i = 0; $i -lt $obj.bindings.Collection.Length; $i++) {
$binding = $obj.bindings.Collection[$i]
if($binding.protocol -eq "https") {
$method = $binding.Methods["AddSslCertificate"]
$methodInstance = $method.CreateInstance()
$methodInstance.Input.SetAttributeValue("certificateHash", $certThumbprint)
$methodInstance.Input.SetAttributeValue("certificateStoreName", $certStore)
$methodInstance.Execute()
}
}
# Remove expired LetsEncrypt certificates for this domain
Write-Log "Remove old certificates"
$certRootStore = "LocalMachine"
$certStore = "My"
$date = Get-Date
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($certStore,$certRootStore)
$store.Open('ReadWrite')
foreach($cert in $store.Certificates) {
if($cert.Subject -eq "CN=$domain" -And $cert.Issuer.Contains("Let's Encrypt") -And $cert.Thumbprint -ne $certThumbprint) {
Write-Log "Removing certificate $($cert.Thumbprint)"
$store.Remove($cert)
}
}
$store.Close()
# Finished
Write-Log "Finished"
$Body = $EmailLog | out-string
Send-MailMessage -SmtpServer $PSEmailServer -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Let's Encrypt certificate renewed for $domain" -Body $Body
} Catch {
Write-Host $_.Exception
$ErrorMessage = $_.Exception | format-list -force | out-string
$EmailLog += "Let's Encrypt certificate renewal for $domain failed with exception`n$ErrorMessage`r`n`r`n"
$Body = $EmailLog | Out-String
Send-MailMessage -SmtpServer $PSEmailServer -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Let's Encrypt certificate renewal for $domain failed with exception" -Body $Body
Exit
}
Side note: I discovered it’s important to let ACMESharp do its thing in the script and not try and do anything with it in another process because it tends to fall over with an exception if some other process is accessing its vault when it wants to.
Another minor update (18 Feb 2017): my $alias
in my live script happened to be the same as my $domain
; if they differed, then the script would fail. The $alias
variable is no longer needed and has been removed from the script above. Thank you to BdN3504 for reporting this in the comments!
Yet another update (15 Apr 2018): The $PSEmailServer
variable in my script was defined but never used. Send-MailMessage
calls updated to use it with -SmtpServer
. Thank you to Roger for reporting.
Automating certificate renewal with Let’s Encrypt and ACMESharp on Windows
Jan 2020, please note: This approach is now deprecated. Let’s Encrypt will stop accepting ACMEv1 requests in June 2020. Have a look at https://letsencrypt.org/docs/client-options/ for alternatives to this process.
UPDATE: 10 February 2017! I’ve updated the script in a new blog post to handle identifer expiry.
Let’s Encrypt is a free, automated, and open Certificate Authority. And it is awesome. It is being used by over 15 million domains already to date.
Let’s Encrypt is a certificate authority. So that means that they issue certificates, specifically for secure https (TLS) websites. Lots of other organisations do this as well. But two things stand out about Let’s Encrypt. First, it’s free! Given that I was paying over $100/year for a certificate for one of my sites until recently, that’s a big win already. The second is, it’s automated!
The automated bit cannot be understated. It means that the first time I use Let’s Encrypt, I have to do a bunch of setup. But from then on, I no longer have to remember the arcane and complicated process of generating a certificate request, uploading it to a CA, waiting for the CA to process the request, and finally importing the certificate along with all the incidentals such as intermediate certificates.
However, the one thing about Let’s Encrypt that has stopped me using it so far is that I run some of my sites on IIS on Windows, but Let’s Encrypt is very *nix-focused. While there are clients for Windows, none of them are very complete and so it’s been a bit of hit and miss using them.
The most up-to-date client/library that I have found appears to be ACMESharp. This library is a PowerShell module, and while there is a GUI front end available, I haven’t used the GUI. I’ve worked entirely with the PowerShell module. ACMESharp is pretty flexible and covers everything I need, except one thing: renewals. It has no built-in automated renewal support. So I rolled my own.
I did most of my work in the Let’s Encrypt staging environment, after foolishly starting in the live environment and rapidly hitting the duplicate certificate rate limit. I recommend you do your testing in the staging environment also!
The easiest way to work in the staging environment is to setup a separate vault profile for ACMESharp. I ended up using my :user
profile for staging and my :sys
profile for the live host. To specify which vault profile you want to use, it’s best to use an environment variable, as otherwise you’ll inevitably forget to append the -VaultProfile
parameter to one of your setup commands and leave yourself in a bit of a mess:
$env:ACMESHARP_VAULT_PROFILE=":user"
What follows is a script setup for my servers, but which should work for most Windows PowerShell scenarios. I have set it up to email me on success or failure; I’ll be watching it over the next little while to ensure that it gets things right. I have set it up as a scheduled task to run every 60 days, per Let’s Encrypt’s recommendation.
The script assumes you have already followed the ACMESharp Quick Start to configure your environment. The variables at the top of the script could be configured as script parameters, but for simplicity I’ve just put them at the top of the script. The script will request a new certificate, import the newly issued certificate into the localmachine certificate store, then assign it to all https bindings on the specified web site instance. Finally, it will delete all expired certificates associated with the domain in question.
UPDATE: 10 February 2017! Please see the revised script!
import-module ACMESharp
#
# Script parameters
#
$domain = "my.example.com"
$alias = "my.example.com-01"
$iissitename = "my.example.com"
$certname = "my.example.com-$(get-date -format yyyy-MM-dd--HH-mm)"
#
# Environmental variables
#
$PSEmailServer = "localhost"
$LocalEmailAddress = "[email protected]"
$OwnerEmailAddress = "[email protected]"
$pfxfile = "c:\Admin\Certs\$certname.pfx"
$CertificatePassword = "PASSWORD!"
#
# Script setup - should be no need to change things below this point
#
$ErrorActionPreference = "Stop"
$EmailLog = @()
#
# Utility functions
#
function Write-Log {
Write-Host $args[0]
$script:EmailLog += $args[0]
}
Try {
Write-Log "Attempting to renew Let's Encrypt certificate for $domain"
# Generate a certificate
Write-Log "Generating certificate for $alias"
New-ACMECertificate ${alias} -Generate -Alias $certname
# Submit the certificate
Submit-ACMECertificate $certname
# Check the status of the certificate every 6 seconds until we have an answer; fail after a minute
$i = 0
do {
$certinfo = Update-AcmeCertificate $certname
if($certinfo.SerialNumber -ne "") {
Start-Sleep 6
$i++
}
} until($certinfo.SerialNumber -ne "" -or $i -gt 10)
if($i -gt 10) {
Write-Log "We did not receive a completed certificate after 60 seconds"
$Body = $EmailLog | out-string
Send-MailMessage -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Attempting to renew Let's Encrypt certificate for $domain" -Body $Body
Exit
}
# Export Certificate to PFX file
Get-ACMECertificate $certname -ExportPkcs12 $pfxfile -CertificatePassword $CertificatePassword
# Import the certificate to the local machine certificate store
Write-Log "Import pfx certificate $pfxfile"
$certRootStore = "LocalMachine"
$certStore = "My"
$pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$pfx.Import($pfxfile,$CertificatePassword,"Exportable,PersistKeySet,MachineKeySet")
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($certStore,$certRootStore)
$store.Open('ReadWrite')
$store.Add($pfx)
$store.Close()
$certThumbprint = $pfx.Thumbprint
# Bind the certificate to the requested IIS site (all https bindings)
Write-Log "Bind certificate with Thumbprint $certThumbprint"
$obj = get-webconfiguration "//sites/site[@name='$iissitename']"
for($i = 0; $i -lt $obj.bindings.Collection.Length; $i++) {
$binding = $obj.bindings.Collection[$i]
if($binding.protocol -eq "https") {
$method = $binding.Methods["AddSslCertificate"]
$methodInstance = $method.CreateInstance()
$methodInstance.Input.SetAttributeValue("certificateHash", $certThumbprint)
$methodInstance.Input.SetAttributeValue("certificateStoreName", $certStore)
$methodInstance.Execute()
}
}
# Remove expired LetsEncrypt certificates for this domain
Write-Log "Remove old certificates"
$certRootStore = "LocalMachine"
$certStore = "My"
$date = Get-Date
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($certStore,$certRootStore)
$store.Open('ReadWrite')
foreach($cert in $store.Certificates) {
if($cert.Subject -eq "CN=$domain" -And $cert.Issuer.Contains("Let's Encrypt") -And $cert.Thumbprint -ne $certThumbprint) {
Write-Log "Removing certificate $($cert.Thumbprint)"
$store.Remove($cert)
}
}
$store.Close()
# Finished
Write-Log "Finished"
$Body = $EmailLog | out-string
Send-MailMessage -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Let's Encrypt certificate renewed for $domain" -Body $Body
} Catch {
Write-Host $_.Exception
$ErrorMessage = $_.Exception | format-list -force | out-string
$EmailLog += "Let's Encrypt certificate renewal for $domain failed with exception`n$ErrorMessage`r`n`r`n"
$Body = $EmailLog | Out-String
Send-MailMessage -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Let's Encrypt certificate renewal for $domain failed with exception" -Body $Body
Exit
}
I guess I’ll find out in 60 days if the script still works! With many thanks to the users of StackOverflow, and other bloggers, for working code samples which saved me a lot of time reading reference documentation for so many of the different bits of glue here, from exception management, through to sending emails with PowerShell, through to assigning certificates to IIS website bindings, and more…
Update 2 December 2016: The original script had a bug that didn’t affect use with IIS. However, when I tried to use the Let’s Encrypt certificate with another program, in this case MailEnable, I found that the program did not have access to the certificate, even though it seemed it should have had.
When I started the relevant MailEnable service, it would display an error such as:
12/02/16 20:13:00 **** Error 0x8009030d returned by AcquireCredentialsHandle
12/02/16 20:13:00 **** Error creating credentials object for SSL session
12/02/16 20:13:00 Unable to locate or bind to certificate with name "my.example.com"
I checked the permissions and various other factors but only when I did a deep comparison of the details of a working certificate against the one that was failing, using certutil, as suggested in that blog linked above, did I spot the problem. The problem lay in the following CRYPT_KEY_PROV_INFO structure:
CERT_KEY_PROV_INFO_PROP_ID(2):
Key Container = {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
Unique container name: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Provider = Microsoft Enhanced Cryptographic Provider v1.0
ProviderType = 1
Flags = 0
KeySpec = 1 -- AT_KEYEXCHANGE
When I compared this against a working certificate, I saw that the Flags
member had a value of 20
(hex), not 0
. 0x20
turns out to be CRYPT_MACHINE_KEYSET
. Because I was missing the MachineKeySet
flag in the certificate import call, this meant that the key was stored under the Administrator user’s keyset instead of the machine keyset. IIS coped with this, but not MailEnable, which runs under a more restricted user’s credentials.
I have also updated the script to delete all Let’s Encrypt certificates for the domain that have been obsoleted by the new certificate, rather than just certificates that have expired (-And $cert.NotAfter -lt $date
), mostly to avoid the risk of accidentally selecting an old certificate manually when doing configuration via UI.
On my servers, I’ve also added a section to the end of the script that restarts various services that depend on the certificate and will not use a new certificate until after restarting.
Don’t forget to navigate to about:blank when embedding IWebBrowser2
Today I spent several hours trying to figure out why an embedded web browser component (in this case TEmbeddedWB
) in a Delphi test app never received the appropriate IHttpSecurity
and IWindowForBindingUI
QueryService
requests.
I was doing this in order to provide more nuanced handling of self-signed certificates in an intranet context. We all do this, right? Here the term “nuanced” means “Of course I trust self signed certificates on my intranet, don’t you?” Feel free to rant and rave on this. 😉
But no matter what I did, what incantations I tried, or what StackOverflow posts I perused, I was unable to find an answer. Until finally I stumbled on a side comment in a thread from 2010. Igor Tandetnik notes that:
Right after creating the control, navigate it to about:blank. Right after that, navigate it to the page you wanted to go to. It’s a known problem that IServiceProvider doesn’t work for the very first navigation.
And this was something that I kinda knew in the back of my head, but of course had forgotten. Thank you Igor.
This post would not be complete without some splendiferous code. Just for reference, it’s so simple if you don’t blank out and forget about:blank
.
unit InsecureBrowser;
interface
uses
Winapi.Windows,
Winapi.Messages,
Winapi.Urlmon,
Winapi.WinInet,
System.SysUtils,
System.Variants,
System.Classes,
Vcl.Graphics,
Vcl.Controls,
Vcl.Forms,
Vcl.Dialogs,
Vcl.OleCtrls,
Vcl.StdCtrls,
SHDocVw_EWB,
EwbCore,
EmbeddedWB;
type
TInsecureBrowserForm = class(TForm, IHttpSecurity, IWindowForBindingUI)
web: TEmbeddedWB;
cmdGoInsecure: TButton;
procedure webQueryService(Sender: TObject; const [Ref] rsid,
iid: TGUID; var Obj: IInterface);
procedure FormCreate(Sender: TObject);
procedure cmdGoInsecureClick(Sender: TObject);
private
{ IWindowForBindingUI }
function GetWindow(const guidReason: TGUID; out hwnd): HRESULT; stdcall;
{ IHttpSecurity }
function OnSecurityProblem(dwProblem: Cardinal): HRESULT; stdcall;
end;
var
InsecureBrowserForm: TInsecureBrowserForm;
implementation
{$R *.dfm}
function TInsecureBrowserForm.GetWindow(const guidReason: TGUID;
out hwnd): HRESULT;
begin
Result := S_FALSE;
end;
function TInsecureBrowserForm.OnSecurityProblem(dwProblem: Cardinal): HRESULT;
begin
if (dwProblem = ERROR_INTERNET_INVALID_CA) or
(dwProblem = ERROR_INTERNET_SEC_CERT_CN_INVALID)
then Result := S_OK
else Result := E_ABORT;
end;
procedure TInsecureBrowserForm.webQueryService(Sender: TObject;
const [Ref] rsid, iid: TGUID; var Obj: IInterface);
begin
if IsEqualGUID(IID_IWindowForBindingUI, iid) then
Obj := Self as IWindowForBindingUI
else if IsEqualGUID(IID_IHttpSecurity, iid) then
Obj := Self as IHttpSecurity;
end;
procedure TInsecureBrowserForm.cmdGoInsecureClick(Sender: TObject);
begin
web.Navigate('https://evil.intranet.site/');
end;
procedure TInsecureBrowserForm.FormCreate(Sender: TObject);
begin
web.Navigate('about:blank');
end;
end.
My favourite debugging story
This was some years ago, when I was living in Vientiane, the capital of the Lao Peoples’ Democratic Republic. It was 1994 or thereabouts – just prior to the release of Windows 95. I had written a piece of software called “Keyman” which was being increasingly used to type in Lao in Windows 3.1, overloading characters in the 128-255 range of the standard US English character set at the time (I don’t want to be too technical here). Before Unicode.
The owner of a local computer store had had reports of an issue with Keyman from one of their clients in the provincial capital of Savannakhet, about a one hour flight from Vientiane in a small plane. The technical minutiae of the problem escape me now, but it was something to do with a certain set of keystrokes which gave the wrong output in some applications – I think Excel. The report had been communicated over the telephone to the computer shop technical staff, and then translated into English for my benefit – as my Lao was probably not good enough to really get the detail. So as you can imagine, Chinese Whispers is a good way to describe the final report I received.
I tried to diagnose the problem from the description, and tried to reproduce it on my computer, but could not figure it out.
Now it is important to remember that Laos in 1994 was still pretty much unknown to the outside world. There were few tourists; it was (and is) a communist country, at least in principle. Things there didn’t work quite the same as in Australia. There was no Internet access in Laos at that time, telephones were unreliable and the use of modems was technically illegal. This meant that remote diagnosis was only possible by means of telephoned conversations over noisy phone lines, by fax, or with posted letters. The post often took weeks, even in-country.
So after a few days of fruitless telephoning back and forth, the owner of the computer shop suggested I accompany him on a trip down to Savannakhet. (From memory, he was already planning to visit). I was but a callow youth, of 17, and so this was a fantastic opportunity!
When we met up at the airport, the first thing I remember was standing in the security line behind a Lao businessman who caused a bit of a ruckus at the hand luggage screening, because his briefcase had two pistols in it. This seemed a little unusual, even in Laos. After some discussion, his pistols were removed from his hand luggage and given to a guard, who told him he could not take them on the plane because there was not a separate luggage hold. I don’t know what happened to them after that as we were ushered through the security.
When our plane started boarding, a second problem arose. It appeared that the flight had been overbooked, or perhaps they’d substituted a smaller aircraft. The plane we could see was a Xian Y-7, a Chinese clone of a Russian Antonov An-24. The Wikipedia page linked above shows a picture, coincidentally enough of a Lao Aviation plane, perhaps the very plane we were to fly on (they only had 4).
But, as I said, the plane was overbooked, and we ended up in the group of about 10 that didn’t get onto the plane. Pistol-man was in the group that boarded the Y-7, and we didn’t see him again.
Fortunately for us, Lao Aviation had a solution to the problem. They simply rolled a second plane out of the hanger, a Harbin Y-12 this time, and fired it up.
Well, they tried to fire it up. It coughed and spluttered, and lots of black smoke poured out of the engines, but it didn’t start. Boh pen nyang. They pushed it back into the hanger, and rolled out yet another Y-12.
At this point I was feeling a little nervous.
The thired plane coughed and spluttered, poured out lots of black smoke, but it started! After a minute, they shut off the engines and asked us to board.
You can see in the picture below how part of the engine cowling is painted black. You can also see, if you look closely, how there are black smudge marks around that black painted area. Yeah. Smoke. I guess that the smoke mustn’t be a big problem, but it wasn’t inspiring at the time.
https://upload.wikimedia.org/wikipedia/commons/4/4b/Lao_Aviation_Harbin_Y-12_Sibille-1.jpg
Image © 2000 Regis Sibille, used under CCSA. (Another picture: http://www.airliners.net/photo/Iran—Revolutionary/Harbin-Y12-II/1503896/L/)
We rolled out and took off moments after the larger first plane. For a while, we could see the larger plane ahead and slightly above us – I don’t know why they didn’t go straight up to cruising altitude as the Y-7 is a lot faster than the Y-12. But eventually the Y-7 was out of sight. The scenery was in places spectacular. As I recall, the plane stayed in Lao airspace for the whole flight, despite this making the flight significantly longer.
Arriving in Savannakhet, we first travelled to the house of a friend of the computer store owner. This man happened to be one of the richest men in southern Laos. He had a beautiful house, filled with beautifully carved tables, paintings and collected antiques. After a brief meeting there, we were escorted by this man to a café in the city for a coffee. Well, some of us drank coffee; I didn’t. I was but 17 and at that age drank far too much Pepsi.
At this sidewalk café, an interesting encounter occurred, which has stuck with me. A street sweeper stopped and ordered a drink, and sat at the same table as this very wealthy man, and they struck up conversation. For some time, all at the table talked. The friendly interactions between two very different social classes was remarkable to me at the time – especially coming from Thailand where the social strata were clearly delineated.
Finally, social requirements met, we made our way to the computer with the problem. It was about 3 or 4 flights up dusty stairs in one of the tallest buildings in the city. There was a lift, but use of it was definitely not recommended. The problem was demonstrated to me, and I was able to observe there what had flummoxed me from afar. After just a few minutes, I realised what the problem was, and had enough information to fix it. I didn’t have my laptop with me so I had to write down some notes – and then we left them, a little sad that we couldn’t fix the problem immediately.
What was the problem? I actually don’t remember the detail. I just remember how we got there and back again!
We took a ferry across the river to Thailand, and took a bus to Nakhon Phanom. There were two reasons: first, my friend the computer shop owner wanted to visit some relatives there, and second, the roads in Laos were at the time in such poor condition that travel on them was best avoided if an alternative was available. Thai roads were busy but generally in excellent condition.
Once in Nakhon Phanom, it was a short motorcycle taxi ride to the relatives’ house by the river. We stopped there for a couple of hours and drank tea with them (yes, even I, Mr Pepsi Boy drank tea).
But when we tootled back to the bus stop, we found our bus had left a few minutes earlier than we had planned!
This was not a big problem. A bystander offered to chase down the bus in his pickup truck. This pickup was sparkling clean, had bright alloy wheels with a thin slick of rubber spray painted on, and a lowered chassis, so much so that the wheels would have been superfluous if they hadn’t been required for their locomotive capability.
It wasn’t this car, but it could well have been its older brother. Now in Thailand, the buses moved fast. Especially once they got out onto the open road. I’ve been on Thai buses doing 150 km/h or more.
So our intrepid young driver started chasing down the bus, flying down the city streets at over double the speed limit, braking hard for corners and easily avoiding the (fortunately) light traffic, and once he got out onto the open road he was eager to show us what his car would do. So here we were, flying down a Thai highway in a stranger’s car doing a ridiculous speed, chasing down a bus driver that didn’t know we existed. I’m pretty sure my parents would not have been pleased.
At the speed we were doing, we caught up to the bus pretty easily. After some vigorous flashing of the headlights, the bus’s left indicator came on, and it slowed and finally stopped. My friend paid the pickup race driver a token of our appreciation, and with slightly wobbly legs we climbed onto the bus, and off we went to Nong Khai.
We arrived in Nong Khai well after midnight, and ended up at a noisy Thai transit hotel, in a room without windows a couple of floors above the nightclub. One bed: a queen bed.
Now, remember, I was a callow 17 year old youth. The idea of sharing a bed with any man was pretty terrifying. But my friend, who I guess was in his 40s at that stage, kindly noted my abject and unwarranted fear, and gave me the whole bed – I think he sat in the chair and dozed. Not fair, I know.
At about 4am the nightclub finally quietened down, and at about 6am we were awoken by the daily noise that accompanies the start of every day – car doors slamming, shouts, trucks reversing. So we gave up on sleep, got up and made our way back across the border to Vientiane.
Upon returning to Vientiane, I fixed the problem in the Keyman code in a few minutes, prepared a new version on a floppy disk, and rode my bicycle over to the computer shop to deliver it. The computer shop sent the disk on to the user in Savannakhet, and as far as I know, that was that.
That’s how debugging used to work in the nearly olden days. None of these fancy remote desktop VPN SSH thingies. It was a lot more fun.
More windbg tricks with Delphi – how to ignore specific exceptions
This is a really quick post, just noting more flexible ways of handling exceptions in WinDBG, for example ignoring EIdConnClosedGracefully
or EAbort
by default.
There are two parts to this:
- Use script files to build complex breakpoint or exception statements. While you could technically embed it all in one line, it becomes increasingly unwieldy. It’s still painful in a script file but slightly less so.
- Use
as
to create aliases for specific memory addresses in the script, making string comparisons much simpler.
To tell WinDbg to use a script file when a Delphi exception occurs:
sxe -c "$$><C:\\scripts\\windbg_delphi_exception.txt" 0EEDFADE
Then, the script file itself:
$$
$$ Report on Delphi exceptions: setup aliases
$$
.block {
as ${/v:exception_cleanup} ad /q ${/v:exception_message}; ad /q ${/v:exception_name}; ad /q ${/v:exception_address}
}
.block {
exception_cleanup
}
.block {
aS /mu ${/v:exception_message} poi(poi(ebp+1c)+4)
aS /ma ${/v:exception_name} poi(poi(poi(ebp+1c))-38)+1
aS /x ${/v:exception_address} poi(ebp+4)
}
$$
$$ Do everything in this block, no matter what the exception is. This way we
$$ get reports on exceptions without interrupting program execution. You may want to
$$ add extra reporting, e.g. stack traces, other variables.
$$
.block {
.echo Delphi exception exception_name at ${exception_address}: exception_message
}
$$
$$ In this block, we add conditionals for exception types we want to ignore, or even
$$ specific addresses or other conditions as you need.
$$
$$ Don't include anything below this block, not even comments, because that breaks the
$$ "gc" command
$$
.block {
.if ($scmp( "${exception_name}", "EIdConnClosedGracefully") == 0) { exception_cleanup; gc }
.elsif ($scmp( "${exception_name}", "EAbort") == 0) { exception_cleanup; gc }
.else {
.echo
exception_cleanup
}
}
Yes, this file has a bit of complexity in it. The WinDbg script interpreter is excessively pernickity. Important things to note are:
- aliases are not expanded until the block is started (hence usage of the aliases is in a separate block to the definition)
- you can’t have any additional statements after a block containing a
gc
ort
or similar command, otherwise the script interpreter has a little hissy fit - the
exception_cleanup
alias is pretty essential to avoid leaving aliases about that then get expanded at the wrong time (leading to premature expansion of aliases from a previous exception). - the
exception_name
alias is not 100% reliable. This is because it is referencing a Delphi short string, which is not null-terminated, leading to garbage characters displayed at the end in some cases. Sadface. There is probably a way to work around this but I haven’t found it yet. - You’ll see sometimes I use the
${alias_name}
expansion and other times I use justalias_name
. Use${alias_name}
where non-whitespace characters may be immediately after the alias, as they will be interpreted as part of the alias. - The
${/v:alias_name}
version prevents expansion of the alias, which is useful for referring to the name of the alias if it is already defined, for example to delete it.
Useful knowledge on WinDbg scripts from other sources:
- http://blogs.msdn.com/b/debuggingtoolbox/archive/tags/windbg+scripts/default.aspx
- http://www.msuiche.net/2007/08/03/first-steps-with-windbg-scripting-memento/
- WinDbg reference
Update 12 Oct 2020: Fixed up final block to use .elsif
and .else
to avoid script execution past gc
Windbg and Delphi – a quick reference
This is a list of my blog posts on using WinDBG with Delphi apps, mostly for my reference. Internally, I use a version of tds2dbg with some private modifications, but you should have reasonable results with the public version.
WinDBG and Delphi exceptions in x64
Locating Delphi exceptions in a live session or dump using WinDbg
Debugging a stalled Delphi process with Windbg and memory searches
Finding class instances in a Delphi process using WinDbg
More windbg tricks with Delphi – how to ignore specific exceptions (Jan 2016)
I also have some other posts that talk about WinDBG and/or Delphi which can be helpful for illustrative purposes:
Another partial malware diagnosis
Detecting the Citadel Trojan with an Application Failure
When characters go astray: diagnosing missing characters when printing with IE9
WaitForSingleObject. Why you should never use it.
IE11, Windbg, JavaScript = joy
Notes on a Khmer mobile keyboard for Keyman
While other Khmer keyboards exist for iOS and Android, I wanted to try playing with one myself, given I am currently learning Khmer. Creating a keyboard layout is a great way to rapidly become very familiar with a script!
Keyman Developer makes it easy to play around with the layout of a keyboard and rapidly iterate the design. I was able to turn the original desktop layout into a mobile-optimised layout in under two hours, using only the visual editor.
The base keyboard comes from the khmer10 Keyman keyboard created by Andrew Cunningham, which is based on the NiDA Khmer keyboard layout. This is a desktop keyboard, which follows a phonetic-style input, with letters placed as far as possible on keys with a similar sound in the English alphabet. For example, ក is on the [k] key.
Design principles
I had a number of goals I wanted to achieve with this keyboard.
A design good for a language learner
I wanted the design to help me remember the script, the sounds and related letters. This may not be optimal for a person fluent in the language, but for me, the NiDA layout’s phonetic-style layout was a good starting point.
Reduce the number of keys …
As the original design is for a desktop keyboard, there are too many keys to fit on a normal mobile layout. As a mobile keyboard should ideally have no more than ten keys in a row, I started with reducing that as one goal.
… But don’t lose all relationship with the desktop keyboard
I tried to avoid moving keys around on the keyboard, or removing keys other than the ones described below. This way, once I do become familiar with the mobile layout, it is not a difficult transition to using the NiDA layout on a desktop computer.
Move symbols and numbers off base layers
A number of non-alphabetic symbols are placed in a seemingly haphazard fashion on both the unshifted and Shift layers. The position of these symbols is probably pragmatic – the keys were available and not used for any other purpose. On a touch layout, we don’t need to maintain this because we can have as many or as few keys as we wish.
I moved all non-alphabetic symbols and numbers to a numeric/symbol layer.
Relate sub consonants to base consonants
ក្ក ក្ខ ក្គ ក្ឃ ក្ង ក្ច ក្ឆ ក្ជ ក្ឈ ក្ញ ក្ដ ក្ឋ ក្ឌ ក្ឍ ក្ណ ក្ត ក្ថ ក្ទ ក្ធ ក្ន ក្ប ក្ផ ក្ព ក្ភ ក្ម ក្យ ក្រ ក្ល ក្វ ក្ឝ ក្ឞ ក្ស ក្ហ ក្ឡ ក្អ
The original keyboard uses the key [j] as a prefix to create a sub consonant, by emitting the Unicode U+17D2 sub consonant marker character. This is a little obscure, and meant that the shapes of sub consonants were never visible on the keyboard.
I wanted to hide this encoding-based knowledge. I have added the sub consonants to the keyboard under a longpress menu for each base consonant, and added a rule to delete both the consonant and the prefix U+17D2 marker together when backspace is pressed. Thus, the average keyboard user need never know about the existence of U+17D2.
Independent vowels
As these are less frequently typed than the dependent vowels, they really didn’t need their own key on the keyboard. Again, from a language learner point of view, placing the independent vowels under the related dependent vowel symbol, as longpress menus, helps me to learn the shapes more rapidly. It also means that I don’t confuse the independent vowel shapes with similarly shaped consonants on the layout.
Adding missing characters
The Khmer digits were already on the keyboard, but the Hindu-Arabic numerals were not. I added these as long-press under the Khmer digit keys on the numeric/symbol layer.
Things that are not yet right
This keyboard is nowhere near finished, but it’s now at a good point to start using it, before making further optimisations. After using it for a while, I will have a clearer understanding of what is uncomfortable or awkward to use, and will also probably have better ideas of how to resolve the issues.
Some of the issues I already know about are:
- Still too many keys per row: some rows have 12 keys. I should try to reduce this to 10 keys.
- There are vowel combinations that may or may not be necessary. These do not render correctly on most operating systems on the keyboard, but do when used in writing.
- The backspace key should be either on the top row, or on the third row. It’s only on the second row at present because that was where there was space!
- The keyboard is missing a number of symbols that I probably won’t be using for now, but should be available for a complete solution, such as the Lek Attak numeric divination symbols. Some archaic letters are also not yet present.
- The Khmer OS fonts do not use the dotted circle for isolated diacritics, which makes them hard to read on the keyboard.
- The layers are not precisely the same width, which leads to slightly disconcerting movement on layer switches.
- The shift key on the shift layer is missing its icon.
I just realised the independent vowels are currently missing – I need to re-add those soon!
Screenshot on Android:
Issues with using the keyboard include:
iOS has rendering bugs with Khmer, for example ខ្ញុំ on iOS overlaps the two subscript marks.
On Android, there are different rendering issues, for example the font is too large for the keys by default (showing the original keyboard as I don’t have an Android device handy for an up-to-date screenshot).
Get the keyboard + source
Despite these limitations, the keyboard should be usable for typing most Khmer text today, as least on iOS. It certainly covers most of my needs as a language learner today. As such, I’ve made it available for install into Keyman for iPhone and iPad or Keyman for Android.
First, install the app from the appropriate link above. Then click the link below to add the keyboard to your device. I haven’t included a font in the keyboard, so you’ll need a device which already supports rendering the script.
Install the keyboard (version 1.1)
Install the keyboard (version 1.2)
New! Install the Khmer Angkor beta keyboard (version 1.0) – by Makara Sok
I welcome any feedback, of course!
The source of the keyboard is available on GitHub at https://github.com/mcdurdin/experimental-keyboards.