B4J Library ABMaterial - an exploration of cookies

PREAMBLE

A useful primer:

https://clearcode.cc/blog/browsers-first-third-party-cookies/

I apologise in advance for the scale of this but it is forced by the need to explore cookie behaviour under various server settings - local/remote secure/unsecure selfsigned/certificate authority signed.

As part my effort to get on top of ABMaterial for a webapp I am planning I need to master cookies for remembering logins etc.

This is not a code snippet or a tutorial or a library - but as it can be installed and run standalone I will claim it as a library.

You may like to have a look at A TOUR THROUGH THE COOKIE JAVASCRIPT before deciding if it interests you enough to go through the (quite heavy) installation process.

It is based on several elements already accessible in the forums:

1. Alain's introductory ABM template - used as a base for this exercise:

https://www.b4x.com/android/forum/t...-absolute-beginners-update-2024-05-02.117237/

2. This reference describes how to make a self-signed keystore - sufficient for testing, putting server behind a https://localhost:...

https://eclipse.dev/jetty/documenta...rating-key-pairs-and-certificates-JDK-keytool

3. Erel's B4J library for LetsEncrypt SSL certificates - incorporated because self-signed keystores are not adequate in a production environment (browser in use will throw up ugly "not safe" warnings):

https://www.b4x.com/android/forum/threads/server-letsencrypt-ssl-certificates.159285/#content

You should note that I have adopted an absolutely minimalistic approach to presenting this - I have stripped out everything that does not have a direct bearing on creating/inspecting/deleting cookies in ABMaterial.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
INSTALLATION

Step 1


Install ABMMini on your D: drive - see:

https://www.b4x.com/android/forum/t...-absolute-beginners-update-2024-05-17.117237/

Make sure you download ABMMini240517.zip (note the 240517).

It is assumed that you end up with a folder [D:\ABMMini] containing:

Library folder
Template folder
LICENSE.TXT
README.txt

and these additions to your B4J [Additional Libraries] folder:

ABMaterial.jar
...
thumbnailator-0.4.8.jar

Step 2

Copy [D:\ABMMini\Template] to [D:\ABMMini\Template - Copy]

Step 3

Rename [D:\ABMMini\Template - Copy] as [D:\ABMMini\Cookie_4_ABM]

Step 4

Delete [D:\ABMMini\Cookie_4_ABM\Template.b4j]
Delete [D:\ABMMini\Cookie_4_ABM\Template.b4j.meta
Delete [D:\ABMMini\Cookie_4_ABM\ABMPageTemplate.bas]

Step 5

Download the zip file [Cookie_4_ABM.zip] unzip it and copy the folder Cookie_4_ABM into [D:\ABMMini].

[D:\ABMMini\Cookie_4_ABM] should now contain:

Files folder
Objects folder
ABMCacheV3.bas
Cookie_4_ABM.b4j
Cookie_4_ABM.b4j.meta
Cookie_4_ABM_Object.bas
Cookie_4_ABM_Page.bas
LetsEncrypt.bas

and the Objects folder should have a file:
[D:\ABMMini\Cookie_4_ABM\Objects\www\.well-known\acme-challenge\dummy.txt]
 

Attachments

  • Cookie_4_ABM.zip
    12.2 KB · Views: 98
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
RUN AS LOCALHOST

Before running any new configuration always clear browsing data on the client device.


Launch the B4J project [D:\ABMMini\Cookie_4_ABM\Cookie_4_ABM.b4j] in your local PC

Ensure this statement in Main/AppStart:

'Server.PortSSL = 51043

is commented out - forcing the server to listen on port 51042 as http.

Just run in Debug mode then, on a device on the same local network, launch a browser and enter the URL [localhost:51042/Cookie_4_ABM]

If this does not work you may need to find the local IP address of the PC (something like 192.168.1.109) then use a URL like [192.168.1.109:51042/Cookie_4_ABM]

Tap on the message "Tap anywhere here to check..." and look at the server's log area.

For Android, iOS and Windows 10 Chrome/Edge/Firefox - only cookies of the form "Cookie_Android/iOS_unsecure..." are created - i.e. secure cookies are not created - this is defined behaviour.

Tap on the message "Tap anywhere here to delete..." to delete all cookies.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
RUN WITH SELF-SIGNED SSL ON LOCALHOST

Before running any new configuration always clear browsing data on the client device.


The following is based on:

https://eclipse.dev/jetty/documenta...rating-key-pairs-and-certificates-JDK-keytool

which describes how to make a self-signed keystore - sufficient for testing, putting server behind a https://localhost:...

On the local PC that is to run server (i.e. PC that will run Cookie_4_ABM B4J project):

1. Right click Windows icon (bottom left)
2. Click [Command Prompt (Admin)]
3. In [Administrator Command Prompt] window conduct a dialog similar to:

C:\WINDOWS\system32>cd "C:\java\jdk-14.0.1\bin"

C:\java\jdk-14.0.1\bin>keytool -keystore keystoreselfsign -genkey -keyalg RSA

Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]:
xxxx
What is the name of your organizational unit?
[Unknown]:
xxxx
What is the name of your organization?
[Unknown]:
xxxx
What is the name of your City Or Locality?
[Unknown]:
xxxx
What is the name of your State Or Province?
[Unknown]:
xxxx
What is the two-letter country code For this unit?
[Unknown]:
xx
Is CN=xxxx, OU=xxxx, O=xxxx, L=xxxx, ST=xxxx, C=xx correct?
[no]:
yes
Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 90 days
for: CN=xxxx, OU=xxxx, O=xxxx, L=xxxx, ST=xxxx, C=xx

