Today I learned: How to forward a port using ufw

For forwarding a port using ufw it’s necessary to operate on iptables rules defined in /etc/ufw/before.rules

Add the following rules to before.rules

*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 11.11.11.11:80
-A POSTROUTING ! -o lo -j MASQUERADE
# don't delete the 'COMMIT' line or these rules won't be processed
COMMIT

-A POSTROUTING ! -o lo -j MASQUERADE allows the traffic to be discerned as though not originating from a nat. ! -o lo prevents the lo interface to be masqueraded and break DNS resolving.

Today I learned: Fedora and HAProxy SELinux

I encountered a problem with HAProxy and SELinux. It seemed like the server wasn’t found and I was getting 503. On inspecting SELinux logs I realized certain changes must be made when working on a SELinux environment. I had no issues on other systems except RedHat family OSes.

  • haproxy_connect_any is the name of the SELinux boolean variable.
  • -P specifies that the change should be persistent across reboots.
  • 1 is the value being set, which means enabling the permission or behavior associated with the haproxy_connect_any boolean.

This configuration was necessary for me when using Certbot with HAProxy on Fedora. I was able to use them separately, and yet I couldn’t have the Certbot server defined in HAProxy on a different port(e.g. port 380) for a more complex configuration.

setsebool -P haproxy_connect_any 1

This is necessary because HAProxy requires more permissions.

sesearch -A | grep haproxy_connect_any
allow haproxy_t packet_type:packet recv; [ haproxy_connect_any ]:True
allow haproxy_t packet_type:packet send; [ haproxy_connect_any ]:True
allow haproxy_t port_type:tcp_socket name_bind; [ haproxy_connect_any ]:True
allow haproxy_t port_type:tcp_socket name_connect; [ haproxy_connect_any ]:True
allow haproxy_t port_type:tcp_socket { recv_msg send_msg }; [ haproxy_connect_any ]:True

Today I learned: How to add port 80 and 443 to Firewalld

Firstly we need to find the active zones of Firewalld

sudo firewall-cmd --get-active-zones

Then we execute the commands to add the services permanently then we reload:

sudo firewall-cmd --zone=public --add-service=http --permanent
sudo firewall-cmd --zone=public --add-service=https --permanent
sudo firewall-cmd --reload

We may also do it in such a manner:

sudo firewall-cmd --zone=public --add-port=80/tcp --permanent
sudo firewall-cmd --zone=public --add-port=443/tcp --permanent
sudo firewall-cmd --reload

Today I learned: SSH permissions on Linux and ssh-agent

To ensure proper SSH functionality it’s necessary to setup proper permissions. The .ssh directory in home must be 700. While authorized_keys and private keys should be 600. Public keys should be 644.

To start ssh-agent you must execute:

eval $(ssh-agent -s)

To add a key to the agent use:

ssh-add path/to/key.

Today I learned: Firewalld Masquerade & Docker

While using Docker on Fedora 34 I encountered an issue where my containers would not communicate properly. So I had a Docker Compose configuration with an internal network and a default bridge network. While I could ping the various servers from inside the containers, connections to various ports were failing. So ICMP traffic was up and running while the rest didn’t work. That is an obvious firewall configuration problem. Fedora 34 uses Firewalld as its firewall, while underneath it’s really iptables. Solving this particular problem requires enabling masquerading. First we would need to find out the active firewall zones:

sudo firewall-cmd --get-active-zones

FedoraServer

interfaces: eth0

docker

interfaces: br-0eb49bac0303 docker0

Bash response

In my case I am interested in the FedoraServer zone:

sudo firewall-cmd --zone=FedoraServer --add-masquerade --permanent
sudo firewall-cmd --reload

Then I would recommend restarting the Docker daemon:

sudo systemctl restart docker

This should do the trick!

Today I learned: Activating Varnish cache for Magento 2

Today I will do my best to explain how to configure Varnish cache for Magento 2.4.2.

I used the official Docker image for Varnish and built a simple Dockerfile along with a Magento recommended ENV value:

FROM varnish:6.6.0-1
COPY default.vcl /etc/varnish
ENV VARNISH_SIZE=2G

The default.vcl file is provided below:

vcl 4.0;

backend default {
  .host = "webserver";
  .port = "80";
  .first_byte_timeout = 600s;
}

