Mongomotor sem motor

  • publicado em 29 de setembro de 2025

O mongomotor é um ORM assíncrono para mongodb baseado no mongoengine e que usava o motor como driver assíncrono. Mas agora o motor foi deprecated em favor do pymongo assíncrono, então eu precisava mexer no mongomotor para trocar o driver. Com isso surgiu a terceira encarnação do mongomotor, que é meio que do mesmo jeito que a primeira.

Como era?

Do lado do motor/pymongo o que mudou foi que o motor usa um pool de threads para rodar as operações de IO, já o pymongo assíncrono implementa o suporte à asyncio diretamente no pymongo. A api, em teoria, é a mesma do motor. Já do meu lado, no mongomotor, eu usava toda a estrutura do motor pra também rodar as operações num pool de threads, e como manter meu próprio fork do motor não era de jeito nenhum uma opção, eu precisaria mudar como o mongomotor funciona.

Esses dois módulos aqui (que já não existem mais) eram o cerne da coisa:

# metaprogramming.py

import functools
from mongoengine.base.metaclasses import (
    TopLevelDocumentMetaclass,
    DocumentMetaclass
)
from mongoengine.connection import DEFAULT_CONNECTION_NAME
from mongoengine.context_managers import switch_db as me_switch_db
from motor.metaprogramming import MotorAttributeFactory
from pymongo.database import Database
from mongomotor import utils
from mongomotor.exceptions import ConfusionError
from mongomotor.monkey import MonkeyPatcher


def asynchronize(method, cls_meth=False):
    """Decorates `method` so it returns a Future.

    :param method: A mongoengine method to be asynchronized.
    :param cls_meth: Indicates if the method being asynchronized is
       a class method."""

    # If we are not in the main thread, things are already asynchronized
    # so we don't need to asynchronize it again.
    if not utils.is_main_thread():
        return method

    def async_method(instance_or_class, *args, **kwargs):
        framework = get_framework(instance_or_class)
        loop = kwargs.pop('_loop', None) or get_loop(instance_or_class)

        future = framework.run_on_executor(
            loop, method, instance_or_class, *args, **kwargs)
        return future

    # for mm_extensions.py (docs)
    async_method.is_async_method = True
    async_method = functools.wraps(method)(async_method)
    if cls_meth:
        async_method = classmethod(async_method)

    return async_method


def synchronize(method, cls_meth=False):
    """Runs method while using the synchronous pymongo driver.

    :param method: A mongoengine method to run using the pymongo driver.
    :param cls_meth: Indicates if the method is a class method."""

    def wrapper(instance_or_class, *args, **kwargs):
        db = instance_or_class._get_db()
        if isinstance(db, Database):
            # the thing here is that a Sync method may be called by another
            # Sync method and if that happens we simply execute method
            r = method(instance_or_class, *args, **kwargs)

        else:
            # here we change the connection to a sync pymongo connection.
            alias = utils.get_alias_for_db(db)
            cls = instance_or_class if cls_meth else type(instance_or_class)
            alias = utils.get_sync_alias(alias)
            with switch_db(cls, alias):
                r = method(instance_or_class, *args, **kwargs)

        return r
    wrapper = functools.wraps(method)(wrapper)
    if cls_meth:
        wrapper = classmethod(wrapper)
    return wrapper


def _get_db(obj):
    """Returns the database connection instance for a given object."""

    if hasattr(obj, '_get_db'):
        db = obj._get_db()

    elif hasattr(obj, 'document_type'):
        db = obj.document_type._get_db()

    elif hasattr(obj, '_document'):
        db = obj._document._get_db()

    elif hasattr(obj, 'owner_document'):
        db = obj.owner_document._get_db()

    elif hasattr(obj, 'instance'):
        db = obj.instance._get_db()

    else:
        raise ConfusionError('Don\'t know how to get db for {}'.format(
            str(obj)))

    return db


def get_framework(obj):
    """Returns an asynchronous framework for a given object."""

    db = _get_db(obj)
    return db._framework


def get_loop(obj):
    """Returns the io loop for a given object"""

    db = _get_db(obj)
    return db.get_io_loop()


def get_future(obj, loop=None):
    """Returns a future for a given object"""

    framework = get_framework(obj)
    if loop is None:
        loop = framework.get_event_loop()
    future = framework.get_future(loop)
    return future


class switch_db(me_switch_db):

    def __init__(self, cls, db_alias):
        """ Construct the switch_db context manager

        :param cls: the class to change the registered db
        :param db_alias: the name of the specific database to use
        """
        self.cls = cls
        self.collection = cls._collection
        self.db_alias = db_alias
        self.ori_db_alias = cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME)
        self.patcher = MonkeyPatcher()

    def __enter__(self):
        """ changes the db_alias, clears the cached collection and
        patches _connections"""
        super().__enter__()
        self.patcher.patch_async_connections()
        return self.cls

    def __exit__(self, t, value, traceback):
        """ Reset the db_alias and collection """
        self.cls._meta["db_alias"] = self.ori_db_alias
        self.cls._collection = self.collection
        self.patcher.__exit__(t, value, traceback)


class OriginalDelegate(MotorAttributeFactory):

    """A descriptor that wraps a Motor method, such as insert or remove
    and returns the original PyMongo method. It still uses motor pool and
    event classes so it needs to run in a child greenlet.

    This is done  because I want to be able to asynchronize a method that
    connects to database but I want to do that in the mongoengine methods,
    the driver methods should work in a `sync` style, in order to not break
    the mongoengine code, but in a child greenlet to handle the I/O stuff.
    Usually is complementary to :class:`~mongomotor.metaprogramming.Async`.
    """

    def create_attribute(self, cls, attr_name):
        return getattr(cls.__delegate_class__, attr_name)


class Async(MotorAttributeFactory):

    """A descriptor that wraps a mongoengine method, such as save or delete
    and returns an asynchronous version of the method. Usually is
    complementary to :class:`~mongomotor.metaprogramming.OriginalDelegate`.
    """

    def __init__(self, cls_meth=False):
        self.cls_meth = cls_meth
        self.original_method = None

    def _get_super(self, cls, attr_name):
        # Tries to get the real method from the super classes
        method = None

        for base in cls.__bases__:
            try:
                method = getattr(base, attr_name)
                # here we use the __func__ stuff because we want to bind
                # it to an instance or class when we call it
                method = method.__func__
            except AttributeError:
                pass

        if method is None:
            raise AttributeError(
                '{} has no attribute {}'.format(cls, attr_name))

        return method

    def create_attribute(self, cls, attr_name):
        self.original_method = self._get_super(cls, attr_name)
        self.async_method = asynchronize(self.original_method,
                                         cls_meth=self.cls_meth)
        return self.async_method


class Sync(Async):
    """A descriptor that wraps a mongoengine method, ensure_indexes
    and runs it using the synchronous pymongo driver.
    """

    def create_attribute(self, cls, attr_name):
        method = self._get_super(cls, attr_name)
        return synchronize(method, cls_meth=self.cls_meth)


class AsyncWrapperMetaclass(type):
    """Metaclass for classes that use MotorAttributeFactory descriptors."""

    def __new__(cls, name, bases, attrs):

        new_class = super().__new__(cls, name, bases, attrs)
        for attr_name, attr in attrs.items():
            if isinstance(attr, MotorAttributeFactory):
                real_attr = attr.create_attribute(new_class, attr_name)
                setattr(new_class, attr_name, real_attr)

        return new_class


class AsyncTopLevelDocumentMetaclass(AsyncWrapperMetaclass,
                                     TopLevelDocumentMetaclass):
    """Metaclass for top level documents that have asynchronous methods."""


class AsyncGenericMetaclass(AsyncWrapperMetaclass):
    """Metaclass for any type of documents that use MotorAttributeFactory."""


class AsyncDocumentMetaclass(AsyncWrapperMetaclass, DocumentMetaclass):
    """Metaclass for documents that use MotorAttributeFactory."""
# core.py

import types
from motor.core import (AgnosticCollection, AgnosticClient, AgnosticDatabase,
                        AgnosticCursor)
from motor.metaprogramming import create_class_with_framework
import pymongo
from pymongo.database import Database
from pymongo.collection import Collection
from mongomotor.metaprogramming import OriginalDelegate


def _rebound(ret, obj):
    try:
        ret = types.MethodType(ret.__func__, obj)
    except AttributeError:
        pass
    return ret