4.
This will result in a file [C:\java\jdk-14.0.1\bin\keystoreselfsign, copy it to the Cookie_4_ABM B4J project \Objects folder

5. Launch the B4J project [D:\ABMMini\Cookie_4_ABM\Cookie_4_ABM.b4j]

6. In the Main module change "yourpassword" to the password you used to generate keystoreselfsign.

7. In the Main/AppStart procedure of the Main module make sure there is a statement that reads:

CASignedKeystore = False

forcing the server to use a self signed SSL.

8. Ensure this statement in Main/AppStart:

Server.PortSSL = 51043

is not commented out - forcing the server to listen on port 51043 as https.

9. Run the B4J project in Debug mode - you should now have a running ABMaterial server with cookie capabilities.

10. On a device on the same local network, launch a browser and enter the URL [https://localhost:51043/Cookie_4_ABM]

11. If 10. does not work you may need to find the local IP address of the PC (something like 192.168.1.109) then use a URL like [https://192.168.1.109:51043/Cookie_4_ABM]

12. Tap on the message "Tap anywhere here to check..." and look at the server's log area.

For Android, iOS and Windows 10 Chrome/Edge/Firefox - cookies of the form "Cookie_Android/iOS_unsecure..." and "Cookie_Android/iOS_secure..." are created - this is defined behaviour.

13. Tap on the message "Tap anywhere here to delete..." to delete all cookies.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
RUN WITH CERTIFICATE AUTHORITY (LetsEncrypt) SIGNED SSL

Before running any new configuration always clear browsing data on the client device.


Self-signed SSLs are fine for testing but are not adequate in a production environment - browser in use will throw up ugly "not safe" warnings.

The following assumes you are installing on a remote PC which is an AWS EC2 Windows instance (server edition) but you should fairly readily be able to translate it to other remote environments.

It is also assumed you have registered a domain (yourdomain.com) via the AWS Route 53 service that points to the AWS EC2 Windows instance.

The following is based in part on:

https://www.b4x.com/android/forum/threads/server-letsencrypt-ssl-certificates.159285/#content

which describes how to generate and maintain Certificate Authority signed SSLs via LetsEncrypt.

Step 1

Install CertBot - if you use the above URL you might get quite confused and you should also be aware of:

https://www.b4x.com/android/forum/threads/certbot-discontinuing-windows-beta-support-in-2024.160254/

At time of writing you need to get to:

https://certbot.eff.org/instructions?ws=other&os=windows

then scroll down to a URL:

https://github.com/certbot/certbot/...d/certbot-beta-installer-win_amd64_signed.exe

which will download [certbot-beta-installer-win_amd64_signed.exe]

Locate this file, launch it and follow breadcrumbs to install [C:\Program Files\Certbot\bin\certbot.exe]

Step 2

Install Git which includes OpenSSL:

https://gitforwindows.org/

This is straightforward, you download [Git-2.44.0-64-bit.exe]

Locate this file, launch it and follow breadcrumbs to install [C:\Program Files\Git\usr\bin\openssl.exe]

Step 3

When it is creating an SSL certificate, LetsEncrypt conducts a "challenge" dialog with PC on http (port 80) to confirm ownership of the domain.

To accommodate this there must be a server running on the PC that is listening on this port.

Ensure your AWS EC2 Windows instance has a security group rule http for port 80.

As you are using a server edition of Windows you can use IIS.

Step 4

Install IIS.

See:


Skip [Section 1 Configuring And Launching EC2 Instance]
Use [Section 2 Installation of web Server IIS on EC2 Instance]
Skip [Section 3 Creating webpage And hosting it on EC2 Instance]
Skip [Section 4 Deleting Instance]

Step 5

In the PC's Windows taskbar search box, search For "IIS" then click [Internet Information Services (IIS) Manager]

In left hand panel right click only entry (something like [EC2AMAZ-UJ82A3M...]) > [Add Website]
Site name: [.well-known] <<<<<<< without [ ], note the leading full stop (.) in [.well-known]
Physical path: navigate to [D:\ABMMini\Cookie_4_ABM\Objects\www]
[Connect as...] > [Application user (pass-through authentication)] > [OK]
[OK]

Step 6

Following file should already exist, but if it doesn't...

Set up a test by using Notepad to create a file [D:\ABMMini\Cookie_4_ABM\Objects\www\.well-known\acme-challenge\dummy.txt] containing some junk (e.g. "dummy junk")

Step 7

On another PC, test IIS works by launching a browser and using a URL of:

http://yourdomain.com/.well-known/acme-challenge/dummy.txt
which should display "dummy junk"

Step 8

Certbot must be run with administrative privileges, the easiest way to do this when running the B4J project in Debug mode is to give the B4J.exe administrative privileges:

1. Locate B4J.exe (for example: C:\Program Files\Anywhere Software\B4J\B4J.exe)
2. Right click it > [Properties] > [Compatibility] > [Run this program as an administrator] > [Apply] >[OK]
3. Do not forget to unwind this when finished.

Step 9

1.
Launch the B4J project [D:\ABMMini\Cookie_4_ABM\Cookie_4_ABM.b4j]

2. In the Main module change "yourpassword" to the password you wish to use.

3. In the Main module change "yourdomain.com" to the name of the domain you have set up via the AWS Route 53 service that points to the AWS EC2 Windows instance.

4. In the AppStart procedure of the Main module make sure there is a statement that reads:

CASignedKeystore = True

5. Run the B4J project in Debug mode - the first time you run it it should generate the file keystore.jks in [D:\ABMMini\Cookie_4_ABM\Cookie_4_ABM\Objects] folder and then hang.

6. Again, run the B4J project in Debug mode - you should now have a running ABMaterial server with cookie capabilities

7. On a device, launch a browser and enter the URL [https://yourdomain.com:51043/Cookie_4_ABM], where yourdomain.com is as per 3.

8.
Tap on the message "Tap anywhere here to check..." and look at the server's log area.

For Android, iOS and Windows 10 Chrome/Edge/Firefox - cookies of the form "Cookie_Android/iOS_unsecure..." and "Cookie_Android/iOS_secure..." are created - this is defined behaviour.

9. Tap on the message "Tap anywhere here to delete..." to delete all cookies.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
A TOUR THROUGH THE COOKIE JAVASCRIPT

The guts of this exercise is the javascript in Cookie_4_ABM_Object module which I wrote - without any real knowledge of its eccentricities, and garble brackets etc - by using Microsoft Copilot:

B4X:
function handlecookiemessage1() {

    let date = new Date();
    date.setDate(date.getDate() + 30);
    let day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getUTCDay()];
    let dateNum = String(date.getUTCDate()).padStart(2, '0');
    let month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getUTCMonth()];
    let year = date.getUTCFullYear();
    let hours = String(date.getUTCHours()).padStart(2, '0');
    let minutes = String(date.getUTCMinutes()).padStart(2, '0');
    let seconds = String(date.getUTCSeconds()).padStart(2, '0');
    let formattedDate = day + ', ' + dateNum + '-' + month + '-' + year + ' ' + hours + ':' + minutes + ':' + seconds + ' UTC';
    let ticks = date.getTime();
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': formattedDate});

    var userAgent = window.navigator.userAgent;
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': userAgent});
    if (userAgent.match(/iPad|iPhone/i)) {
        // User is on iPad or iPhone
        try {
            document.cookie = 'cookietest=1';
            const isCookieEnabled = document.cookie.indexOf('cookietest=') !== -1;
            document.cookie = 'cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
            if (isCookieEnabled) {
                b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "iOS, 1st party cookies enabled"});
            } else {
                b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "iOS, 1st party cookies blocked"});
                return;
            }
        } catch (e) {
            b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "iOS false"});
            return;
        }
        document.cookie = 'Cookie_iOS_secure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate + '; Secure';
        document.cookie = 'Cookie_iOS_unsecure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate;
    } else if (userAgent.match(/Android/i)) {
        // User is on Android
        const isCookieEnabled = navigator.cookieEnabled;
        if (isCookieEnabled) {
            b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Android, 1st party cookies enabled"});
        } else {
            b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Android, 1st party cookies blocked"});
            return;
        }
        document.cookie = 'Cookie_Android_secure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate + '; Secure';
        document.cookie = 'Cookie_Android_unsecure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate;
    } else {
        b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Neither iPhone nor Android"});
        document.cookie = 'Cookie_Neither_secure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate + '; Secure';
        document.cookie = 'Cookie_Neither_unsecure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate;
    }
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': document.cookie});
}