As you can see it’s a very simple configuration for bootstrapping the service. The host in this case is the DNS name of your Magento server(e.g. Apache or NGINX). For this experiment we are “baking” the config into the Dockerfile. Once we have our Magento up and running we may connect to Varnish and access our site.

But to actually use Varnish we will have to configure it in the admin as well as adjust the default.vcl configuration file. So we have to log in into the admin panel. Then access Stores > Settings Configuration > Advanced > System > Full Page Cache. Once you access that page you must select Varnish Cache as the Caching Application. Then in Varnish Configuration you must set your server host and port. Afterwards press Export VCL for Varnish 6. Makes sure to save your changes. Once you download the vcl file we will have to adjust it.

First of all I should mention that starting with Magento 2.4.2 the webroot is supposed to be configured to be the pub folder. This is relevant for configuring Varnish because the base configuration generated by the admin panel includes the pub folder in various paths. The problem is since the content is served from pub, Varnish has no clue whatsoever what pub is. From its point of view it sees only the root. So we must remove all pub references from the file otherwise you will get a 503 error because it can’t access the health_check.php file. So just remove the pub references from the config. For example instead of:

backend default {
    .host = "webserver";
    .port = "80";
    .first_byte_timeout = 600s;
    .probe = {
        .url = "/pub/health_check.php";
        .timeout = 2s;
        .interval = 5s;
        .window = 10;
        .threshold = 5;
   }
}

Write in the following manner:

backend default {
    .host = "webserver";
    .port = "80";
    .first_byte_timeout = 600s;
    .probe = {
        .url = "/health_check.php";
        .timeout = 2s;
        .interval = 5s;
        .window = 10;
        .threshold = 5;
   }
}

Once you do that you should restart Varnish and the site should be available. Thanks for reading.

Today I learned: Adding CKEditor5 to Voyager Laravel extension

Today I will show you how to integrate CKEditor5 in a custom Voyager form field. There are 3 basic steps we must follow to even have the new form field appear in the Voyager BREAD type selection.

  1. Create a controller handler which we will save in app\FormFields\CKEditorHandler.php
<?php

namespace App\FormFields;

use TCG\Voyager\FormFields\AbstractHandler;

class CKEditorHandler extends AbstractHandler
{
    protected $codename = "ckeditor";

    public function createContent($row, $dataType, $dataTypeContent, $options)
    {
        return view('formfields.ckeditor_formfield', compact('row', 'dataType', 'dataTypeContent', 'options'));
    }
}

2. Register the controller in app\Providers\AppServicesProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use TCG\Voyager\Facades\Voyager;
use App\FormFields\CKEditorHandler;
use App\FormFields\GrapesJsHandler;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        Voyager::addFormField(CKEditorHandler::class);
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

3. Create a form field blade file in resources\views\formfields\ckeditor_formfield.blade

<br>
<input type="button" onclick="window.toggleEditor()" value="Edit">
<textarea style="display: none" class="form-control" name="{{ $row->field }}" id="myckeditor">
    {{ old($row->field, $dataTypeContent->{$row->field} ?? '') }}
</textarea>

@push('javascript')
    <script src="{{asset('assets/js/ckeditor5/build/ckeditor.js')}}"></script>
    <script>
        function hideTranslationButtons() {
            function toggleButton(elem) {
                if (elem.style.display === "none") {
                    elem.style.display = "block";
                } else {
                    elem.style.display = "none";
                }
            }
            let buttons = document.getElementsByName("i18n_selector");
            for (let el of buttons) {
                toggleButton(el.parentElement);
            }
        }

        function getElement() {
            return document.getElementById("myckeditor");
        }

        function startEditor() {
            ClassicEditor
                .create(getElement(), {
                    toolbar: {
                        items: [
                            'heading',
                            '|',
                            'bold',
                            'italic',
                            'link',
                            'bulletedList',
                            'numberedList',
                            '|',
                            'outdent',
                            'indent',
                            '|',
                            'imageUpload',
                            'blockQuote',
                            'insertTable',
                            'undo',
                            'redo'
                        ]
                    },
                    language: 'en',
                    image: {
                        toolbar: [
                            'imageTextAlternative',
                            'imageStyle:inline',
                            'imageStyle:block',
                            'imageStyle:side',
                            'linkImage'
                        ]
                    },
                    table: {
                        contentToolbar: [
                            'tableColumn',
                            'tableRow',
                            'mergeTableCells',
                            'tableCellProperties',
                            'tableProperties'
                        ]
                    },
                    licenseKey: '',


                })
                .then(editor => {
                    window.editor = editor;


                })
                .catch(error => {
                    console.error('Oops, something went wrong!');
                    console.error('Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:');
                    console.warn('Build id: xyb2mfluc2ki-unt8fr6ckh47');
                    console.error(error);
                });
        }
        function stopEditor() {
            window.editor.destroy();
            document.getElementById("myckeditor").style.display = "none";
        }
        window.toggleValue = false;
        function toggleEditor() {
            if (window.toggleValue === false) {
                window.toggleValue = true;
                hideTranslationButtons();
                startEditor();
            } else if (window.toggleValue === true) {
                window.toggleValue = false;
                hideTranslationButtons();
                stopEditor();
            }
        }
        window.toggleEditor = toggleEditor;
    </script>