class MongoMotorAgnosticCursor(AgnosticCursor):

    __motor_class_name__ = 'MongoMotorCursor'

    distinct = OriginalDelegate()
    explain = OriginalDelegate()

    def __init__(self, *args, **kwargs):
        super(AgnosticCursor, self).__init__(*args, **kwargs)

        # here we get the mangled stuff in the delegate class and
        # set here
        attrs = [a for a in dir(self.delegate) if a.startswith('_Cursor__')]
        for attr in attrs:
            setattr(self, attr, getattr(self.delegate, attr))

    # these are used internally. If you try to
    # iterate using for in a main greenlet you will
    # see an exception.
    # To iterate use a queryset and iterate using motor style
    # with fetch_next/next_object
    def __iter__(self):
        return self

    def __next__(self):
        return next(self.delegate)

    def __getitem__(self, index):
        r = self.delegate[index]
        if isinstance(r, type(self.delegate)):
            # If the response is a cursor, transform it into a
            # mongomotor cursor.
            r = type(self)(r, self.collection)
        return r

    # @aiter_compat
    # def __aiter__(self):
    #     return self

    # async def __anext__(self):
    #     # An optimization: skip the "await" if possible.
    #     if self._buffer_size() or await self.fetch_next:
    #         return self.next_object()
    #     raise StopAsyncIteration()


class MongoMotorAgnosticCollection(AgnosticCollection):

    __motor_class_name__ = 'MongoMotorCollection'

    # Using the original delegate method (but with motor pool and event)
    # so I don't get a future as the return value and don't need to work
    # with mongoengine code.
    # insert = OriginalDelegate()
    insert_many = OriginalDelegate()
    insert_one = OriginalDelegate()
    # save = OriginalDelegate()
    # update = OriginalDelegate()
    update_one = OriginalDelegate()
    update_many = OriginalDelegate()
    find_one = OriginalDelegate()
    # find_and_modify = OriginalDelegate()
    find_one_and_update = OriginalDelegate()
    find_one_and_delete = OriginalDelegate()
    index_information = OriginalDelegate()

    def __init__(self, database, name, _delegate=None):

        db_class = create_class_with_framework(
            MongoMotorAgnosticDatabase, self._framework, self.__module__)

        if not isinstance(database, db_class):
            raise TypeError("First argument to MongoMotorCollection must be "
                            "MongoMotorDatabase, not %r" % database)

        delegate = _delegate if _delegate is not None else\
            Collection(database.delegate, name)
        super(AgnosticCollection, self).__init__(delegate)
        self.database = database

    def __getattr__(self, name):
        if name.startswith('_'):
            # Here first I try to get the _attribute from
            # from the delegate obj.
            try:
                ret = getattr(self.delegate, name)
            except AttributeError:
                raise AttributeError(
                    "%s has no attribute %r. To access the %s"
                    " collection, use collection['%s']." % (
                        self.__class__.__name__, name, name,
                        name))
            return _rebound(ret, self)

        return self[name]

    def __getitem__(self, name):
        collection_class = create_class_with_framework(
            MongoMotorAgnosticCollection, self._framework, self.__module__)

        return collection_class(self.database, self.name + '.' + name)

    def find(self, *args, **kwargs):
        """Create a :class:`MongoMotorAgnosticCursor`. Same parameters as for
        PyMongo's :meth:`~pymongo.collection.Collection.find`.

        Note that ``find`` does not take a `callback` parameter, nor does
        it return a Future, because ``find`` merely creates a
        :class:`MongoMotorAgnosticCursor` without performing any operations
        on the server.
        ``MongoMotorAgnosticCursor`` methods such as
        :meth:`~MongoMotorAgnosticCursor.to_list` or
        :meth:`~MongoMotorAgnosticCursor.count` perform actual operations.
        """
        if 'callback' in kwargs:
            raise pymongo.errors.InvalidOperation(
                "Pass a callback to each, to_list, or count, not to find.")

        cursor = self.delegate.find(*args, **kwargs)
        cursor_class = create_class_with_framework(
            MongoMotorAgnosticCursor, self._framework, self.__module__)

        return cursor_class(cursor, self)


class MongoMotorAgnosticDatabase(AgnosticDatabase):

    __motor_class_name__ = 'MongoMotorDatabase'

    dereference = OriginalDelegate()
    # authenticate = OriginalDelegate()

    def __init__(self, client, name, _delegate=None):
        if not isinstance(client, AgnosticClient):
            raise TypeError("First argument to MongoMotorDatabase must be "
                            "a Motor client, not %r" % client)

        self._client = client
        delegate = _delegate or Database(client.delegate, name)
        super(AgnosticDatabase, self).__init__(delegate)

    def __getattr__(self, name):
        if name.startswith('_'):
            # samething. try get from delegate first
            try:
                ret = getattr(self.delegate, name)
            except AttributeError:
                raise AttributeError(
                    "%s has no attribute %r. To access the %s"
                    " collection, use database['%s']." % (
                        self.__class__.__name__, name, name,
                        name))
            return _rebound(ret, self)

        return self[name]

    def __getitem__(self, name):
        collection_class = create_class_with_framework(
            MongoMotorAgnosticCollection, self._framework, self.__module__)

        return collection_class(self, name)

    def eval(self, code, *fields):
        return self.command('eval', code, *fields)


class MongoMotorAgnosticClientBase(AgnosticClient):

    # max_write_batch_size = ReadOnlyProperty()

    def __getattr__(self, name):
        if name.startswith('_'):
            # the same. Try get from delegate.
            try:
                ret = getattr(self.delegate, name)
            except AttributeError:

                raise AttributeError(
                    "%s has no attribute %r. To access the %s"
                    " database, use client['%s']." % (
                        self.__class__.__name__, name, name, name))

            return _rebound(ret, self)

        return self[name]

    def __getitem__(self, name):
        db_class = create_class_with_framework(
            MongoMotorAgnosticDatabase, self._framework, self.__module__)

        return db_class(self, name)


class MongoMotorAgnosticClient(MongoMotorAgnosticClientBase, AgnosticClient):

    __motor_class_name__ = 'MongoMotorClient'

Com esses dois caras aí - e mais uns monkey patches - o que eu fazia era estender as classes do mongoengine, usava os decorators para deixar os métodos assíncronos e assim eu não precisava escrever muito código, muito da lógica ainda ficava no mongoengine. Algo assim:

class QuerySet(MEQuerySet, metaclass=AsyncGenericMetaclass):

    distinct = Async()
    explain = Async()
    in_bulk = Async()
    map_reduce = Async()
    modify = Async()
    update = Async()
    ...

Nem tudo ficava assim, alguns métodos eu tinha que meio que re-escrever algumas coisas, mas já me poupava muita coisa.

Como ficou?

Agora, sem o pool de threads e toda a metaprogramação pra deixar a coisa assíncrona, eu precisei re-escrever as coisas de verdade. Agora, um método que antes eu usava só um Async(), agora eu tive que implementar de verdade, tipo assim:

...
async def distinct(self, field):
        """Return a list of distinct values for a given field.

        :param field: the field to select distinct values from

        .. note:: This is a command and won't take ordering or limit into
           account.
        """
        queryset = self.clone()

        try:
            field = self._fields_to_dbfields([field]).pop()
        except LookUpError:
            pass

        raw_values = await queryset._cursor.distinct(field)
        if not self._auto_dereference:
            return raw_values

        distinct = await self._dereference(
            raw_values, 1, name=field, instance=self._document)

        doc_field = self._document._fields.get(field.split(".", 1)[0])
        instance = None

        # We may need to cast to the correct type eg.
        # ListField(EmbeddedDocumentField)
        EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
        ListField = _import_class("ListField")
        GenericEmbeddedDocumentField = _import_class(
            "GenericEmbeddedDocumentField")
        if isinstance(doc_field, ListField):
            doc_field = getattr(doc_field, "field", doc_field)
        if isinstance(doc_field, (EmbeddedDocumentField,
                                  GenericEmbeddedDocumentField)):
            instance = getattr(doc_field, "document_type", None)

        # handle distinct on subdocuments
        if "." in field:
            for field_part in field.split(".")[1:]:
                # if looping on embedded document, get the document
                # type instance
                if instance and isinstance(
                    doc_field, (EmbeddedDocumentField,
                                GenericEmbeddedDocumentField)
                ):
                    doc_field = instance
                # now get the subdocument
                doc_field = getattr(doc_field, field_part, doc_field)
                # We may need to cast to the correct type eg.
                # ListField(EmbeddedDocumentField)
                if isinstance(doc_field, ListField):
                    doc_field = getattr(doc_field, "field", doc_field)
                if isinstance(
                    doc_field, (EmbeddedDocumentField,
                                GenericEmbeddedDocumentField)
                ):
                    instance = getattr(doc_field, "document_type", None)

        if instance and isinstance(
            doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)
        ):
            distinct = [instance(**doc) for doc in distinct]

        return distinct
        ...