// Run handlecookiemessage1 once when JavaScript is defined
handlecookiemessage1();

// Run handlecookiemessage1 when cookie_message1 is clicked
var cookie_message1 = document.getElementById('cookie_message1');
cookie_message1.addEventListener('click', handlecookiemessage1); 

var cookie_message2 = document.getElementById('cookie_message2');
cookie_message2.addEventListener('click', function() {

    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': " "});
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Cookies to be deleted:"});
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': document.cookie});
    var cookies = document.cookie.split(";");
    for (var i = 0; i < cookies.length; i++) {
        var cookie = cookies[i];
        var eqPos = cookie.indexOf("=");
        var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
        document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
    }
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Cookies deleted"});
  
});

The first function - handlecookiemessage1:

B4X:
function handlecookiemessage1() {

    let date = new Date();
    date.setDate(date.getDate() + 30);
    let day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getUTCDay()];
    let dateNum = String(date.getUTCDate()).padStart(2, '0');
    let month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getUTCMonth()];
    let year = date.getUTCFullYear();
    let hours = String(date.getUTCHours()).padStart(2, '0');
    let minutes = String(date.getUTCMinutes()).padStart(2, '0');
    let seconds = String(date.getUTCSeconds()).padStart(2, '0');
    let formattedDate = day + ', ' + dateNum + '-' + month + '-' + year + ' ' + hours + ':' + minutes + ':' + seconds + ' UTC';
    let ticks = date.getTime();
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': formattedDate});

    var userAgent = window.navigator.userAgent;
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': userAgent});
    if (userAgent.match(/iPad|iPhone/i)) {
        // User is on iPad or iPhone
        try {
            document.cookie = 'cookietest=1';
            const isCookieEnabled = document.cookie.indexOf('cookietest=') !== -1;
            document.cookie = 'cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
            if (isCookieEnabled) {
                b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "iOS, 1st party cookies enabled"});
            } else {
                b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "iOS, 1st party cookies blocked"});
                return;
            }
        } catch (e) {
            b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "iOS false"});
            return;
        }
        document.cookie = 'Cookie_iOS_secure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate + '; Secure';
        document.cookie = 'Cookie_iOS_unsecure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate;
    } else if (userAgent.match(/Android/i)) {
        // User is on Android
        const isCookieEnabled = navigator.cookieEnabled;
        if (isCookieEnabled) {
            b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Android, 1st party cookies enabled"});
        } else {
            b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Android, 1st party cookies blocked"});
            return;
        }
        document.cookie = 'Cookie_Android_secure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate + '; Secure';
        document.cookie = 'Cookie_Android_unsecure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate;
    } else {
        b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Neither iPhone nor Android"});
        document.cookie = 'Cookie_Neither_secure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate + '; Secure';
        document.cookie = 'Cookie_Neither_unsecure' + ticks + '=abcxyz/expiry=' + formattedDate + '; expires=' + formattedDate;
    }
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': document.cookie});
}

