Skip to main content

Thiago Lages de Alencar

Crawler: Responsável por navegar entre websites utilizando links encontrados neles mesmos
Scraper: Responsável por extrair informações importantes dos websites navegados

Crawling é essêncial para scraping, pois você não tem como conseguir extrair informações de um site sem navegar para ele antes.
Scraping não é essêncial para crawling, pois os dados do site podem ser irrelevantes para você.

Por exemplo:

  • Google utiliza um crawler para navegar a internet e indexar páginas delas, porém não extrai as informações dos websites em si
  • OpenAI utiliza scraper para pegar os videos do youtube e utilizar na inteligência artificial deles

Se eu tivesse que separar crawlers em categorias, seria:

  • Browser: Utilizando um browser real para crawlear
  • Raw: Simulando um browser atráves de requisições pela internet

Hoje em dia simular um navegador é incrivelmente difícil então a maneira raw é bem menos potente e fornece muito menos funcionalidades.

Browser

Existem 3 ferramentas famosas de automação de navegadores:

É importante notar que elas todas se declaram para usos de testes, porém ainda assim são muito utilizadas para web scraping.

Selenium

Criado em ~2004 mas que continua a oferecer suporte para todos os navegadores e diversas linguagens (não necessariamente bem).

Muito do seu estilo vem do fato de ter sido criado utilizando Java e depois adaptado para outras linguagens.

javascript
import { Browser, Builder, By } from "selenium-webdriver";

const driver = await new Builder().forBrowser(Browser.CHROME).build()
await driver.get('https://www.etiquetaunica.com.br/')
await driver.manage().setTimeouts({implicit: 1000});

let hamburguerButton = await driver.findElement(By.xpath('//*[@id="headerWrapper"]/div[1]/button[1]'))
await hamburguerButton.click()

let brandButton = await driver.findElement(By.xpath('//*[@id="menu"]/div/div[2]/ul/li[7]/a'))
await brandButton.click()

Puppeteer

Criado pela Google em ~2017 para fornecer automação ao Google Chrome utilizando JavaScript.

Note que o locator favorito deles envolve utilizar CSS.

javascript
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({headless: false})
const page = await browser.newPage()

await page.setViewport({ width: 1600, height: 1024 })
await page.goto('https://gringa.com.br/')
await page.locator('#section-header > div > div.Header__FlexItem.Header__FlexItem--fill.Header__FlexItem_left--mobile > nav > ul > li:nth-child(3) > a').click()

Playwright

Criado pela Microsoft em ~2020 para fornecer automação em diversos navegador. O estilo é bem parecido ao do Puppeteer pois boa parte dos desenvolvedores vieram do Puppeteer 🤣.

A linguagem primária de programação dele é JavaScript, porém fornece suporte a diversas outras (não necessariamente bem).

javascript
import { chromium, devices } from "playwright";

const browser = await chromium.launch({ channel: 'chrome', headless: false })
const context = await browser.newContext(devices['Desktop Chrome'])
const page = await context.newPage()

await page.goto('https://canseivendi.com.br/')
await page.getByRole('link', {name: 'Marcas'}).click()

Raw

Neste caso o mais importante é você possuir uma boa quantidade de bibliotecas que o ajudem a realizar a tarefa, porém o básico é conseguir requisitar a página na internet e parsear o conteúdo HTML.

python
import httpx
from parsel import Selector

USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"

response = httpx.get(
"https://canseivendi.com.br/bolsas",
headers={"user-agent": USER_AGENT},
)

selector = Selector(text=response.text)
names = selector.xpath('//div[@class="name"]/a/text()').getall()
links = selector.xpath('//div[@class="name"]/a/@href').getall()
prices = selector.xpath('//div[@class="cash"]/text()').getall()

Note que navegadores fazem muito mais que apenas uma requisição, pois uma página pode envolver fazer diversas requisições (por imagens, videos, códigos javascripts, etc).

Thiago Lages de Alencar
note

Pesquisa limitada pelo fato de Twitter ainda estar bloqueado no Brasil.

Aproveitando que eu estava olhando o drama em cima de Godot e assistindo Ace Attorney... Resolvi brincar com o site https://objection.lol.

Video

Details

Twitter

Discord

Godot Foundation

Website

jun-17

jul-08

oct-11

Fork

SMS

Docs

Twitter++

BlueSky

Google

Discord++

Youtube

Thiago Lages de Alencar

Está é uma lista com minha opnião sobre diversas ferramentas de terminal.
Aqueles marcados com estrela (⭐) são ainda utilizados por mim.

CLI

  • nala
    • Substitui: apt
    • Uso diário: Alto
    • Nota: Torna muito melhor a visualização e entendimento da instalação de programas
  • duf
    • Substitui: df
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois acabo olhando em GUIs essas informações
  • dust
    • Substitui: du
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois acabo olhando em GUIs essas informações
  • fastfetch
    • Substitui: neofetch
    • Uso diário: Baixo
    • Nota: Providência mais informação e o projeto não foi abandonado
  • jq
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois trato JSON por código

TUI

  • bottom
    • Substitui: htop, top
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois é muito chato ter que lembrar de todos os atalhos, acabo voltando para htop
  • superfile
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Nunca uso pois dificilmente tenho necessidade de usar file manager no terminal
  • yazi
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Nunca uso pois dificilmente tenho necessidade de usar file manager no terminal
  • micro
    • Substitui: nano
    • Uso diário: Alto
    • Nota: Muito mais parecido com um editor de texto do dia a dia

Shells

  • fish
    • Substitui: bash
    • Nota 1: Providência uma ótima experiência logo de cara
    • Nota 2: Sintaxe melhor que bash porém ainda não da vontade de aprender
  • nushell
    • Substitui: bash, fish
    • Nota 1: Providência uma ótima experiência logo de cara
    • Nota 2: Sintaxe muito mais agradável
    • Nota 3: Remove a necessidade de possuir jq
    • Nota 4: Remove a necessidade de possuir curl pois possui o comando http

Em shells "uso diário" é 100% de quando você utilizar o shell, então sempre é alto.

Prompt

  • Starship
    • Opnião 1: Permite grande costumização do prompt com facilidade, tornando o prompt mais agradável
    • Opnião 2: Quando lidando com Git possue um peso considerativo

Em prompts "subsititui" é sempre sobre substituir o padrão.
Em prompts "uso diário" é 100% de quando você utilizar o shell, então sempre é alto.

References

Thiago Lages de Alencar

Imagem mostrando camada do App, OS e Hardwares

Quando você quer ler o arquivo, você pede ao sistema operacional pelo conteúdo do arquivo.

Quando você quer escrever no arquivo, você pede ao sistema operacional para inserir o conteúdo no arquivo.