Eu basicamente peguei o método do mongoengine, copiei e alterei o que precisava pra funcionar com o pymongo assíncrono. Muito do código do mongoengine que eu só usava de graça, agora que eu copiei é minha responsabilidade manter, mas isso tem até sua vantagem inesperada: Como o mongomotor é uma biblioteca que já tem o que eu preciso, ela é mantida só atualizando as dependências e corrigindo eventuais bugs. Isso, junto com o fato de que eu mexo nas entranhas do mongoengine, acontece de dar uma quebradeira chata quando atualiza. Comigo pegando mais código do mongoengine acredito que vai dar uma diminuída. Ainda vai acontecer, porque ainda temos monkey patches e mexidas nas entranhas, mas com o tempo e as correções que com certeza virão nas atualizações, tende a estabilizar. Eu acho. Além do que o código assim fica muito mais simples e fácil de achar os bugs.

Um pouco de história

Eu falei que essa era a terceira encarnação do mongomotor. Já a primeira (por volta de 2013) não era nem uma biblioteca separada, era um package dentro de um projeto feito com tornado e a ideia era bem parecida com essa. Eu simplesmente reimplementava o que precisava pra funcionar com o tornado. Quando eventualmente quis usar o mesmo esquema em outro projeto - esse com asyncio - foi que separei em uma biblioteca separada e fui ver como o motor implementava o suporte a tornado e asyncio (que na época não tinha nem a sintaxe de async/await). Quando vi como o motor funcionava pensei: «Porra, isso é muito louco».

Foi assim que surgiu a segunda encarnação do mongomotor, com o metaprogramming que mostrei no começo. Aquilo era tudo ideia que peguei do motor e adaptei pra usar com o mongoengine. O pymongo tinha um parâmetro não documentado que era basicamente feito pro motor usar. Se a memória não me falha, o parâmetro chamava _sockets e tinha um comentário tipo: «não mexe se você não sabe o que é isso». Basicamente tava lá só pro motor. O mongoengine não tinha nada assim pra mim, então monkey patch foi mato.

Agora temos o terceiro mongomotor. Primeiro re-escrevi pra suportar asyncio, agora pra tirar o motor. O que será depois? Só falta o mongoengine me aprontar alguma.

Antes de ir embora

Lembra que eu comentei que a api do motor e do pymongo async era teoricamente a mesma? Então, não é! No motor o queryset.aggregate não é um método assíncrono e no pymongo é. Tive que quebrar api do mongomotor.


Bugs que fizeram alguns aniversários

  • publicado em 24 de setembro de 2025

Esses dias, numa madrugada topei com um bug no toxicbuild que existia há uns 8 anos pelo menos. No dia seguinte, de manhã no trabalho, um colega me fala que encontrou um bug que estava lá quase há 6 anos. E além disso ser engraçado e bem curioso que dois bugs arqueológicos tenham sido encontrados num espaço de horas, me deixou pensando sobre a coisa de escrever software.

A gente até tenta

A primeira coisa que vem à cabeça quando alguém lê isso claro que é: «porra, mas você é burro também, hem». Errado não tá 100%, eu trabalho sozinho no toxicbuild mesmo, poderia ser isso. Mas no trabalho somos um time, então aí fica mais difícil culpar só a burrice. E a gente nem é desleixado, a gente tenta fazer a coisa bonitinha. No trabalho nossas rotinas de testes são parecidas com as do toxicbuild, que eu sei que não são perfeitas, mas considero ser algo honesto e que se pode confiar - até onde se pode confiar…

A primeira coisa que eu pensei foi: «preciso melhorar nossos testes» e isso é uma verdade, sempre há espaço pra melhorar, mas as coisas nem sempre são tão diretas assim. No caso do bug do trabalho, um teste fuzzy talvez tivesse pegado, mas o nosso código lá roda em um lugar estranho com gente esquisita, não é tão simples assim rodar uns testes doido lá. Já no caso do toxicbuild isso não seria suficiente, o problema não era com um input estranho, era uma interação entre o que eu escrevi e o ambiente no geral, tudo rolando junto, tanto que só peguei o bug quando mudei as parada de servidor (e nem foi a primeira mudança).

Mas sempre vai faltar

Encanado com isso de melhorar as coisas duas coisas me ficaram claras: precisa melhorar e nunca vai ser o suficiente. Nossa, descobri a pólvora agora! Todo mundo sabe que bug free não existe. Mas é curioso mesmo assim depois de vinte anos escrevendo software ver os bugs escapando assim.


Hoje você passa e não vê que amanhã vai ser você

  • publicado em 31 de agosto de 2025

A última vez que passei pela praça da Sé era um dia bonito, ensolarado e eu estava indo ver uma exposição na caixa cultural. Mas o que eu mais lembro desse dia foi algo inesperado que vi na praça, não algo que tenha visto na exposição.

Saí do metrô e estava achando até estranho que a praça estava menos podreira que o habitual. Tinha uns maluco lá, o de sempre, mas tava bem de boas. Mais ou menos no meio do caminho até a caixa cultural tinha uma rodinha com uns dois GCMs e uns maninho de lá. Chegando mais perto vi que eles estavam olhando pra um mano caído na praça. Um cara tinha acabado de morrer ali!

O cara era um gordinho meio inchado de pinga, mas longe do estereótipo do nóia carcomido que morre na rua. O cara tava mais pro maluco pau d’água do bairro, uma cara de «o gordinho simpático do rolê». Ele tava lá, com uma cara de que tava tirando um cochilinho gostoso, mas esse era o cochilo eterno.

Isso aconteceu não muito depois de eu ter ficado sabendo de um conhecido que morreu na frente da galeria do rock. O cara tava lá bebendo com os amigos dele de boas, sentou no banco que fizeram na frente da galeria e tava conversando com o pessoal. Do nada ele cai do banco e quando foram ver, já tinha ido.

Esse dia da praça não foi a primeira vez que vi um cara morto assim do nada, esse conhecido não foi o primeiro a morrer, amigos muito mais próximos já morreram. Mas sei lá, essa vez da praça me deixou pensativo. Acho que a meia-idade vai deixando a gente mole.

O cara tá lá de boas, fazendo o rolezinho dele e pá! Cai morto de uma hora pra outra. Em algum tempo não vai existir traço da existência da pessoa e é isso. A vida é só um sopro, hoje você tá aqui, amanhã não tá, depois de amanhã ninguém que você conheceu estará, e nem as tabuletas embaixo das portas estarão.


Enviar email sem enviar email

  • publicado em 28 de agosto de 2025

No post anterior eu comentei que fiz um formulário de contato pro blog e pra isso precisava me avisar quando chegasse mensagem. O mais óbvio seria mandar um email pra mim mesmo, mas por que enviar um email quando eu posso só salvar um arquivo num diretório?

Eu já conhecia por cima o maildir, que é um formato usado para armazenar emails num sistema de arquivos local, onde cada email é um arquivo, tem um layout específico de diretórios e regras para os nomes dos arquivos. A especificação do maildir é bem pequena e trata dos diretórios onde as mensagens devem ser salvas e como os nomes dos arquivos devem ser gerados. Já o formato usado no arquivo com com o email é definido pelas RFCs 5322, 2045, 2046 e 2047.

O maildir

A estrutura de diretórios do maildir são três diretórios: tmp, cur e new. O diretório tmp é onde ficam os arquivos dos emails que ainda estão sendo recebidos. Quando o arquivo é recebido ele tem que ser copiado para o diretório new. O cliente de emails avisa o usuário que existem novos emails. E quando o usuário ler o email ele deve ser movido para o diretório cur. O nome do arquivo deve ser um nome único e quando movido de new para cur o nome deve ter adicionado algumas informações no final.

Nota

Na especificação do maildir tem algumas sugestões para gerar o nome único, entre eles pegar algo de /dev/urandom e deixar o nome como o hexa do que foi pego em /dev/urandom. Basicamente pode-se usar um UUID4 (acho :P)

As informações adicionadas ao final do nome do arquivo depois de movido são basicamente informações sobre o status do email. A semântica da info é a seguinte:

Info começada com 1,: Semântica experimental
Info começada com 2,: Cada letra depois da vírgula é uma flag independente.

  • As flags são:
    • P (passed): O usuário encaminhou o email para alguém

    • R (replied): O usuário respondeu o email

    • S (seen): O usuário viu o conteúdo do email

    • T (trashed): Mensagem marcada como lixo

    • D (draft): Mensagem é um rascunho

    • F (flagged): Uma tag definida pelo usuário

Então uma mensagem em um arquivo nomeado 22eb0cf0-b71c-4411-88a1-aef292007e58 quando está em new, depois de movido para cur teria um nome mais ou menos assim 22eb0cf0-b71c-4411-88a1-aef292007e58:2,SR.

O IMF

IMF é o internet message format que é um formato para transmissão de mensagens de texto em ASCII definido na RFC 5322 e estendido com a introdução de MIME tyes pela RFC 2045 (e relacionados) para utilização de outros conjuntos de caracteres e envio de outros tipos de conteúdo além de texto, como imagens e sons.