@endpush

Now for a quick explanation of the Blade file. You need a CKEditor JS bundle. You may get it on the official site, ideally you can generate the bundle with whatever plugins you need. Build it here. You can find the CKEditor initialization snippet in the sample html file in the archive you will retrieve. The JS bundle is in the build folder of the same archive.

The form field files are integrated in a Voyager structure that uses the @stack directive, this is where the @push directive injects the scripts. We created a textarea that has the form-control class. This is essential. If you don’t add this class it will not save the data and you will get errors. Next, you must understand how CKEditor and Voyager work.

Let’s assume we enabled Voyager multilanguage support and have chosen English and Dutch. When we are inside a BREAD edit page there are two blue buttons in our Voyager app: English and Dutch. You may find them in the top right corner, you can’t miss them. When you press on a language button it changes the textarea data and stores internally the changes from the previous language. So when you press another language it actually changes the textarea child data.

Now for the CKEditor part. When we instantiate a new editor we inject it into a DOM element. In our case it is #myckeditor id. It creates an editor instance with the data from the textarea. Making language change work is a bit tricky though. When we press a Voyager language change button, the data in the textarea is replaced with another language while the previous language data is stored by Voyager. At the same time CKEditor holds in memory the data it is working with. When you submit the form it will inject whatever value it has back into the textarea. So if you edited Dutch and then switched to English, it will inject the Dutch data into the English section simply because the instance knows only what it holds at the moment. As a result Voyager will work, but whatever you edit will be injected equally in all languages. What we need to do is destroy the editor instance and recreate it when we change the language. When we destroy the editor instance it injects the internal data back into the textarea. Then when we press the Voyager button, it takes whatever we just edited, replaces it with another language and stores it internally until the form is submitted.

Ideally we would attach an event listener to the language change button so the editor will be destroyed and reinitialized. The problem I encountered is that we would have to do some pretty deep magic hacking to find the script responsible for the language change and introduce additional logic. I couldn’t do that as I had trouble finding whatever script is responsible for the language change. Therefore I created an edit button that hides the language change buttons and shows CKEditor. Once you finish entering your text and want to change to another language you would have to press this button so the editor instance is destroyed properly and the data re-injected back in the textarea. At the same time the translation buttons reappear and Voyager is happy to store your data in however many languages you want. Thank you for reading!

Today I learned: Primary and Secondary Joins

While developing my app I had to shape the data I get from the SQLAlchemy in a simplified way. Given models Client, Order, Receipt, where Order is referencing Client and Receipt is referencing Order I wanted to access in a view only relationship all receipts directly from the Client model. The way this is done is via primary and secondary joins.

class Client(Base):
    __tablename__ = "clients"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    entity = Column(String)

    receipts = relationship("Receipt", secondary="orders", primaryjoin="Client.id==Order.client_id",secondaryjoin="Receipt.order_uuid==Order.uuid", viewonly=True)


class Order(Base):
    __tablename__ = "orders"

    uuid = Column(UUID(as_uuid=True), primary_key=True)
    client_id = Column(Integer, ForeignKey("clients.id", ondelete="CASCADE"))
    created = Column(DateTime)


class Receipt(Base):
    __tablename__ = "receipts"

    uuid = Column(UUID(as_uuid=True), primary_key=True)
    order_uuid = Column(UUID(as_uuid=True), ForeignKey("orders.uuid", ondelete="CASCADE"))
    created = Column(DateTime)

Of course such a relationship can only be read-only because in this case it would be impossible to preserve the references to allow an editable relationship.

So far, so good. Good luck!!!