É importante saber que o sistema operacional toma diversos cuidados para que desenvolvedores não acessem diretamente o hardware, ou seja, por baixo dos panos você está pedindo para o sistema operacional ler/escrever.

  • C
    • fgets()
    • fwrite()
  • Python
    • file.read()
    • file.write()
  • Rust
    • file.read_to_string()
    • file.write_all()
  • Go
    • os.ReadFile()
    • os.WriteFile()

Veremos como garantir a segurança de um arquivo quando se tem múltiplos processos querendo altera-lo.

Process

A função utilizada para se criar processos é fork(), está função faz com que o atual processo crie um processo filho quase idêntico e executando o mesmo código que o pai.

Olhe este código que printa duas vezes "Hi":

#include <stdio.h>
#include <unistd.h>

int main() {
fork();
printf("Hi\n");
}

Se você executa-lo irá notar que o filho é tão igual ao pai que ele continua exatamente do mesmo local que o pai se encontrava (logo após fork() retornar um valor). Se tivessemos variáveis, poderiamos ver que até o valor delas são idênticos ao do pai.

No entanto, precisamos de uma maneira de reconhecer quem é o pai e filho, caso contrário este código executária exatamente a mesma coisa para ambos (não seria nada produtivo). Acontece que a função fork() retorna um valor e este valor é utilizado para sabermos se estamos no pai ou no filho.

#include <stdio.h>
#include <unistd.h>

int main() {
int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
printf("I'm the child process\n");
} else {
printf("I'm the parent process\n");
}
}

A função fork() vai retornar ao pai o PID do filho (ou -1 em caso de error).
A função fork() vai retornar ao filho zero.

tip

Normalmente o código do pai e filho são inseridos em funções em vez de deixar tudo dentro de um if/else.

if(pid == 0) {
child_code();
} else {
parent_code();
}

Ou se utiliza funções exec para transformar completamente o código executado naquele processo.

Problem

Imagem ilustrativa do app 1 lendo do arquivo, app 2 lendo do arquivo, app 1 escrevendo no arquivo, app 2 escrevendo no arquivo

Quando dois processos interagem com o mesmo arquivo, pode acontecer da informação ser preenchida incorretamente? Afinal, precisamos primeiramente descobrir se isso é possível ou não de acontecer.

Como o escalonamento pode ser imprevisivel, uma maneira de testar se durante a interação com um arquivo houve troca de processo é repetindo a ação diversas vezes e ver se pelo menos uma vez ocorreu.

O seguinte código irá ser executado para o processo pai e filho:

int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);

O código irá garantir que o cursor está no início do arquivo, ler o atual número do arquivo, mover o cursor para o início do arquivo e sobreescrever o número.

note
fprintf(file, "%d     ", ++i);

Por que inserir espaço após o número? Foi uma maneira de evitar que o número de ambos processos se misturem.

Por exemplo: Processo 1 escreve 5000 e processo 2 escreve 9, o arquivo irá conter "9000" pois o 9 foi escrito em cima do 5.

Agora só precisamos adicionar a lógica de criar processo vista anteriormente:

#include <stdio.h>
#include <unistd.h>

void code() {
int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);
}

int main() {
FILE* file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

Quando executei este código para 10 iterações, o valor final do arquivo foi 20.
Quando executei este código para 1000 iterações, o valor final do arquivo foi 1000.
Quando executei este código para 10000 iterações, o valor final do arquivo foi 10015.

O que somos capaz de deduzir com isto?

  • O resultado é imprevisível pois não temos controle de quando o escalonador vai trocar os processos
  • Dependendo do volume de iterações e da máquina do usuário, um processo pode ou não conseguir fazer a tarefa antes do escalonador trocar o processo
  • Se houver troca durante uma tarefa, pode corromper o resultado do arquivo

Quais as chances disto acontecer? Depende do software, pois existem arquivos que a chance de dois softwares interagirem ao mesmo tempo é 0%.

Locks

Acontece que existe mais de uma maneira de aplicar locks no Linux.

Uma pessoa bem surpresa

  • flock
    • file lock
    • Origem do sistema operacional BSD
  • lockf
    • lock file
    • POSIX
    • É uma versão simplificada do fcntl "Advisory record locking"
  • fcntl "Advisory record locking"
    • file control
    • POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Advisory record locking"
  • fcntl "Open file description locks (non-POSIX)"
    • file control
    • Linux specific
      • Existem propostas para ser adicionado ao padrões POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Open file description locks (non-POSIX)"

Se quiser saber mais sobre cada um, é bom ler o post no blog: https://gavv.net/articles/file-locks/
Incrivel como um blog de 8 anos atrás me ajudou mais do que pesquisas no Google.

flock

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

flock(fd, LOCK_EX);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
flock(fd, LOCK_UN);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

lockf

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

lockf(fd, F_LOCK, 0);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
lockf(fd, F_ULOCK, 0);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

fcntl - "Advisory record locking"

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
struct flock fl;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;

while (count < 10000) {
int i;

fl.l_type = F_WRLCK;
fcntl(fd, F_SETLKW, &fl);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &fl);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

References

Extra

Por diversão, eu experimentei replicar o mesmo problema em outras linguagens.

package main

import (
"fmt"
"os"
"os/exec"
)

func code() {
var count int = 0
file, _ := os.OpenFile("example.txt", os.O_RDWR, 0666)

for count < 10000 {
var i int

file.Seek(0, 0)
fmt.Fscanf(file, "%d", &i)
file.Seek(0, 0)
fmt.Fprintf(file, "%d ", i+1)

count++
}

file.Close()
}

func main() {
if len(os.Args) >= 2 {
code()
fmt.Println("Child finished")
} else {
os.WriteFile("example.txt", []byte{'0'}, 0600)

command := exec.Command("/usr/bin/go", "run", "main.go", "fork")
command.Start()
code()
command.Wait()

fmt.Println("Parent finished")
}
}

Thiago Lages de Alencar

My english is trash :v

Remember the old days where we would immerse ourselfs while reading books?

I don't... I suck at books.

Person laughing

But you know the feeling, right? We just have to change books for movies/games/sports...

Pretty sure that after the title and text above, you can guess everything that I'm going to write about (if you reflect deep enough, you don't even have to read this post...).

Storytelling

What do theaters, movie theaters and libraries have in common? When you make noise, people go "Shh".

Person making &quot;shhhh!&quot;

I'm joking... They are storytelling places.

Where you go to read, watch, see, or hear a predetermined story. The author tries to build a path for how you should feel in that storytelling (sometimes is not even close).

Video games are storytelling? If you put a story in it... Yes! They may not be the best storytelling because it's hard to control what players feel, but that's why people insert cinematics in games.

Cinematics are a way of telling a story without letting you ruin everything (I'm being dramatic). Just think about what stupid thing players could if you let them free in the cinematic (probably throw themselves from a high place... I don't know).

People love to test the boundaries of the games or try to find something that the author didn't count on.