Mensagens são basicamente duas partes: o cabeçalho e o corpo. O cabeçalho pode ter várias linhas, que são os campos do cabeçalho, no formato: NomeDoCampo: Valor\r\n. Uma linha em branco separa o cabeçalho do corpo. O corpo na especificação original era somente uma mensagem em ASCII e com a introdução de MIME types o corpo pode ser uma variedade de formatos, definidos num cabeçalho. Aqui vai um exemplo de uma mensagem com um corpo multipart que pode conter vários tipos de mensagem no mesmo corpo

Date: Thu, 28 Aug 2025 01:54:01 -0300
From: Juca <juca@poraodojuca>
To: Zé <ze@casadoze>
Subject: Olá
MIME-Version: 1.0
Message-ID: 1756357403.issodeveriaserumaidentificacao@poraodojuca
Content-Type: multipart/mixed; boundary="uma-string-que-marca-o-limite"

--uma-string-que-marca-o-limite
Content-Type: text/plain; charset="UTF-8"

Como vai, como vai, vai, vai?

--uma-string-que-marca-o-limite
Content-Type: text/html; charset="UTF-8"

<html><body><strong>
Como vai, como vai, vai, vai?
</strong></body></html>

--uma-string-que-marca-o-limite
Content-Type: image/jpeg
Content-Disposition: attachment; filename="foto.jpeg"
Content-Transfer-Encoding: base64

... aqui viria uma imagem jpeg em base64 ...

--uma-string-que-marca-o-limite--

O que acontece aí é o seguinte: A primeira parte, até a primeira linha em branco é o cabeçalho da mensagem e o restante depois da primeira linha em branco é o corpo, um corpo que tem 3 partes distintas, uma em texto puro, uma em html e uma imagem anexa.

Os campos do cabeçalho obrigatórios são o From e o Date, o Message-ID apesar de não ser obrigatório deveria estar presente em todas as mensagens. O formato de Message-ID é <timestamp>.<uma-string-identificadora>@<umhost>. O Content-Type: multipart/mixed; boundary="uma-string-que-marca-o-limite" indica que o corpo da mensagem está dividido em várias partes, cada um com seu próprio Content-Type. boundary é uma string que separa uma parte do corpo de outra. Essa string deve ser gerada de maneira que seja bem difícil ela esteja repetida no corpo do email.

Eu não implementei tudo isso

Claro que isso é coisa demais só pro que eu precisava. O formulário de contato é só um campo de texto e pra fazer a parte do maildir já tinha o go-maildir. Então a minha implementaçãozinha meia-boca ficou mais ou menos assim:

// EmailMessage represents an email to be sent. Note that as this have
// no content type and the body is a string, only text/plain bodies are
// supported.
type EmailMessage struct {
        From      string
        To        []string
        Subject   string
        Body      string
        Timestamp int64
}

// NewEmailMessage checks for missing from or to.
func NewEmailMessage(from string, to []string, subject string, body string) (EmailMessage, error) {
        if from == "" {
                return EmailMessage{}, errors.New("from can't be empty")
        }
        if to == nil || len(to) == 0 {
                return EmailMessage{}, errors.New("to can't be empty")
        }
        ts := time.Now().Unix()
        msg := EmailMessage{
                From:      from,
                To:        to,
                Subject:   subject,
                Body:      body,
                Timestamp: ts,
        }
        return msg, nil
}


type keyGen func() (string, error)

// MaildirSender represents a maildir delivery
type MaildirSender struct {
        MaildirPath string
        keyGen      keyGen
}

// SendEmail writes an EmailMessage to a local maildir
func (s MaildirSender) SendEmail(msg EmailMessage) error {
        var d = maildir.Dir(s.MaildirPath)
        err := initMaildir(d)
        if err != nil {
                return err
        }

        mformat, err := EmailMessage2Maildir(msg, s.keyGen)
        if err != nil {
                return err
        }

        del, err := maildir.NewDelivery(s.MaildirPath)
        if err != nil {
                return err
        }

        _, err = del.Write([]byte(mformat))
        if err != nil {
                return err
        }

        err = del.Close()
        if err != nil {
                return err
        }
        return nil
}

// NewMaildirSender returns a new NewMaildirSender instance
func NewMaildirSender(path string) MaildirSender {
        s := MaildirSender{
                MaildirPath: path,
                keyGen:      GenKey,
        }
        return s
}

// EmailMessage2Maildir converts an EmailMessage to a string in the
// maildir file format.
func EmailMessage2Maildir(msg EmailMessage, gen keyGen) (string, error) {

        dtfmt := "Mon, 2 Jan 2006 15:04:05 -0700"
        loc, err := time.LoadLocation("UTC")
        if err != nil {
                return "", err
        }
        dt := time.Unix(msg.Timestamp, 0).In(loc)
        dtStr := dt.Format(dtfmt)
        key, err := gen()
        if err != nil {
                return "", err
        }
        msgId := fmt.Sprintf("<%d.%s@localhost>", msg.Timestamp, key)

        mformat := fmt.Sprintf("From: %s\n", msg.From)
        toStr := strings.Join(msg.To, ",")
        mformat += fmt.Sprintf("To: %s\n", toStr)
        mformat += fmt.Sprintf("Subject: %s\n", msg.Subject)
        mformat += fmt.Sprintf("Date: %s\n", dtStr)
        mformat += fmt.Sprintf("Message-ID: %s\n", msgId)
        mformat += fmt.Sprintf("MIME-Version: 1.0\n")
        mformat += fmt.Sprintf("Content-Type: text/plain; charset=\"UTF-8\"\n")
        mformat += "\n"
        mformat += msg.Body
        return mformat, nil
}

Mais pra frente do post vai ficar claro que eu não precisava do go-maildir, mas na hora foi isso o que eu fiz e boas, ficou! Agora é só meter um rsync pra pegar esses arquivos pra minha máquina e foi.

Claro que tinha um bug no Kmail

O Kmail é um cliente de mail pro KDE que tem muitos anos que eu sempre tento dar uma chance. Fui dar mais uma chance com o maildir, e claro que tinha um bug. Então chegou a hora da jigajoga!

A coisa é assim: na minha máquina local eu tenho uma estrutura de diretórios do maildir, com new, cur e tmp, assim o Kmail já reconhece esse diretório como um diretório que vai receber emails. Aí eu faço um rsync do diretório new do servidor para o diretório new na minha máquina e movo as menagens para um diretório de backup no servidor.

Nota

Aqui que a coisa fica clara que eu não precisava do maildir no servidor, era só jogar o arquivo num diretório qualquer, mas enfim… Burrice nunca falta no estoque.

O bug é que depois que eu fazia o rsync dos emails não atualizava o Kmail automaticamente, eu precisava clicar em «Atualizar» pra receber uma notificação. Então pra finalizar, depois do rsync, a gente dá um chute no Kmail pra ele acordar.

#!/bin/bash

LOCALDIR=~/somewhere/new/
REMOTEDIR='/somewhere/new/'
REMOTEDIR_CUR='/somewhere/cur/'

rsync -avz --progress eu@meuservidor:$REMOTEDIR $LOCALDIR --rsync-path="sudo rsync"
ssh eu@meuservidor "sudo find $REMOTEDIR -maxdepth 1 -type f -exec mv {} $REMOTEDIR_CUR \;"

# aqui a gente usa o dbus e pega todos os recursos que estão vinculados ao Akonadi
qdbus org.freedesktop.Akonadi /ResourceManager org.freedesktop.Akonadi.ResourceManager.resourceInstances |
    while read resource;
    do

        if [[ "$resource" == *"maildir"* ]]; then
            # se for maildir, a gente manda sincronizar a coisa
            SERVICE_DBUS="org.freedesktop.Akonadi.Resource.${resource}"
            OBJECT_PATH="/"
            METHOD_NAME="org.freedesktop.Akonadi.Resource.synchronize"
            qdbus $SERVICE_DBUS $OBJECT_PATH $METHOD_NAME

        fi
    done

E agora sim, envio e recebo emails sem enviar emails!


Blog novo, vida velha

  • publicado em 17 de agosto de 2025

Faz um tempo que eu já tava pensando: «Pô, pra que banco de dados, servidor de aplicação, um monte de coisa só pro meu blog? Seria muito melhor uns html queimado.», mas a preguiça sempre batia. Um dia fui pensar em colocar um feed vi que o blog ainda rodava em python3.5. Ia dar um trabalhão pra atualizar! Então chegou a hora de um blog novo.

Um blog com sphinx

Já que eu queria uns html queimado, o mais simples seria usar o sphinx, que já estou acostumado, é de boas escrever rst e já tinha um projeto de blog com sphinx, o ABlog. Com isso já deu uma animada, afinal eu não fui o único tonto que pensei em fazer um blog com sphinx.