Sets a formatted date for use in the "expiry" field of created cookies - and saves a tick form of it also.

Determines what environment the browser is in (iPad/iPhone, Android and Neither (typically a Windows browser (Chrome, Edge, Firefox...)) - and reports it back to the server.

Determines if 1st party cookies are blocked or not and reports it back to the server. AFAIK only IOS Safari is capable of blocking 1st party cookies - typical!!!

Creates both a secure and unsecure cookie if possible - with some playing around with expiry stuff - note the actual "expiry" field is not returned when cookies are reported on so if you want this info you need to append it separately as shown here.

Reports back to the server all cookies in existence.

-------------------------------------------------

This just fires handlecookiemessage1 as the javascript is loaded.

B4X:
// Run handlecookiemessage1 once when JavaScript is defined
handlecookiemessage1();

-------------------------------------------------

This just sets up for handlecookiemessage1 to be fired when the message "Tap anywhere here to check if 1st party cookies are enabled..." is clicked.

B4X:
// Run handlecookiemessage1 when cookie_message1 is clicked
var cookie_message1 = document.getElementById('cookie_message1');
cookie_message1.addEventListener('click', handlecookiemessage1);

-------------------------------------------------

And finally this sets up for all cookies to be reported then deleted when the message "Tap anywhere here to delete any and all cookies" is clicked.

B4X:
var cookie_message2 = document.getElementById('cookie_message2');
cookie_message2.addEventListener('click', function() {

    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': " "});
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Cookies to be deleted:"});
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': document.cookie});
    var cookies = document.cookie.split(";");
    for (var i = 0; i < cookies.length; i++) {
        var cookie = cookies[i];
        var eqPos = cookie.indexOf("=");
        var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
        document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
    }
    b4j_raiseEvent('${Passed_Instance_Name}_Report', {'value': "Cookies deleted"});
   
});
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
BEHAVIOURS DEPENDING ON HOW RUN

RUN AS LOCALHOST (http)


For all tested browsers - only cookies of the form "Cookie_Android/iOS_unsecure..." are created - i.e. secure cookies are not created - this is defined behaviour.

RUN WITH SELF-SIGNED SSL ON LOCALHOST (https)

For all tested browsers- cookies of the form "Cookie_Android/iOS_unsecure..." and "Cookie_Android/iOS_secure..." are created - this is defined behaviour.

RUN WITH CERTIFICATE AUTHORITY (LetsEncrypt) SIGNED SSL (https)

For all tested browsers- cookies of the form "Cookie_Android/iOS_unsecure..." and "Cookie_Android/iOS_secure..." are created - this is defined behaviour.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
CORRECTIONS AND UPDATES LOG
 

JackKirk

Well-Known Member
Licensed User
Longtime User
KNOWN BUGS
 

JackKirk

Well-Known Member
Licensed User
Longtime User
BROWSER TESTING TO DATE

-Successful-

Chrome mobile 123.0.6312.99 on Pixel 3 running Android 12
Chrome mobile 123.0.6312.99 on vivo running Android 10
Safari mobile on iPhone 7 running iOS 15.8.2
Safari mobile on iPhone 12 running iOS 17.3.1
Desktop Chrome 123.0.6312.59 on Windows 10 build 19045.4170
Desktop Edge 123.0.2420.81 on Windows 10 build 19045.4170
Desktop Firefox 124.0.2 on Windows 10 build 19045.4170

If you test it on a browser not in the above lists please tell me and I will update this list - please include device, browser and version, OS and version.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
ADDITIONAL NOTES
 

JackKirk

Well-Known Member
Licensed User
Longtime User
RESERVED
 
Top