Anyway, I'm getting off track. It's harder than movies and books because it's not just storytelling, but it's still possible to do a great job at it.

info

"It's not just storytelling"

Not saying that storytelling is easy, just that when you have to count everything that the player can do... Telling a good story becomes difficult.

Think about a great book or movie that somebody attempted to transform into a game... It's really hard to tell the same story. The developer may want them to feel like a great hero, but maybe they are pissed after dying 20 times to the same boss.

¯\_(ツ)_/¯

Immerse our selfs

Remember when I joked about places where people "Shh" you? I wasn't joking.

Person watching a monitor/tv

Is kind of obvious that people "Shh" in those places because it's really hard to immerse in something, but it's very easy to lose focus.

When I say "immerse", think about the situation where you are so focused on something that you ignore somebody calling your name.

Obviously, is important for storytelling games (except comedy, which is a free ticket to "f*ck it" town), but it's also important for competitive games! You don't have to immerse yourself in feelings but in the game mechanics (thinking about everything that others players can do).

That's probably why we get more angry or sad after losing a competitive game where we invest ourselfs to win. Over time, we learn to manage these feelings better, but it's still not a happy feeling.

note

"I don't get sad or angry"

Err... Not even fustrated? Are you sure that you were invested in the game? Maybe you just didn't care about the game.

Attention problem

Attention is a limited resource... We can't make 20 tasks at the same time and get the same quality of making one with all our attention (exceptions exist, but I don't think you are one of them).

Person being distracted
(It was supposed to be someone getting distracted and dying in the game...)

A classic example is your parents trying to talk to you while you are playing. In this cases, you attempt to multitask between playing and answering them (if you are like me, you are probably dead in the game).

In case of games/movies/books storytelling the same can happen... How can you get in the horror game mood of "I'm going to die, I want to run" if you have to stop to do something for your parents just before the scary moment?

note

Stop blaming the player, is also devs fault.

You want me to blame developers? Okay, it's also the developers fault for not making great storytelling that locks you into the screen... But this post is more about our problem, I can't just start badmouthing developers now (this post would get BIG and I don't have time to complain about everyone (I actually have... I'm unemployed... Sad...)).

Streamers

Streamer playing

"Making games for streamers seems hard as f***"

How do you capture the attention of someone who may stop to read others people messages and thinking about an answer to them?

How do you reproduce into another the feeling that you had when children where you would play in front of TV without noticing your parents calling you?

How do you tell about the struggles of your character if the other person is busy reflecting their life to other people?

Streamers are not "just" players, they are entertainment too! This means that they need to share their attention with their viewers.

  • Look at the chat to check if the stream didn't go offline
  • Interact with the chat to show that you cares about them
  • Thanks the subscrpitions and donations
  • Express what is on their mind while playing because the viewers want to know

I'm not here to blame anyone, just pointing the difficulties.

Compared to other options for playing games:

  • Playing the game alone in the room
    • You don't have to say a word
  • Recording a video for Youtube
    • People normally want to know what is on your mind
    • I didn't reflect enough on this so I don't have another point... Sorry Youtubers

Conclusion

I don't have any...

I don't know how developers attack this... They invest less in long immersive moments? They do more short content? They always assume that the player is a streamer? I mean, you can't just ignore the fact that a streamer could badmouth your game (this can affect sales, right?).

note

It's always good to see that streamers know that this is something that can happen.

https://youtu.be/5LtQAcGxQlM?t=193

Thiago Lages de Alencar
warning

Este post parte do princípio que o leitor viu o post anterior!

Além disto este post for reescrito pois o formato não estava do meu agrado (igual ao post anterior).

O repositório com exemplos continua vivo em:
https://github.com/thiagola92/learning-authentication

A lógica básica é a mesma que o post anterior, porém usando o email no lugar do username.

UI de login com email e password

O que temos de extra é que podemos utilizar o email do usuário para nos dar mais confiança que a pessoa acessando é o dono da conta.

Register

Client

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário).

Server

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário) porém não cadastramos o usuário ao final!

Carta representando um email

O motivo é bem simples: O email fornecido é realmente da pessoa que cadastrou a conta?

Imagina se nós terminassemos o cadastro acreditando naquele email... Dia seguinte aquele email poderia receber emails nosso, achar super esquisito e nos marcar como spam. Se isso acontecesse muito o Google iria acabar marcando nosso email corporativo como spam!

Para resolver esse problema basta cobrarmos uma prova que a pessoa é a dona do email. Podemos fazer isso enviando um email para ela e esperando que ela nos diga algo que possue no email.

note

Partindo do princípio que a pessoa dona do email é a única que deveria ter acesso ao email.

O modo clássico é gerar um código no servidor e enviar para o email da pessoa este código, agora ela precisa acessar o email e nos dizer o código para provar que ela conseguiu o código certo.

Visão geral do processo

Email é bom pois nós não precisamos ser dependentes de terceiros para utilizar (Gmail/Outlook/etc).

import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["Subject"] = "Confirmation code"
message["From"] = "noreply@yourwebsite.com"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

# I'm running my own email server for tests
with smtplib.SMTP("localhost", 8025) as s:
s.send_message(message)
info

Utilizei a biblioteca aiosmtpd para criar o server de test:
python -m aiosmtpd -n

Se tudo foi feito corretamente, todo email novo deve aparecer lá no formato:

---------- MESSAGE FOLLOWS ----------
Subject: Confirmation code
From: noreply@yourwebsite.com
To: user@example.com
Content: Your confirmation code is 123
X-Peer: ('127.0.0.1', 52020)

------------ END MESSAGE ------------

Porém o exemplo acima é inseguro, pois não utiliza a SLL (que ajuda a criar uma camada de segurança entre nós e o serviço). Hoje em dia isso é obrigatório para interagir com quase todos os serviços de email (eles não vão aceitar emails sem essa segurança).

import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["Subject"] = "Confirmation code"
message["From"] = "noreply@yourwebsite.com"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

with smtplib.SMTP("localhost", 8024) as s:
s.starttls()
s.send_message(message)
info

Levantei outro server de email e nele estou utilizando TLS.

Criei meu certificado auto-assinado:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'

E utilizei ele durante a inicialização do server:
python -m aiosmtpd -n --tlscert cert.pem --tlskey key.pem -l localhost:8024

Então é isso? Podemos mandar email para provedores como Gmail? Nope.

Rosto chorando

Deve ser bem óbvio que embora este código funcione localmente, ele jamais passaria por qualquer segurança do nível de aplicação.

Ele possui TLS para proteger contra man-in-the-middle mas como que o provedor vai saber que nós somos os donos daquele email? Se nós botarmos noreply@gmail.com, nós temos que provar que temos acesso à aquela conta no Gmail.

A maneira clássica é logar na conta.

import smtplib
from email.message import EmailMessage

# Removed "From" because providers will use the email used in s.login()
message = EmailMessage()
message["Subject"] = "Confirmation code"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