A primeira coisa a fazer foi dar uma olhada no que o ABlog tem. E ele já tem basicamente metade do que eu preciva: já criava listagem de posts automáticamente e gerava feeds. Do que ele tem eu só precisaria mexer nas listas de posts pra paginar a coisa. Depois de paginar as listas de posts era só eu fazer o mesmo esquema que tem pros posts, só que pra fotos: Uma página pra cada foto e uma lista de fotos. Mamão-com-açúcar.

É hora de fazer

Chegou a hora do vamo vê e aí eu vi. Primeiro a estrutura blog -> catalog -> collection. Muito mais do que eu preciso, mas da hora. Depois tem o tal do init estranho que é _init e não __init__ que copia do __dict__ do cara. Serve pra um singleton durante todo o build. Esperto! Sobrescrevi Blog (com PhotoBlog), Catalog, Collection pra paginar as listas de posts, alterei a função que conecta no sinal do sphinx pra gerar os html e uso uma opção a mais para mostrar o post todo na listagem ao invés de só o começo. Foi de boas.

Agora era fazer uma listagem paginada de fotos. Alterei PhotoBlog pra ter uma collection paginada de fotos, mas ainda me faltavam as fotos. Criei uma directive Photo e aí faltava juntar uma coisa com a outra. Quando comecei a olhar como o ablog fazia isso foi quando começou minha tristeza. A função do ablog que faz isso é gigante, chatona de ler e o pior, é uma função muito ensimesmada, digamos assim, então não dá muito pra extender a parada. E essa não é a única, tem mais algumas peças nesse estilo. A preguiça começou a bater, mas vamos lá…

O esquema era basicamente pegar todos os nodes do tipo foto, guardar isso no env do build e depois registrar nos PhotoBlog e boas, temos uma coleção de fotos no blog. Depois disso extendi o dirhtml builder do sphinx pra copiar os arquivos das fotos pro diretório de imagens do build e gerar as thumbnails que serão exibidas na lista de fotos. Nesse ponto eu já tinha a maioria das funcionalidades que eu queria, era hora de mexer no css. Aí a preguiça chegou de vez!

Os comentários

Eu fiquei um tempão sem mexer no blog, fazendo coisas mais úteis com meu tempo live (tipo jogar xadrez ou tocar guitarra :P) até que eu lembrei que todo blog precisa de comentários e animei de fazer os comentários.

Nota

O ABlog já tinha integração com o disqus e com o Isso, que é basicamente o que eu precisava, mas o disqus eu não queria usar e a velha mania de não ler a documentação direito me fez não ver que tinha integração com o Isso. Na verdade nunca tinha ouvido falar dele, então acabei fazendo o meu mesmo. Tonto tontando.

A ideia era fazer uma coisa bem simples pros comentários, então decidi fazer o esquema usando o sqlite de banco e uma api onde eu só preciso incluir um js na página e os cometários já aparecem. Até aí nada demais, umas tabelinhas no sqlite, um endpoint pra criar comentário, endpoint pra listar comentários (aqui dois na verdade, um que retorna um json e um que retorna um html), meia dúzia de linha de js (como o js deveria ser usado), o arroz com feijão da coisa. Até que o Bubbletea me chamou a atenção.

O esquema dos comentários - chamado parlante - funciona com clientes que tem permissão para certos domínios, então eu precisava cadastrar clientes e domínios e nada mais natural(?!) que fazer uma text user interface pra isso, não? Foi assim que o que era pra ser só uma paradinha de nada passou a ter dois executáveis: o parlante e o parlane-tui. Já não bastava ter feito o que não precisava, fez o desnecessário duas vezes! Mas tudo bem, foi. E agora eu tinha o que precisava pro blog. Ou quase…

Chegando nos finalmente

Depois de mais um tempão sem mexer no blog, peguei uns diazinhos de férias e decidi terminar essa parada. Dei um tapa na tui pra ficar mais bonitinha e foi pro css do blog. Eu já tinha o tema do sphinx que eu usava nas minhas documentações, a ideia era usar o mesmo tema pro blog, era só dar um tapa e mesmo assim e encheu o saco. A página de foto me deu uma canseira, foi difícil decidir como ela deveria ficar e depois demorei pra descobrir como usar a coisa do display:flex com order. Depois de mexer no tema me faltava o feed das fotos e um feed geral dos posts e fotos.

Comecei fazendo feed das fotos, que o conteúdo é basicamente o títudo da foto e a imagem. Depois foi fazer o feed geral e nesse momento eu me encontrei de novo com uma das funções feitas pra si mesma no ABlog, a parte que gera o feeds. No feed geral o item pode ser uma foto ou um post. Consegui reusar o item do feed de fotos, já o de posts não deu, tive que fazer um item de feed pra post específico pro feed geral. Passando por isso eu achava que já tava tudo pronto. Dei aquele rsync com o servidor e comecei a navegar no blog pra ver se tinha faltando algo. Tudo bem, tudo bom, até que a hostinger vem me atrapalhar.

A infra é sempre contra nóis, né?

Enquanto eu tava dando uma (o que eu esperava ser) útima olhada na parada, do nada o blog parou de responder, não pingava nem nada. No painel da hostinger a vps de pé, dava pra usar pelo ssh web dos cara, as regra de iptables tudo normal. Que porra que tava acontecendo? Troquei a minha conexão de casa pela do telefone e pimba! Foi! Fazendo um traceroute da coisa vi que quem tava dropando meus pacote era o úlitmo hop antes de chegar na minha vps, era a hostinger bloqueando meu ip! Agora passar pelo suporte que ia ser a dificuldade.

Primeiro tem que passar pelo bot maldito que te dá todos os passos que eu já tinha feito, depois quando chega num humano a resposta padrão é sempre a mesma: vps é um serviço auto gerenciado e a gente não pode fazer nada. Até alguém prestar atenção e entender que o bloqueio era por parte da hostinger foi difícil. Depois que entenderam, o chamado foi pra outra equipe, com outro prazo de atendimento. Depois de dois dias recebi um email dizendo que tinham feito alterações no firewall da hostiger e eu não deveria ter mais problemas. Funcionou até não funcionar mais.

Quando fui copiar o texto da página Sobre do blog antigo eu me lembrei que tinha um formulário de contato lá que na verdade nunca funcionou porque não tinha nem botão pra enviar a mensagem. Já que eu tava fazendo um blog novo essa era a hora de fazer funcionar. Implementei o contato no parlante e na hora de subir a nova versão, cadê que eu conseguia chegar no servidor? Bloquearam meu ip de novo! Vai lá eu falar com o suporte, toda aquela coisa (esse vai e volta com infra já tava me lembrando da firma) e até agora a coisa continua zuada.

Chegamos ao final

Depois de mais de uma semana desisti da hostinger. Fiz uma conta no oracle cloud e este bloguinho agora está no que diz ser uma vps pra sempre grátis. A máquina é bem meia boca, mas como só tem os html do blog e o parlante, dá e sobra. Depois de toda essa odisseia agora eu tenho um feed. Uau!

Ganhei algo com isso? Não. Vou ganhar algo com isso? Não. O blog é novo, mas a vidinha continua a mesma.


Uma jigajoga bacana

  • publicado em 12 de novembro de 2024

Jigajoga |ó| (ji-ga-jo-ga) - Artifício, ludíbrio; mecanismo ou solução resultante de improvisação. Depois que eu aprendi essa palavra eu nunca mais consegui dizer hack ou gambiarra, só jigajoga. E hoje vou contar de uma jigajoga do trampo.

O galho

Esses tempos no trampo eu precisava identificar um usuário que chega no nosso whatsapp. Em geral pra mandar alguém pro whatsapp você só manda url https://wa.me/<telefone>?text=oi. O problema aí é que eu não faço ideia de como o usuário chegou no whatsapp. Ele pode ter acessado via um link ou ter simplesmente chegado no whatsapp e falado oi. A única coisa que consigo saber com isso é o texto que o usuário me mandou e o número do telefone dele.

A primeira parte da coisa é simples, ao invés de enviar o usuário para o link do whatsapp direto, manda um link pra mim, aí eu consigo gerar um fingerprint do usuário e depois redirecionar para o whatsapp. Mas depois que mandar o usuário pro whatsapp, como eu sei que o usuário que chegou lá é o usuário x?

A ideia

Logo de cara um colega me mostrou como outra empresa tava fazendo isso: mandando uma string XYZ na mensagem, pedindo para o usuário enviar essa mensagem com a string e essa string serviria de id pra identificar o usuário. Mas isso é feio pra caralho. Então minha primeira ideia foi usar caracteres “invisíveis” (non-printable) no meio da mensagem. E bom, se eu precisava de uma identificação única de usuário o óbvio seria uuid, no caso o 4.

Então peguei uma lista de 17 caracteres “invisíveis” (16 pra a-f e 1 pra “-“) e aí fazia a tradução da representação em string de um uuid v4 pra uma string invisível usando os caracteres non-printable. Subi a coisa rapidinho, testei no meu whatsapp, funcionou. Coisa linda.

O código ficou mais ou menos assim:

# -*- coding: utf-8 -*-

NON_PRINTABLE_CHARS = [
    '\u200b',
    '\u2060',
    '\u2061',
    '\u2062',
    '\u2063',
    '\u2064',
    '\u2066',
    '\u2067',
    '\u2068',
    '\u2069',
    '\u206A',
    '\u206B',
    '\u206C',
    '\u206D',
    '\u206E',
    '\u206F',
    '\uFE06',
]

PRINTABLE_CHARS = '0123456789abcdef-'


def translate_uuid_to_invisible(u):
    inv = ''
    ustr = str(u)
    for i in range(len(ustr)):
        inv += NON_PRINTABLE_CHARS[PRINTABLE_CHARS.index(ustr[i])]

    return inv

def translate_uuid_from_invisible(inv):
    u = ''
    for i in range(len(inv)):
        u += PRINTABLE_CHARS[NON_PRINTABLE_CHARS.index(inv[i])]

    return u

def put_fingerprint(text, uuid):
    inv = translate_uuid_to_invisible(uuid)
    t = text[0] + inv + text[1:]
    return t

def get_fingerprint(t):
    if t[1] not in NON_PRINTABLE_CHARS:
        return ''

    inv = t[1:37]
    u = translate_uuid_from_invisible(inv)
    return u


if __name__ == '__main__':
    from uuid import uuid4

    txt = 'Olá, mundo!'
    u = uuid4()

    t = put_fingerprint(txt, u)
    fp = get_fingerprint(t)

    assert str(u) == fp

Claro que nunca funciona de primeira

Depois com mais testes percebi que eu tinha um problema: no whatsapp web, a depender do uuid, ficava uns espaços em branco no meio do texto, algo tipo o i. e isso por causa da combinação de caracteres. Apesar dos caracteres que eu escolhi não terem uma representação, eles tem uma função e a maioria deles eu nem sei qual a função.

Pra corrigir isso, eu primeiro tentei ir substituindo os caracteres por outros até que desse certo, mas é um trabalhão danado, sem change. Então eu precisava diminuir o número de caracteres usados pra ficar mais fácil a coisa de tirar os espaços do whatsapp web.

Menos (caracteres) é mais (espaço)

Um uuid é um número de 128 bits, então eu pensei em escrever isso em “binário”, aí eu só precisaria de dois caracteres, mas em contrapartida eu teria 128 caracteres a mais em cada mensagem. Nada é grátis, mas era mais importante o texto ficar “certo” do que o tamanho da mensage.

Escolhi os caracteres u200b (zero width space) e u2060 (zero width word joiner), alerei o código pra pegar a string com a representação do número do uuid em binário e simplesmente troquei 0 e 1 pelos caracteres zero witdh escolhidos. Altera o código, sobre rapidinho, testa, testa, testa… Bada bim! Bada bam! Bada bum! Dessa fez funcionou. Merge no master, pusha e vai! Daqui a uns minutinhos tá em prd!

O código alterado ficou mais ou menos assim:

# -*- coding: utf-8 -*-

from uuid import UUID

NON_PRINTABLE_CHARS = [
    '\u200b',
    '\u2060',
]

PRINTABLE_CHARS = '01'


def translate_uuid_to_invisible(u):
    inv = ''
    bitstr = f'{u.int:128b}'.replace(' ', '0')
    for i in range(len(bitstr)):
        inv += NON_PRINTABLE_CHARS[PRINTABLE_CHARS.index(bitstr[i])]

    return inv

def translate_uuid_from_invisible(inv):
    bitstr = ''
    for i in range(len(inv)):
        bitstr += PRINTABLE_CHARS[NON_PRINTABLE_CHARS.index(inv[i])]

    n = int(bitstr, 2)
    u = UUID(int=n)
    return u

def put_fingerprint(text, uuid):
    inv = translate_uuid_to_invisible(uuid)
    t = text[0] + inv + text[1:]
    return t

def get_fingerprint(t):
    if t[1] not in NON_PRINTABLE_CHARS:
        return ''

    inv = t[1:129]
    u = translate_uuid_from_invisible(inv)
    return u


if __name__ == '__main__':
    from uuid import uuid4

    txt = 'Olá, mundo!'
    u = uuid4()

    t = put_fingerprint(txt, u)
    fp = get_fingerprint(t)

    assert u == fp

Ainda tem problemas, 128 caracteres a mais pra um simples «oi» é feio, se eu printar isso num terminal ainda fica um espaço entre as letras, mas não é pra usar no terminal mesmo… E isso é uma jigajoga, não dá pra esperar muito, só que resolva o problema em mãos.

Moral da história: Não tem!


Uma passada de olhos em websockets

  • publicado em 31 de outubro de 2024

Esses dias implementando websockets no tupi-proxy e precisava de um cliente e um servidor websocket pra poder testar e ao invés de pegar algo pronto eu escrevi o que eu precisava. Então pra não ficar parecendo que foi um trabalho inútil, vou escrever sobre websockets agora.

Websockets são uma conexão tcp normal onde assim que a conexão é estabelecida o cliente envia os headers de uma requisição http, com os headers Upgrade: websocket e Connection: upgrade. Ao receber esses headers o servidor responde com esses mesmo headers indicando que suporta websockets. Depois disso o cliente e servidor podem trocar mensagens.

Mas como assim?

Bom, primeiro o servidor tem que estar escutando em uma porta, aí o cliente faz uma conexão tcp e manda uma requisição http GET para o servidor. Assim:

GET /ws HTTP/1.1
Host: myhost.net:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

O header Sec-WebSocket-Key enviado pelo cliente é uma string de 16 caracteres ascii aleatórios encodados em base64.

Ao receber essa requisição o servidor deve responder assim:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

O header Sec-WebSocket-Accept enviado pelo servidor é obtida da seguinte maneira: o servidor concatena o Sec-WebSocket-Key enviado pelo cliente com a string mágica «258EAFA5-E914-47DA-95CA-C5AB0DC85B11», gera um hash sha1 com essa string concatenada e por fim encoda em base64.

Depois desse processo de handshake o servidor e o cliente devem manter a conexão aberta e aí podem trocar mensagens usando o formato descrito na RFC 6455.

O formato das mensagens

As mensagens trocadas via websockets são chamadas de frames. Os frames enviados tanto pelo servidor quanto pelo cliente tem o seguinte formato:

0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

Essa coisa feia aí quer dizer que cada frame é formado da seguinte maneira:

  • Primeiro byte:
    • bit 0: Indica se o frame é um frame final de mensagem ou se a mensagem será continuada em outro frame

    • bits 1 a 3: Bits revervados à extensões.

    • bits 4 a 7: Opcode

  • Segundo byte:
    • bit 0: Indica se uma máscara está sendo usada

    • bits 1 a 7: O tamanho do payload

  • Próximos dois bytes:
    • O tamanho do payload se o tamanho no segundo byte for >= 126.

  • Próximos 8 bytes:
    • O tamanho do payload se o tamanho no segundo byte for == 127

  • Próximos 4 bytes:
    • A máscara se uma estiver sendo usada

O restante dos bytes (até o tamanho do payload) é o payload.

Opcodes e máscara

Os opcodes dão informação sobre o tipo do payload ou podem ser opcodes de controle. O opcode 0 indica que a mensagem é uma continuação da mensagem no frame anterior e o payload desse frame deve ser combinado com o payload do anterior; o opcode 1 indica que o payload é um texto encodado em utf-8; o opcode 2 indica que o payload é um binário; o opcode 8 é um opcode de controle usado para encerrar a conexão; o opcode 9 é um opcode de controle para ping e por fim o opcode 10 é um opcode de controle usado para pong.

A máscara são 32 bits aletórios que que vão encriptar os dados usando XOR. Os clientes obrigatóriamente devem usar máscara quando enviando dados pro servidor e o servidor não deve usar máscara quando enviando mensagens ao cliente.

Bom, é basicamente isso o protocolo de websockets. Agora ao que importa.

Uma implementaçãozinha

Primeiro uma implementação para wire encode e wire decode que vai ser usada tanto pelo cliente quanto pelo servidor.

// Frame é como a gente envia mensagens através do websocket.
// Essa struct representa aquele desenho feio lá de cima.
type Frame struct {
    Opcode   byte
    Len      uint
    Payload  []byte
    Mask     []byte
    IsFinal  bool
    IsMasked bool
}

// WebSocket contém as operações básicas do protocolo
// performadas tanto pelo cliente quanto pelo servidor.
// Note que WebSocket não tem um método para criar uma
// conexão já que a conexão sempre tem que ser criada
// pelo cliente e nunca pelo servidor
type WebSocket struct {
    Conn net.Conn
}