with smtplib.SMTP("localhost", 8024) as s:
s.starttls()
s.login("noreply@gmail.com", "password")
s.send_message(message)

Este código não vai funcionar!

Hoje em dia já sabemos que usuário e senha sozinhos não providênciam uma segurança forte durante a autenticação, por isso que Google nos cobra outros métodos de MFA para provar que nós somos nós mesmos durante os logins (telefone, passkeys, authenticators, ...).

Google requer que você explicitamente adicione uma senha para aquele "app" e mesmo assim ele não recomenda fazer isto!
Link para a resposta no StackOverflow

Bem, vamos parar por aqui pois eu apenas tenho interesse em ver o conceito de segurança com o email (não quero ensinar a se autenticar em diversos provedores com email).

Client 2

Existe duas opções aqui, a primeira é o server ter armazenado numa tabela temporária seu email, código enviado por email e hash da senha.

emailcodehash
thiagola92@email.com48935e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
darklord@email.com38922d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25
juninho@email.com0283b7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c

Neste caso basta o usuário enviar o código para o server validar a conta.

import httpx
from urllib.parse import urlencode

body = urlencode({"code": code})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register/code", headers=headers, content=body)
note

Embora eu tenha feito como um requisição POST, não existe nenhuma obrigação de ser assim.

Uma maneira mais familiar é o usuário receber uma URL com o código/token para o usuário acessar e isto seria o suficiente para confirmar.

http://127.0.0.1:8000/register/{code}

A outra é ele não ter armazenado o hash e só esperar que você envie a senha novamente.

emailcode
thiagola92@email.com4893
darklord@email.com3892
juninho@email.com0283
import httpx
from urllib.parse import urlencode

body = urlencode({"email": email, "password": password, "code": code})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register/code", headers=headers, content=body)
note

Em ambas as tabelas é normal de ter a data de criação do código, para que ele não fique válido para sempre (não queremos que ninguém chute todas as opções de código).

Server 2

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário).

Recovery

Client

A grande vantagem de ter email é adicionar um ponto de recuperação de senha.

Usuário se perguntando qual a senha

Dado que o usuário esqueceu a senha, ele só precisa requisitar o resete de senha da conta relacionada a aquele email.

import httpx
from urllib.parse import urlencode

body = urlencode({"email": email})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/recovery", headers=headers, content=body)

Como estamos falando do caso em que o usuário não lembra a senha, não podemos cobrar nenhuma autenticação... Em outras palavras, qualquer pessoa má intencionada pode ficar requisitando e resta ao server tomar cuidados para elas não conseguirem acesso.

Server

É a mesma ideia do login, queremos confirmar que é o dono da conta então mandamos para o email dele um código/token para ele utilizar na alteração da senha atual dele.

Lembrando que é importante botarmos tempo para o usuário fazer essa alteração e limite de tentativas, pois não queremos que usuários má intencionados usem força bruta para descobrir o código/token.

Client 2

Basicamente idêntico ao registrar, porém temos que fornecer a nova senha. Isto pode ser feito tudo em uma requisição ou em duas (acessar o URL com token e inserir a nova senha em um form).

Server 2

Valide o código/token!

Carimbo

Cada tentativa errada é um sinal de perigo, por isso é importante lembrar:

  • Invalidar o código/token depois de certo tempo
    • Por isto alguns tokens são grandes, pois nunca é possível chutar todas as possibilidades antes do tempo esgotar
  • Contar o número de tentativas de acertar o código/token para um email
    • Importante invalidar o código depois de certo número de tentativas, não queremos X máquinas tentando acertar o código

References

Thiago Lages de Alencar
warning

Eu não estava feliz que o post era muito "aqui o código de X/Y/Z", pois gerava paredão de texto. E quando escrevo estes posts, a intenção é reforçar conhecimento para no futuro eu conseguir voltar e relembrar do assunto (e não do código em específico).

O repositório com exemplos continua vivo em:
https://github.com/thiagola92/learning-authentication

Username com password é uma das maneiras mais velhas de se criar autenticação no seu sistema.

UI para login

Nós confiaremos que aquele usuário é o dono da conta se ele souber a senha que está relacionada àquele username. Note que falei "confiaremos" pois nunca podemos ter 100% de certeza, só estamos tentando reduzir a possibilidade de ser alguém indesejado na conta.

Register

Client

UI para registrar

Para entender como o registro de novos usuários funciona, podemos olhar como formulários em HTML funcionam.

<form action="./register">
</form>

Formulários coletam todos os dados dos elementos <input> dentro deles e enviam para o endereço especificado no atributo action. Tenha em mente que enviar quer dizer "mandar para aquele endpoint um request HTTP".

note

Caso você já tenha feito uma API REST em qualquer linguagem, você pode começar a notar detalhes familiares. Acontece que os navegadores não fazem nenhuma mágica, eles enviam o mesmo tipo de request que você está acostumado a lidar no backend de APIs REST.