// Send wire encode um frame e envia os bytes em uma conxão
// já aberta
func (ws *WebSocket) Send(fr *Frame) error {
    data, err := ws.WireEncode(fr)
    if err != nil {
        return err
    }

    _, err = ws.Conn.Write(data)
    return err
}

// Recv lê da conexão aberta e retorna o frame recebido.
// Se Recv recebe um frame ping, envia um frame pong e
// volta a ler da conexão. Se recebe um frame close
// retorna um erro io.EOF.
// Note que Recv não fecha a conexão.
func (ws *WebSocket) Recv() (*Frame, error) {

    for {
        fr, err := ws.WireDecode()

        if err != nil {
            return &Frame{}, err
        }

        switch fr.Opcode {
        case OpcodeClose:
            return &Frame{}, io.EOF

        case OpcodePing:
            fr.Opcode = OpcodePong
            err := ws.Send(fr)
            if err != nil {
                return &Frame{}, err
            }

        default:
            return fr, nil

        }
    }
}

// RecvPayload retorna todo o payload da mensagem. Se a mensagem
// estiver divida em mais de um frame, lê todos os frames e
// só aí retorna o payload completo
func (ws *WebSocket) RecvPayload() ([]byte, byte, error) {
    var unfinishedPayload []byte
    unfinishedOpcode := byte(0xFF)
    for {
        fr, err := ws.Recv()
        if err != nil {
            return []byte{}, 0, err
        }
        if !fr.IsFinal {
            unfinishedPayload = append(unfinishedPayload, fr.Payload...)
            if unfinishedOpcode == 0xFF {
                unfinishedOpcode = fr.Opcode
            }
            continue
        }

        if fr.Opcode == OpcodeCont {
            unfinishedPayload = append(unfinishedPayload, fr.Payload...)
            return unfinishedPayload, unfinishedOpcode, nil
        }
        return fr.Payload, fr.Opcode, nil

    }
}

// Close manda um frame de controle close e fecha a conexão.
func (ws *WebSocket) Close() error {
    msg := []byte("close connection")
    fr := Frame{
        Opcode:  OpcodeClose,
        Payload: msg,
        Len:     uint(len(msg)),
        IsFinal: true,
    }
    ws.Send(&fr)
    return ws.Conn.Close()
}

// WireEncode transforma um frame em uma sequencia de bytes
// que vai ser enviada pela conexão.
// WireEncode não força o uso de máscara
func (ws *WebSocket) WireEncode(fr *Frame) ([]byte, error) {
    data := make([]byte, 2)

    if fr.IsFinal {
        // aqui se o frame for o frame final de uma mensagem
        // a gente seta o primeiro bit pra zero.
        data[0] = 0x00
    } else {
        // se não for um frame final a gente seta pra 1
        data[0] = 0x80
    }
    // os quatro últimos bits do primeiro byte são
    // o opcode
    data[0] |= fr.Opcode

    l := len(fr.Payload)

    if l <= 125 {
        // se o payload for menor que 126 bytes
        // o temanho será os últimos 7 bits do
        // primeiro byte
        data[1] = byte(l)

    } else if float64(l) < math.Pow(2, 16) {
        // se o tamanho do payload couber em dois bytes a gente
        // marca os sete últimos bits do segundo byte como 126
        // e marca o tamanho do payload nos próximos dois.
        data[1] = byte(126)
        s := make([]byte, 2)
        binary.BigEndian.PutUint16(s, uint16(l))
        data = append(data, s...)
    } else if float64(l) < math.Pow(2, 64) {
        // se o tamanho do payload cabe em oito bytes marcamos
        // nos próximos 8
        data[1] = byte(127)
        s := make([]byte, 8)
        binary.BigEndian.PutUint64(s, uint64(l))
        data = append(data, s...)
    } else {
        // muito grande. tem que dividir a mensagem em
        // mais de um frame
        return []byte{}, errors.New("Payload muito grande")
    }

    if fr.Mask != nil && len(fr.Mask) > 0 && len(fr.Mask) != 4 {
        return []byte{}, errors.New("Invalid mask")
    }
    if fr.Mask != nil && len(fr.Mask) == 4 {
        // Se uma mascara é usada setamos o primeiro bit
        // do segundo byte para 1 e fazemos o XOR no payload
        data[1] = 0x80 | data[1]
        data = append(data, fr.Mask...)
        xOR(fr.Payload, fr.Mask)
    }
    // e por fim o payload depois da tralha toda
    data = append(data, fr.Payload...)
    return data, nil
}

// WireDecode lê da conexão aberta e retorna o frame recebido.
// Aqui a gente tá basicamente fazendo o contrário do que fizemos
// em WireEncode
func (ws *WebSocket) WireDecode() (*Frame, error) {
    fr := Frame{}
    d := make([]byte, 2)
    _, err := ws.Conn.Read(d)
    if err != nil {
        return nil, err
    }

    // verificando se o primeiro bit é 0 ou 1 pra saber
    // se é um frame final. 0 == final
    final := (d[0] & 0x80) == 0x00

    // Pegando os últimos 4 bits do primeiro byte que
    // são o opcode
    opcode := d[0] & 0x0F

    // Primeiro byte indica se tá usando máscara ou não
    // 1 == tá usando
    isMasked := (d[1] & 0x80) == 0x80

    // os 7 últimos bits do segundo byte pro tamanho do
    // payload. Se for <= 125 já será o tamanho real
    len := d[1] & 0x7F
    l := uint(len)

    fr.Opcode = opcode
    fr.IsFinal = final
    fr.IsMasked = isMasked

    if l == 126 {
        // se o marcado no segundo byte é 126 então o tamanho
        // está nos próximos dois bytes
        d := make([]byte, 2)
        _, err := ws.Conn.Read(d)
        if err != nil {
            return nil, err
        }
        l = uint(binary.BigEndian.Uint16(d))
    } else if l == 127 {
        // se o marcado no segundo byte é 127 então o tamanho
        // está nos próximos 8 bytes
        d := make([]byte, 8)
        _, err := ws.Conn.Read(d)
        if err != nil {
            return nil, err
        }
        l = uint(binary.BigEndian.Uint64(d))
    }

    fr.Len = l

    mask := make([]byte, 4)
    if isMasked {
        // se tá usando máscara, os próximos 4 bytes serão
        // a máscara.
        _, err = ws.Conn.Read(mask)
        if err != nil {
            return nil, err
        }
    }

    // e por fim o payload do frame
    payload := make([]byte, l)
    _, err = ws.Conn.Read(payload)

    if isMasked {
        xOR(payload, mask)
        fr.Mask = mask

    }
    fr.Payload = payload
    return &fr, nil
}

Agora o código do websocket client:

// WebSocketClient é quem inicia a conexão de websocket.
type WebSocketClient struct {
    WebSocket
    URL *url.URL
}

// Handshake envia uma requisição http com headers upgrade
// perguntando se o servidor suporta websockets
func (ws *WebSocketClient) Handshake() error {
    // O hash aqui são 16 caracteres ascii aleatórios encodados
    // em base64
    hash := getSecHashClient()
    req := &http.Request{
        URL:    ws.URL,
        Header: make(http.Header),
    }

    req.Header.Set("Upgrade", "websocket")
    req.Header.Set("Connection", "upgrade")
    req.Header.Set("Sec-WebSocket-Accept", hash)

    err := req.Write(ws.WebSocket.Conn)
    if err != nil {
        return err
    }
    reader := bufio.NewReaderSize(ws.Conn, 4096)
    resp, err := http.ReadResponse(reader, req)
    if err != nil {
        return err
    }

    // O status que o servidor deve retornar informando
    // que suporta websockets é o status 101
    if resp.StatusCode != http.StatusSwitchingProtocols {
        return errors.New("Server does not support websockets")
    }

    if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
        strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
        return errors.New("Invalid response")
    }
    return nil
}

// Send envia um frame ao servidor e antes de enviar
// gera uma máscara para o frame
func (ws *WebSocketClient) Send(fr *Frame) error {
    fr.Mask = getMask()
    return ws.WebSocket.Send(fr)
}

// NewWebSocketClient retorna um cliente de websocket já
// connectado a um servidor que suporta websockets
func NewWebSocketClient(rawURL string) (*WebSocketClient, error) {
    u, err := url.Parse(rawURL)
    if err != nil {
        return &WebSocketClient{}, nil
    }

    hostPort, err := getHostPort(u)
    if err != nil {
        return &WebSocketClient{}, err
    }

    conn, err := net.Dial("tcp", hostPort)
    if err != nil {
        return &WebSocketClient{}, err
    }
    ws := WebSocketClient{
        WebSocket: WebSocket{
            Conn: conn,
        },
        URL: u,
    }

    err = ws.Handshake()
    if err != nil {
        return &WebSocketClient{}, err
    }

    return &ws, nil
}

Agora o server que a única coisa que faz é retornar o que o cliente mandar, mas se for um texto retorna o texto invertido:

// WebSocketServer responde a uma conexão feita pelo cliente.
type WebSocketServer struct {
    WebSocket
    Header http.Header
}

// Handshake retorna status 101 indicando que aceita websockets
func (ws *WebSocketServer) Handshake() error {
    secKey := ws.Header.Get("Sec-WebSocket-Key")
    hash := getSecHashServer(secKey)
    headers := []string{
        "HTTP/1.1 101 Switching Protocols",
        "Upgrade: websocket",
        "Connection: upgrade",
        "Sec-WebSocket-Accept: " + hash,
        "",
        "",
    }
    _, err := ws.Conn.Write([]byte(strings.Join(headers, "\r\n")))
    return err
}

// Recv retorna o frame enviado pelo cliente. Se o cliente enviar
// um frame sem máscara Recv retorna um erro já que o cliente
// sempre tem que usar uma máscara
func (ws *WebSocketServer) Recv() (*Frame, error) {
    fr, err := ws.WebSocket.Recv()

    if err != nil {
        return fr, err
    }
    if !fr.IsMasked {
        return &Frame{}, errors.New("Clients must mask the payload")
    }
    return fr, err
}

// Echo simplesmente retorna a mensagem enviada pelo cliente
// invertendo a string se o payload for utf-8.
func (ws *WebSocketServer) Echo() error {
    for {
        payload, opcode, err := ws.RecvPayload()
        if err != nil && errors.Is(err, io.EOF) {
            log.Println("Connection closed")
            return nil
        }

        if err != nil {
            log.Println(err.Error())
            return err
        }

        if opcode == OpcodeText {
            runes := []rune(string(payload))
            pl := len(runes)
            reversed := make([]rune, pl)
            for i := pl - 1; i >= 0; i-- {
                j := (pl - 1) - i
                reversed[j] = runes[i]
            }
            payload = []byte(string(reversed))
        }

        fr := Frame{
            Payload: payload,
            Opcode:  opcode,
        }
        err = ws.Send(&fr)
        if err != nil {
            log.Println(err.Error())
            return err
        }
    }
}

E por fim pra testar as coisas tudo junto a gente faz um http handler e uma cli.

func wsCli() {

    ws, err := NewWebSocketClient("ws://localhost:8081")
    if err != nil {
        panic(err.Error())
    }

    var msg string
    for {
        fmt.Print(": ")
        reader := bufio.NewReader(os.Stdin)
        msg, err = reader.ReadString('\n')
        if err != nil {
            panic(err.Error())
        }

        frame := Frame{
            Payload: []byte(msg),
            IsFinal: true,
            Opcode:  OpcodeText,
        }
        ws.Send(&frame)
        resp, err := ws.Recv()
        if err != nil {
            panic(err.Error())
        }
        fmt.Printf(string(resp.Payload) + "\n")
    }

}

func wsHandler(w http.ResponseWriter, r *http.Request) {
    h, ok := w.(http.Hijacker)
    if !ok {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    conn, _, err := h.Hijack()
    if err != nil {
        log.Println(err.Error())
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    ws := WebSocketServer{
        WebSocket: WebSocket{
            Conn: conn,
        },
        Header: r.Header,
    }

    defer ws.Close()

    err = ws.Handshake()
    if err != nil {
        log.Println(err.Error())
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    err = ws.Echo()
    if err != nil {
        log.Println(err.Error())
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
}

A main function fica assim:

func main() {
    server := flag.Bool("server", false, "start the server")
    client := flag.Bool("client", false, "start the client")

    flag.Parse()

    if !*server && !*client {
        panic("one of server or client must be true")
    }

    if *server && *client {
        panic("only one of server and client can be true")
    }
    if *server {
        log.Fatal(http.ListenAndServe(":8081", http.HandlerFunc(wsHandler)))
    } else {
        wsCli()
    }
}

Agora só compilar assim:

$ go build -o ws ws.go

Inicie o servidor assim:

$ ./ws -server

E agora você pode usar o cliente para falar com o servidor via websockets

$ ./ws -client
: olá, mundo

odnum ,álo
:

O código completo pode ser baixado aqui.

E é isso!


Emacs é o editor mais legal

  • publicado em 19 de outubro de 2024

A beleza do Common Gateway Interface

  • publicado em 02 de outubro de 2024

Aqueles que já estão se aproximando da meia-idade vão se lembrar dos cgi scritps. Eles foram a primeira maneira de se fazer páginas dinâmicas por http, mas quando eu comecei a trabalhar como programador, cgi scripts já eram considerados ultrapassados, uma coisa que não se faz mais.

O fora de moda pode ser bom

Esses dias eu tava dando manutenção em um projetinho (não pergunta o porquê d’eu manter isso até hoje…) e queria subir pra vps nova. O problema era que eu usava o mod_perl do apache para servir via http. E aí eu precisaria instalar o apache que não uso pra nada dependências no meu código. Eu não tava nem um pouco feliz com isso, queria só rodar um script.

Foi aí que me lembrei do nosso bom e velho cgi. Aí implementei um plugin cgi para o tupi e é isso. Agora não preciso de nenhuma dependência, meu cgi script só precisa ler variáveis de ambiente, ler o stdin e o mandar o retorno pro stdout. Simples e bonito!

Nem tudo são flores

Apesar de serem legais, cgi scripts caíram em desuso por alguns motivos. Cada chamada cria um novo processo, os scripts precisam todos parsear o corpo por si só e por aí vai. Mas pelo menos no caso de um processo por chamada, num tempo onde tem muita gente usando esses serverless que sobe uma insância nova só pra rodar uma função, o que é um processinho comparado à isso?


Plugins in go (lang, não horse)

  • publicado em 05 de julho de 2024

Eu gosto bastante da ideia de programas que potencialmente podem ser a junção e peças menores, unix pipes, serviços remotos e coisas do tipo e permitem escrever programas menores que se comunicam via uma interface definida. Nessa mesma linha, plugins podem ser bem úteis extendendo um programa sem que o programa principal saiba do que está acontecendo.

Em go existe o package plugin que permite escrever e carregar plugins dinamicamente. Basicamente o que precisa ser feito é definir o que o plugin precisa implementar (uma ou mais funções) e o programa principal carrega o plugin em tempo de execução e executa as funções implementadas pelo plugin.

O programa principal

A primeira coisa que precisamos é de um programa que seja capaz de carregar e executar plugins. Este será o programa principal que será extendidos pelos plugins. Para carregar o plugin se usa plugin.Open e plugin.Plugin.Lookup.

Então comecemos com um programa bem simples, sem suporte a plugins:

package main

import (
    "flag"
    "os"
)

func main() {
    action := flag.String("action", "faz", "")
    flag.CommandLine.Parse(os.Args[1:])

    s := "coisa"

    println(*action + " " + s)
}
$ ./programa
faz coisa
$./programa -action outra
outra coisa

Nada muito o que explicar aqui, né? Então agora vamos implementar o suporte a plugins.

A ideia agora é que a gente vai usar plugins baseados no parâmetro action que o usuário passar.

package main

import (
    "flag"
    "fmt"
    "os"
v    "plugin"
)

func main() {
    action := flag.String("action", "faz", "")
    flag.CommandLine.Parse(os.Args[1:])
    s := "coisa"

    var f plugin.Symbol
    var fn func(string)
    var ok bool

    // Aqui plugin.Open recebe o caminho do plugin.
    // A gente vai procurar por um plugin baseado
    // na action que o usuário passou
    fpath := fmt.Sprintf("./%s_plugin.so", *action)
    p, err := plugin.Open(fpath)

    if err != nil {
        // Olha esse goto maroto engolindo os bug tudo!
        goto PRINT
    }

    // Aqui a gente procura no plugin por um símbolo
    // com o nome de Action
    f, err = p.Lookup("Action")
    if err != nil {
        goto PRINT
    }

    // Aqui verifica se o símbolo é realmente do tipo
    // que a gente espera
    fn, ok = f.(func(string))
    if !ok {
        goto PRINT
    }

    fn(s)
    return

PRINT:
    println(*action + " " + s)

}

Por enquanto como não temos nenhum plugin nosso programa continua fazendo a mesma coisa:

$./programa
faz coisa
$./programa -action outra
outra coisa

O plugin

Para implementar o plugin a gente precisa só implementar uma função chamada Action que recebe uma string como parâmetro:

package main

func Action(s string) {
    println("Aqui o plugin fazendo outra " + s)
}

Agora o plugin precisa ser compilado com a flag -buildmode=plugin

go build -buildmode=plugin outra_plugin.go

E agora nosso programa  pode usar o plugin:
$./programa
faz coisa
$./programa -action outra
Aqui o plugin fazendo outra coisa

E é isso!


Próximo