Como eu estou escrevendo isto ao mesmo tempo que testo o código, eu vou trocar o endpoint para a minha API (http://127.0.0.1:8000/register).

<form action="http://127.0.0.1:8000/register">
</form>

Nós precisamos adicionar o input do tipo submit pois ele é utilizado para engatilhar o envio.

<form action="http://127.0.0.1:8000/register">
<input type="submit" value="register">
</form>

Se você clicar no botão de registrar, você mandara um request GET para o endpoint register, com nenhuma informação pois não existe nenhum campo input que segure informação.

No nosso caso precisamos de um campo para o username e outro para o password:

<form action="http://127.0.0.1:8000/register">
<input type="text" name="username" value="username"><br>
<input type="password" name="password" value="password"><br>
<input type="submit" value="register">
</form>

Note que utilizamos os tipos text e password apenas para dar o comportamento correto no navegador. O importante mesmo é o atributo name pois ele define o nome a qual o valor vai estar relacionado quando enviado.

Se clicarmos no botão de registrar, enviaremos as informações na URL:
/register?username=username&password=password

Por padrão o formulário envia um request GET, o que é ótimo se você quiser compartilhar URL com alguém ou salvar no favoritos.

Porém não é nada seguro quando estamos falando de informação sensível como a senha do usuário! Neste caso queremos enviar no corpo do request POST (onde não fica visível a qualquer pessoa olhando a tela do seu computador).

Podemos especificar o método utilizado no request atráves do atributo method:

<form action="http://127.0.0.1:8000/register" method="post">
<input type="text" name="username" value="username"><br>
<input type="password" name="password" value="password"><br>
<input type="submit" value="register">
</form>

Agora ao clicar no botão de registrar, enviaremos as informações no corpo do request e a URL para qual você vai ser direcionado não vai conter seus dados (/register).

Corpo do request: username=username&password=password

note

Note que no final das contas é o mesmo formato porém em lugares diferentes.

Se você tiver uma API, conseguirá ver que ambos possuem o campo content-type da requisição com o valor application/x-www-form-urlencoded.

Tudo isto poderia ser reproduzido em python com:

import httpx
from urllib.parse import urlencode

body = urlencode({"username": username, "password": password})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register", headers=headers, content=body)
note

Eu continuarei mostrando o código Python equivalente ao que seria feito pelo navegador/server pois acho que da uma visão lógica de como as coisas são feitas por baixo dos panos.

Server

Okay, seu server recebeu a requisição de cadastro do usuário. O que fazer agora? Validações!

Robo lendo um papel

O conteúdo da requisição está no formato esperado?

Podemos verificar se o campo content-type está com application/x-www-form-urlencoded.

if request.headers.get("Content-Type") != "application/x-www-form-urlencoded":
raise Exception("Invalid body format")

Claro que isso não impede o usuário de formatar o conteúdo incorretamente, porém agora podemos assumir que ele errou o formato e enviarmos uma mensagem de erro coerente com o problema.

try:
body = await request.body()
body = body.decode()
fields = parse_qs(body)
except ValueError:
raise Exception("Body incorrectly formatted")

E se o usuário esquecer um dos campos? Sim, precisamos validar isto também.

if "username" not in fields or "password" not in fields:
return Exception("Missing username or password")

E se o usuário tiver caracters inválido?
E se a senha tiver caracters inválido?
E se o usuário já existir no banco?
...

Acho que você já entendeu que validação é importante.
Agora vamos falar de storage!

Database

Como se armazena o username?
Igual a qualquer outro campo texto...

Como se armazena a senha?
Não se armazena senha...

Pode parecer estranho a primeira vista mas não precisamos armazenar a senha para conferir se alguém nos deus a senha correta.

Existem funções hash criptográficas que produzem saídas com propriedades que nos ajudam a conferir se a senha de um usuário está correta.

Propriedades que nos interessão nessas funções:

  • Dada uma entrada de bytes, sempre produz a mesma saída de bytes
    • Nada de especial aqui, apenas está garantindo que não é afetado por outro fatores aleatórios (tempo, temperatura, etc)
  • Não existe função que reverte a operação
    • Em outras palavras, tendo a saída de bytes da função você não consegue saber a entrada que foi dada para a função hash (sem ir chutando todas as possibilidades)
  • Qualquer mudança na entrada de bytes gera uma saída de bytes muito diferente
    • A ideia é que as pessoas não devem saber que as entradas são parecidas a partir da saída

Diversas funções hash criptográficas existem, cada uma com o próprio algoritmo.
No nosso caso vamos utilizar o algoritmo sha256 para os exemplos!

>>> sha256(entrada)
saida
note

Segurança é algo que muda com o tempo, então funções hash criptográficas seguras de antigamente podem já não ser mais seguras.

Estou usando a função hash que usa o algoritmo sha256 apenas de exemplo, não estou considerando se é segura ou não para o ano atual.

entradasaída
abcba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
abda52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
ABCb5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78
123a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
  • Se você der a mesma entrada, vai receber a mesma saída
  • Olhando a saída você não sabe a entrada
  • A mudanaça de um bit entre "abc" e "abd" mudou totalmente a saída

Não sei se ficou claro, mas isso é perfeito para podermos conferir se alguém acertou a senha.

Assim que um usuário registra no serviço e nos da senha, podemos calcular o hash da senha (saída da função hash para a senha) para armazenar.

>>> sha256(b"password").hexdigest()
'5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'

>>> sha256(b"mega_password").hexdigest()
'2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25'

>>> sha256(b"senha").hexdigest()
'b7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c'
usernamehash
thiagola925e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
darklord2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25
juninhob7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c

Quando alguém for logar no nosso serviço, a pessoa vai inserir a senha e nós vamos conferir se o hash dessa senha é igual ao que temos no banco.

  • Se for igual, a pessoa sabe a senha e nós podemos autorizar o acesso ao serviço
  • Se não for igual, a pessoa não sabe a senha e nós devemos negar o acesso ao serviço

Qual a vantagem de armazenar assim?
Se algum hacker acessar nosso banco, ele não vai conseguir saber a senha das pessoas.

Mesmo que a pessoa saiba que meu hash é:
5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
Se ela tentar o hash como senha, o hash do hash vai ser totalmente diferente.

>>> sha256(b"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8").hexdigest()
'113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3'

Então é isso? Temos que armazenar o hash da senha? Nope.

Face saying nope

O que acontece quando dois usuários possuem a mesma senha?

usernamehash
thiagola925e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
darklord2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25
juninhob7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c
mmiguel2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25

Isso quer dizer que se um hacker advinhar a senha de mmiguel, ele também sabe a senha de darklord.

Para previnir isso é normal adicionar salt às senhas, ou seja, concatenar bytes extras na senha antes de fazer o hash.

Assim que o usuário criar a conta, criaremos um salt para ele com uma função que gera X bytes aleatórios (os.urandom(X)):

# No caso vamos gerar 10 bytes aleatórios.
salt = os.urandom(10)
usernamesalt
thiagola92cea3a49482094b096acb
darklordb9f195b7ce55a51fdc0d
juninho885cfd855add75c08a7a
mmiguel10cc25d0c8adcbc44e9a

Iremos inserir na função hash a concatenação de senha + salt.

>>> sha256(b"password" + b"cea3a49482094b096acb").hexdigest()
'a15760e9639bc64e8908d5d2109900dcbc508e6fc5fa7b161e5149fcdaa04eee'

>>> sha256(b"mega_password" + b"b9f195b7ce55a51fdc0d").hexdigest()
'8c7044c8e42efebbf7d19027db0a00c8d2242197a0e001d5d0d977cccfaa020c'

>>> sha256(b"senha" + b"885cfd855add75c08a7a").hexdigest()
'f51f77c4575127dbf8906c60b7b1c80e01772be217c8a1c9df68e4f527b7f8eb'

>>> sha256(b"mega_password" + b"10cc25d0c8adcbc44e9a").hexdigest()
'bbb70b912376b0c266d90cd3ebd1ffa99f6c439a50ed6481bb809b99fd93d300'
usernamesalthash
thiagola92cea3a49482094b096acba15760e9639bc64e8908d5d2109900dcbc508e6fc5fa7b161e5149fcdaa04eee
darklordb9f195b7ce55a51fdc0d8c7044c8e42efebbf7d19027db0a00c8d2242197a0e001d5d0d977cccfaa020c
juninho885cfd855add75c08a7af51f77c4575127dbf8906c60b7b1c80e01772be217c8a1c9df68e4f527b7f8eb
mmiguel10cc25d0c8adcbc44e9abbb70b912376b0c266d90cd3ebd1ffa99f6c439a50ed6481bb809b99fd93d300

Agora se um hacker tentar todas as possibilidades de senhas para o mmiguel e um dia acertar, ele jamais vai ser que é a mesma senha para darklord.

E se o salt gerado for o mesmo para ambos?

Uma maneira de solucionar isto é verificar se já existe alguém com o mesmo hash e salt.
Caso sim, gere um novo salt...

warning

Minha intenção aqui era explicar o conceito.

É importante olhar a documentação da sua linguagem/biblioteca para saber a melhor função hash a se usar!

Por exemplo, python já disponibiliza uma função que já concatena senha e salt, e depois faz o hash (hashlib.scrypt()). Então não existe necessidade de fazer cada etapa dita neste post.

Login

Client

UI para login

Quando falando de website, a maneira para logar é a mesma de se registrar:

import httpx
from urllib.parse import urlencode

body = urlencode({"username": username, "password": password})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/login", headers=headers, content=body)

E o server responde enviando cookies para que o browser use nas próximas requisições de conteúdo.

Note que esse request utiliza o body para enviar username e password, o que deixa o body ocupado por dados relacionados ao login (ao menos que você queira fazer uma bagunça e misturar login com pedido de dados).

Outra maneira conhecida por APIs é passar a autentição no headers pelo campo Authorization:

import httpx
import base64

credentials = f"{username}:{password}"
credentials = credentials.encode()
credentials = base64.b64encode(credentials)
credentials = credentials.decode()

response = httpx.get("http://127.0.0.1:8000/", headers={"Authorization": f"Basic {credentials}"})

Note que nesse modelo nós botamos como valor do Authroization o esquema e depois a credencial:
<scheme> <credential>

No nosso caso o esquema é Basic, onde quer dizer que vamos passar as credenciais em base 64 e concatenadas com :.
<username>:<password>

A vantagem desta maneira é que em uma requisição nós já conseguiriamos nos autenticar e aplicar a operação desejada na API.

note

Essa maneira não é incentivada pois passar em toda requisição username e password é arriscado, basta uma pessoa má intencionado conseguir acesso a uma mensagem para obter sua senha.

Passar em uma requisição para obter um token/cookie é válido, pois toda requisição seguinte nós utilizariamos esse token/cookie para sermos validados no server.

Server

Okay, seu servidor recebeu o request de logar... O que fazer? Validações! (novamente)

Robo lendo um papel com os olhos estressados

Se estivermos falando da mesma maneira utilizada por websites, então as validações são bem parecidas com as de registrar!

if request.headers.get("Content-Type") != "application/x-www-form-urlencoded":
raise Exception("Invalid body format")
try:
body = await request.body()
body = body.decode()
fields = parse_qs(body)
except ValueError:
raise Exception("Body incorrectly formatted")
if "username" not in fields or "password" not in fields:
return Exception("Missing username or password")

Se estivermos falando da maneira das APIs, temos que adaptar ao outro local onde o username e password estão mas nada complicado!

if not request.headers.get("Authorization"):
raise Exception("Missing authorization")
auth = conn.headers["Authorization"]
scheme, credentials = auth.split()

if scheme.lower() != "basic":
raise Exception("Incorrect authorization protocol")
try:
credentials = base64.b64decode(credentials)
credentials = credentials.decode()
username, _, password = credentials.partition(":")
except ValueError:
raise Exception("Credentials incorrectly formatted")

Chegou a hora da verdade.
Quem mandou a requisição é o usuário verdadeiro da conta?

Pessoa com o lazer de uma arma na cabeça

Bem... Ele não vai ser o usuário verdadeiro se o usuário nem existir né?

Podemos verificar isso tentando pegar do banco o salt e hash.

salt, hash = get_user_auth(username)
if not salt or not hash:
raise Exception("Invalid user")

Agora vem a parte fácil... Validar se a senha que ele passou está certa! Basta calcular o hash usando a senha que nos passaram.

if sha256(password + salt).hexdigest() != hash:
raise Exception("Wrong password")

Fim!

Principalmente porque não pretendo cobrir a fundo assuntos como cookies e tokens. Mas tudo coberto já da um ótimo início para entender autenticação/login/cadastro/etc.

References

Thiago Lages de Alencar

Esse post é meio que uma compilação do que eu entendi de cada assunto após de horas lendo na internet e perguntando para meu pai.
Em outras palavras: Pode ter informação incorreta!

TTY

Teletype
(https://en.wikipedia.org/wiki/Teleprinter)

TTY é um comando em linux para saber o nome do terminal o qual a input está diretamente conectada.

$ tty
/dev/pts/1

$ echo "example" | tty
not a tty

No primeiro exemplo, o comando tty veio como input diretamente do terminal.

No segundo exemplo, o comando tty recebeu a input example do comando anterior (não de um terminal).

Fim! Pode ir para a próxima sessão, ao menos que você queira saber o que diabos é uma teletype.

Rosto curioso

Já notou que muitas coisas no computador possuem o nome de objetos que existem fora do computador?

Acontece que o nome é dado baseado nestes objetos para ajudar usuários a entenderem melhor o uso deles no computador! Por exemplo:

  • file
  • folder
  • trash can
  • window

O mesmo vale para TTY, onde o nome veio de teletypes. Infelizmente não é um nome que ajude muito pois computadores já subsittuiram o uso delas então esse nome não ajuda ninguém a saber do que o comando se trata 🤣.

O que são teletypes?
Entenda que elas são uma junção de typewriters e telegraph key.
O primeiro utilizado para escrever em papel e o segundo utilizado para enviar morse code a distância.

Morse code era muito utilizado como uma forma de comunicação binária (som curto/longo) via cabo. Porém para isso acontecer era necessário duas pessoas treinadas em morse code (uma para enviar e outra para receber).

mensagem <=> tradutor <=> morse code <=> transmissão

Com a chegada das teletypes, os tradutores foram substituidos por estas máquinas que eram capaz de traduzir e ler morse code.

mensagem <=> teletype <=> morse code <=> transmissão

Computadores da época utilizavam paper tape como forma de armazenamento de dados e monitores não eram algo acessível. Teletypes viram está oportunidade para preencher outro meio de comunicação (dado que computadores também utilizam binário).

mensagem <=> teletype <=> binário <=> computador

Este video mostra uma teletype recebendo e enviando dados de/a um terminal:
https://www.youtube.com/watch?v=S81GyMKH7zw

Por isto que o termo TTY era utilizado para referência aparelhos enviando/recebendo (input/output) mensagem do computador.

Algumas linguagens até incluem código para fazer essa verificação:

  • C
    • #include <unistd.h>

      isatty(fildes);
  • Python
    • import os

      os.isatty(fd)
  • NodeJS
    • tty.isatty(fd)

Onde a funcionalidade das funções é identificar se a input/ouput está vinculada a um aparelho (device).

warning

Preste bem atenção que sua input pode estar ligada ou não a um aparelho E sua output pode estar ligada ou não a um aparelho.
Um deles estar ligado não quer dizer que ambos estão.

As funções recebem um file descriptor e dizem se ele está ou não linkado a um aparelho.
Você poderia passar STDIN, STDOUT ou até STDERR para a função analisar.

Esse video cobre bem o assunto: https://www.youtube.com/watch?v=SYwbEcNrcjI

Se realmente quiser saber detalhes sobre TTY, existe este blog cheio de informações (que eu não li):
https://www.linusakesson.net/programming/tty/

TTY

Terminal

(https://en.wikipedia.org/wiki/Computer_terminal)

Teletypes originalmente eram conhecidas como "hard-copy terminals" por usarem papel, mas com a vinda de telas nós formamos uma nova ideia de terminal nas nossas cabeças (a tela preta).

Terminal não possui armazenamento de dados, da mesma maneira que teletypes apenas eram responsáveis por ler e escrever do computador, ou seja, a lógica ainda estava no computador. Alguns terminais possuiam um pouco de lógica neles porém nada comparado ao computador.

Se você viu o video da sessão anterior então já deve ter ganhado uma ideia do que é um terminal, pois nele é mostrado uma teletype lendo e escrevendo para um terminal.
Mas caso queira outro video mostrando melhor um terminal:
https://www.youtube.com/watch?v=UNdu0YQfvF0

Terminal

Terminal Emulator

(https://en.wikipedia.org/wiki/Terminal_emulator)

Hoje em dia usamos o termo terminal para representarmos emuladores de terminais.

Diferentemente de terminais, estes estão fortemente ligados a computador e não são máquinas separadas da lógica. Basicamente estamos falando da janela que finge ser um terminal (GNOME terminal).

Terminal Emulator

Shell

(https://en.wikipedia.org/wiki/Shell_script)

Um programa responsável por ficar em loop esperando comandos do usuário para serem executados.

Comandos podem ser:

  • Programas
    • echo
    • ls
    • touch
    • mkdir
    • Buscados em lugares pré definidos (/bin, /usr/bin, ...)
      • Use echo $PATH para ver a lista de lugares a se olhar
  • Comandos do próprio shell
    • type
    • which
    • help
    • man
    • Estes existem dentro do shell e não precisam ser buscados.
  • Shell functions
  • Aliases
    • Comandos definidos por nós, construido de outros comandos

Existem variações e alternativas de shell:

Shell

CLI

Command-line interface
(https://en.wikipedia.org/wiki/Command-line_interface)

É uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa.

Está interface se basea no usuário passar flags e mais informações em conjunto ao comando, dessa maneira mudando o comportamento do commando. Por exemplo, o programa ls disponibiliza diversas flags para alterar o comportamento:

  • ls
    • Lista tudo no diretório atual mas ignora os começando com .
  • ls -a
    • Lista tudo no diretório atual e não ignora os começando com .
  • ls -l
    • Lista tudo no diretório atual mas com mais detalhes

Fique bem claro que é o programa te dando opções de como interagir com ele, não o shell ou terminal, então resta ao programa implementar comportamentos para certas flags.

note

É muito comum programas oferecerem detalhes sobre as flags quando utilizando a flag --help (ls --help).

CLI

TUI

Terminal user interface
(https://en.wikipedia.org/wiki/Text-based_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém está foca em dar uma interação mais visual e continua.

Diferente de CLI's onde toda a interação começa e termina em um comando só, TUI's continuam esperando mais interações do usuário até um dos dois decidirem terminar.

Um exemplo bem comum é top que providência uma visão dos programas/processos/threads em execução do sistema, uma vez inicializado ele esperar por mais interações do usuário. Se você apertar q ele termina, se você apertar h ele fornece a lista de comandos, etc.

Note que a TUI's ainda podem providênciar flags para alterar o comportamento (top --help).

TUI

GUI

Graphical user interface
(https://en.wikipedia.org/wiki/Graphical_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém não está limitada a usar texto para a visualização, pois tem a capacidade de desenhar na tela.

Hoje em dia é o meio mais popular de se usar uma aplicação, quando se abre VSCode, Google Chrome, Discord... Todos são GUI's pois utilizaram a capacidade de desenhar para dar uma interface ao usuário.

Mesmo programas focados em GUI's podem aceitar flags (VSCode: code --help).

GUI

References

Thiago Lages de Alencar

Durante o post C/C++ Libs escrevi sobre usar bibliotecas no Linux e nunca imaginaria que teria novamente a dor de cabeça de ver o assunto quando fosse fazer o mesmo no Windows.

Personagem do desenho The Owl House chorando

warning

Importante avisar que eu irei escrever este post contando que você leu o post passado sobre Linux.

Irei ser breve e não irei estudar detalhes que nem fiz no do linux (pois estou cansado do assunto).

.lib (library)

Basicamente idêntico ao .a do Linux, uma biblioteca estática (static library).

note

Descobri que estes arquivos podem ser abertos tranquilamente com programas de zip tipo: Winrar, 7zip, PeaZip.

O que faz total sentido dado que eles programas para lidar com "File archive and extractor" e no Linux usando o formato .a de "archive".

Seria o próximo do .so do Linux, servindo o mesmo propósito de uma biblioteca compartilhada (shared library) porém o Windows possui uma implementação própria deles (dynamic-link library).

A grande diferença que precisamos saber é que durante a criação de bibliotecas compartilhadas, três formatos de arquivos são criados: .dll, .lib e .exp (vamos ignorar este último).

Onde o arquivo .lib NÃO é o mesmo que o gerado durante a biblioteca estática! Porém ainda é necessário para a utilização da biblioteca compartilhada.

Project from Zero

Seguiremos a mesma ideia do post no Linux. Apenas tendo o nosso código:

project/
└── src/
├── main.c
└── ...
cl src\main.c

Agora com um header:

project/
├── include/
│ └── header.h
└── src/
├── main.c
└── ...
cl src/main.c /Iinclude

.lib

project/
├── include/
│ └── header.h
├── lib/
│ └── name.lib
└── src/
├── main.c
└── ...
cl src/main.c /Iinclude lib/name.lib
note

Windows não tem o padrão de botar lib na frente das bibliotecas então não precisa nem pensar nisso.

.dll

Dessa vez vamos separar em duas etapas: compilar o código para .obj e depois linkar (gerar o .exe), isso torna mais fácil adicionar explicações no meio.

project/
├── include/
│ └── header.h
├── lib/
│ └── name.lib
└── src/
│ ├── main.c
│ └── ...
└── name.dll

Lembre que bibliotecas dinâmicas no Windows utilizam um arquivo .dll e um .lib (por isso temos os dois no projeto).
Windows busca os arquivos .dll na mesma pasta do executável, por isso ele não está na pasta lib.

cl src/main.c /c /Iinclude /Dexample

/c é justamente para pausar antes de linkar.
/Dexample é uma maneira de adicionar uma definição no início do código, equivalente a

#define example

Por que precisamos definir? Em bibliotecas do Windows, muitas vezes pode se encontrar código como o seguinte:

#if defined(EXPORT_DLL)
#define LIB_API __declspec(dllexport)
#elif defined(IMPORT_DLL)
#define LIB_API __declspec(dllimport)
#else
#define LIB_API
#endif

Onde LIB_API é substituido por:

  • __declspec(dllexport) quando criando uma biblioteca dinânmica
  • __declspec(dllimport) quando importando uma biblioteca dinâmica
  • nada quando é uma biblioteca estática

Não entendo bem do assunto então não pretendo entrar no assunto, mas é a maneira do windows lidar com bibliotecas dinâmicas.

Biblitoecas geralmente requerem que você passe essa definição para que ela adicione o contexto certo ao código durante a criação do objeto (para fazer name mangling corretamente?).


link /LIBPATH:lib name.lib main.obj

É durante a etapa de linkar que o arquivo .lib é finalmente utilizado!

Lembrando novamente que o executável vai buscar o .dll na pasta do executável, então bote ambos juntos.

Thiago Lages de Alencar

Forward And Backward Reaching Inverse Kinematics (FABRIK) é a última kinematic que veremos. É utilizada para cadeias de ossos também mas apenas precisamos de uma iteração para definir o estado final (diferentemente da CCD).

Corrente de 4 ossos

Forward And Backward Reaching

A idéia é duas caminhadas na cadeia de ossos, a primeira irá mover os ossos em direção ao alvo (forward) e a segunda vai voltar o osso a base (backward).

A operação importante a se entender durante as duas etapas é ação de alcançar um outro ponto (reaching). Vamos utilizar ela durante as duas etapas então é bom entender isto primeiro.

Reaching

Alcançar um alvo é dividido em 2 etapas, olhar para o alvo e mover até o alvo.

Um osso e um alvo fora do alcance

Na primeira etapa podemos utilizar a mesma lógica do look at ou usar a função que sua game engine disponibilizar para rotacionar até um ponto.

Osso rotacionando até o alvo

Para mover até o ponto é preciso usar o tamanho do osso e calcular onde seria o novo ponto da base do osso. Visualizar onde seria é algo bem simples:

Osso transparente onde o osso precisa estar no final

Calcular isso envolve conseguir criar um vetor que represente o osso. Primeiro precisamos saber o vetor em que o osso se encontra, o vetor do ponto inicial dele até o alvo.

Vetor = Posição do alvo - Posição do osso

Vetor do osso até o alvo

Com isto podemos calcular a proporção desse vetor com o vetor do osso. Em outras palavras, qual o tamanho do vetor do osso em relação a esse vetor? É duas vezes o tamanho deste? É três vezes o tamanho? É metade desse vetor?

Tamanho do vetor = √(Vetor.x² + Vetor.y²)
Proporção = Tamanho do osso / Tamanho do vetor

Utilizando essa proporção podemos criar um vetor do tamanho do osso.

Vetor osso = Vetor * Proporção

Vetor osso

A última coisa é calcular o novo ponto onde o osso deve inciar. Basta pegar o ponto do alvo e reduzir pelo vetor do osso.

Posição do osso = Posição do alvo - Vetor osso

Vetor osso começando no ponto onde o osso tem que terminar

Pronto, sabemos onde botar o osso e podemos mover ele para lá (caso você já não tenha movido ele na última operação)

Osso na posição correta

Forward

A primeira caminhada pela cadeia de ossos envolve fazer cada osso andar em direção (forward) ao osso seguinte. No caso da ponta da cadeia, ela irá mover em direção ao alvo.

TODO

TODO

O ponto vermelho irá representar onde globalmente o início do osso atual está, nós usamos ele para decidir onde o osso seguinte vai alcançar.

TODO

TODO

TODO

Estou pausando aqui para lembrar que movimentar e rotacionar um osso afeta todos os filhos, por isto os ossos filhos são movimentados e rotacionados de forma a ficarem "piores" (mais longe do alvo).

TODO

O osso seguinte irá utilizar o osso anterior como referência, seguimos essa tática para cada um dos ossos.

TODO

TODO

TODO

TODO

TODO

TODO

O ponto azul representa a base da cadeia e é um ponto de referência que usaremos na próxima caminhada.

Backward

note

A última caminhada deixou tudo uma bagunça mas isso apenas porque eu escolhi trata-los da mesma forma que minha game engine (Godot) trata nodes nela.

Se tivessemos usado um array de ossos em vez de relações pai e filho, um não afetaria o outro!

Nós focaremos agora a mover os ossos em direção a base, ou seja, eles caminharam para tráz (backward). Dessa vez não precisamos nos preocupar em rotacionar, apenas mover para o final do osso anterior.

TODO

TODO

TODO

TODO

TODO

TODO

TODO

TODO

Iteration

Ao final de uma iteração podemos ter algo errado como visto acima, mas se repetirmos mais vezes vamos começar a caminhar para algo melhor.

O que acontece se começarmos outra iteração? Vamos começar novamente rotacionando o osso da ponta.

TODO

Se você der zoom na imagem, vai notar que o último osso passou do ponto alvo. Mas a etapa seguinte é mover de forma que o ponto final do osso bata com a posição do alvo.

TODO

Bem, agora o osso seguinte está incorreto... Mas se continuarmos repetindo o processo...

TODO

Aos poucos os ossos vão indo para uma posição melhor, mas eu não pretendo mostra-lo uma segunda iteração pois eu fiz estes desenhos a mão. 🤣

Conclusion

O código simplificado em GDScript (linguagem do Godot):

for i in iterations:
_apply_forwards()
_apply_backwards(base_global_position)


func _apply_forwards() -> void:
# O osso da ponta vai morar no alvo, os seguintes vão tratar o anterior como alvo.
var target_global_position: Vector2 = target.global_position

# Esse array leva da ponta até a base.
for bone in chain:
# Rotaciona em direção ao alvo.
bone.look_at(target_global_position)

# Evita calcular ratio como infinito.
if target_global_position == bone.global_position:
continue

# Calcula a nova posição do osso.
var stretch: Vector2 = target_global_position - bone.global_position
var ratio: float = bone.get_bone_length() / stretch.length()
bone.global_position = target_global_position - stretch * ratio

# Define o alvo do osso seguinte.
target_global_position = bone.global_position


func _apply_backwards(base_global_position: Vector2) -> void:
# Esse array leva da ponta até a base, então agora precisamos caminhar ao contrário.
for i in range(chain.size() - 1, -1, -1):
var bone: Bone = chain[i]

bone.global_position = base_global_position

# Calcula a posição do osso seguinte.
var direction := Vector2(cos(bone.global_rotation), sin(bone.global_rotation))
base_global_position = bone.global_position + direction * bone.get_bone_length()

References