Limitando API: exemplo em Go

Hoje em dia é super comum o uso de API para alimentar aplicações que as vezes esquecemos que elas possuem algumas deficiências. Se o seu site/aplicação possui um backend em forma de API sabe que geralmente ela é publica e é suscetível vários tipos de ataques. Um dos mais comuns é o abuso da utilização da API e consequentemente criando carga no banco de dados podendo deixar toda a infraestrutura lenta.

A solução mais comum é criar um limitador global de requisições. Primeiro vamos criar uma estrutura para armazenar nossas informações.

type limit struct {
    max int
    interval time.Duration
    counter int
    mux sync.Mutex
}
  • max: número máximo de requisições
  • interval: intervalo até ocorrer o resete do número de requisições
  • counter: número atual de requisições
  • mux: forma idiomática de como indicar que todos os elementos dessa estrutura vão sofrer lock/unlock

A lógica é permitir todas as requisições e ir incrementando nosso counter. Periodicamente vamos resetar o contador pelo intervalo definido interval.

Vamos definir alguns métodos para nossa estrutura de controle.

func (l *limit) pass() bool {
    l.mux.Lock()
    defer l.mux.Unlock()
    if l.counter >= l.max {
        return false
    }
    l.counter++
    return true
}

func (l *limit) start() {
    ticker := time.NewTicker(l.interval)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            l.mux.Lock()
            l.counter = 0
            l.mux.Unlock()
        }
    }
}

func (l *limit) toLimit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !l.pass() {
            http.Error(w, "Rate limiting", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}
  • pass: informa se é permitido a execução da requisição atual
  • start: é uma função que vamos chamar em paralelo com a execução da nossa aplicação que faz o resete do nosso contador periodicamente
  • toLimit: nosso middleware que efetivamente realiza a passagem ou rejeição da requisição

A utilização do código é sem rodeios. Crie uma instância da estrutura limit definindo os valores de max e interval, inicie com start e finalmente passe o middleware para a sua cadeia de middlewares.

...
limits := &limit{
        max: 100,
        interval: 10 * time.Second,
    }

go limits.start()
...
E no seu router/dispacher preferido defina o uso do middleware.
...
r := mux.NewRouter()
r.Use(limits.toLimit, logger, auth)
...
Com menos de 50 linhas você pode evitar que seu banco de dados saia de produção, claro que só fazemos isso se essa preocupação existe.
Código completo aqui!
Como diz minha filha tchau, beijos.

 

Anúncios

Trabalho

Nessas três semanas andei escrevendo uma aplicação que imita o saudoso stackoverflow.com. Estou impressionado como o desenvolvimento em frontend parece avançar devagar. Muita coisa melhorou como por exemplo os frameworks de CSS e o Node.js cresceu absurdamente no número de pacotes.

Eu ainda devo ser muito ruim pois a diferença no número de linhas do back para o front foi de apenas 50%. Como que o back implementado seguindo as recomendações da OWASP e com JWT, argon2 implementados manualmente resulta em apenas 1.5x o tamanho do front? A aplicação ainda tem suporte a PostgreSQL e MongoDB. Um total de 883 dependências foram instaladas pelo NPM. Isso me assusta muito! Mesmo a aplicação usando o conceito Single Page Application (SPA).

Tive vários gotchas também pela minha inexperiência com ECMA. Como diabos o ECMA me implementa suporte a inteiros de 64 bits com apenas 53 bits de precisão? Foi tão frustante ter que mudar a lógica de aquisição de IDs pq o ECMA começa a truncar os números depois da precisão segura. Acho muito estranho esse padrão de implementação de grandes inteiros como ponto flutuante. Essa então que os desavisados acabam perdendo precisão é uma maravilha. Mais sobre o assunto nesse aqui.

Outra coisa que me incomodou bastante no Vue.js foi a comunicação entre componentes pai e filho. De pai para filho é possível usar props mas de filho para pai se usa emit. Eu entendo a justificativa de proteção dos dados do pai mas isso acaba virando bem tedioso depois de um tempo.

Outra coisa maravilhosa do Go que utilizei essa semana foi o suporte nativo a benchmarks. Havia uma duvida se era mais performativo realizar um copy de um slice ou criar um novo método que seria chamado em um loop. O copy era mais rápido e realiza duas alocações a menos do que a solução que meu colega de trabalho sugeriu.

Abaixo o Bootstrap e viva o/a/x Bulma. Até a próxima.

Os Dados do Museu Nacional

Provavelmente todo mundo já ficou sabendo do incêndio do Museu Nacional. Infelizmente lá se foi mais uma vez grande parte do conhecimento do país e não tem volta.

Nessas horas tristes que vale lembrar mais uma vez das iniciativas de dados sobre biodiversidade. Pelo menos algo foi salvo dessa tragédia.

O explorador do SiBBr possui 166.147 registros de ocorrência e o IPT (mais atualizado) possui 380.542. Além dos artigos e a formação pessoal são esses números que sobraram. E tomem cuidado pq essas iniciativas também estão abandonadas como o Museu e pode ocorrer um “apagão” de alguns servidores…

Desenvolvimento e família

No momento estou com um tempo livre e pouco dinheiro e isso permite realizar coisas prazerosas na vida. Tudo realmente tem seu lado positivo!

Para não ficar com a mente ociosa acabei lançando o checkports.online, uma web app simples para checar se a porta de algum computador na internet está disponível para conexões. Foi um ótimo exercício implementar uma worker pool em Go e usar Vue e reCaptcha no front. Vamos ver se pelo menos ajuda no pagamento da própria hospedagem.

Na parte de família brinquei de casinha com minha filha por algumas horas. Batom, cabelo sendo escovado e por ai vai. Muito gratificante ver o amor tão puro de uma criança.

marianinhos

R para Humanos

É comum que algumas pessoas me procurem no Skype e WhatsApp para tirar duvidas de R. As duvidas sempre são as mesmas encontradas em cursos presenciais, mas desta vez com os dados da pesquisa do mestrado ou doutorado.

Então pensei um curso no modelo 20-80 (20 teoria e 80 prática) com essas duvidas mais comuns que todo mundo vai passar enquanto trabalha nos próprios dados. Como estou bem apertado financeiramente criei um crowdfunding para os custos do curso.

O curso vai ser na forma de screencasts (vídeos da tela do RStudio) com no máximo 30 minutos cada. Algo para assistir depois do trabalho ou para tirar uma duvida rápida no assunto. Claro que o curso só sai com seu apoio, se tiver um tempinho faça uma visita em R para humanos

CSV. Você acaba de se fuder

Cuidado a postagem abaixo contem ódio, muito ódio.

Eu trabalho com frequência com arquivos no formato CSV (Comma-Separated Values) para manipulação de dados. Se você é pesquisador ou um programador você já trabalhou e teve alguma dor de cabeça com a manipulação de arquivos no formato CSV. Vamos começar com a definição formal do CSV.

O documento que define o formato do CSV é o RFC 4180. Precisa ter alguma noção de informática para entender o documento na integra, mas vamos comentar os pontos importantes.

Definições

Each record is located on a separate line, delimited by a line break (CRLF). For example:

aaa,bbb,ccc CRLF

zzz,yyy,xxx CRLF

 

Então temos o exemplo com dois registros e cada registro é separado por uma quebra de linha. Para os menos versados em informática o CRLF são dois caracteres invisíveis que compõem a quebra de uma linha. Para quem já passo pelo inferno de formatar algum documento seguindo normas ABNT ou de pós-graduações deve conhecer o símbolo ¶. Ele mostra caracteres invisíveis como o espaço e a quebra de linha.

 

The last record in the file may or may not have an ending line break. For example:

aaa,bbb,ccc CRLF

zzz,yyy,xxx

 

Não mudou muito do exemplo anterior, mas agora sabemos que o último registro não precisa ter uma quebra de linha. Isso significa que se você abrir um CSV no bloco de notas a última linha pode ser vazia ou não, somente isso!

 

There maybe an optional header line appearing as the first line of the file with the same format as normal record lines. This header will contain names corresponding to the fields in the file and should contain the same number of fields as the records in the rest of the file (the presence or absence of the header line should be indicated via the optional “header” parameter of this MIME type). For example:

field_name,field_name,field_name CRLF

aaa,bbb,ccc CRLF

zzz,yyy,xxx CRLF

 

Vamos ignorar a parte do MIME type que não é o foco no momento e tentar diferenciar apenas o exemplo atual dos anteriores. Podemos ter um novo registro que é definido como cabeçalho do arquivo que deve aparecer na primeira linha. Trocando em miúdos podemos ter uma linha no começo do arquivo que contém o título das colunas.

 

Within the header and each record, there may be one or more fields, separated by commas. Each line should contain the same number of fields throughout the file. Spaces are considered part of a field and should not be ignored. The last field in the record must not be followed by a comma. For example:

aaa,bbb,ccc

 

Aqui temos a definição de campos, do separador (vírgula) e três regras: devemos ter o mesmo número de campos em todo o arquivo, espaços não devem ser ignorados e o último campo não deve ter o separador. No exemplo acima temos o CSV com um registro e três campos no total, repare que com todas as definições que temos até agora já começa a lembrar o CSV que manipulamos no dia a dia.

 

Each field may or may not be enclosed in double quotes (however some programs, such as Microsoft Excel, do not use double quotes at all). If fields are not enclosed with double quotes, then double quotes may not appear inside the fields. For example:

“aaa”,”bbb”,”ccc” CRLF

zzz,yyy,xxx

 

Não tem muito o que dizer dessa definição, podemos limitar os campos dentro de aspas duplas e caso não seja delimitado por aspas duplas pode ocorrer dentro do campo. Você vai entender melhor na próxima.

 

Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes. For example:

“aaa”,”b CRLF

bb”,”ccc” CRLF

zzz,yyy,xxx

 

É aqui onde a merda toda geralmente acontece. Até aqui foi dito que a vírgula é o separador de campos, mas você por algum motivo quer colocar parágrafos nos campos. É bem provável que você vai ter várias vírgulas no seu CSV. Isso vai CAGAR a interpretação do CSV como foi definida até agora. Para resolver esse problema existe a opção de usar as aspas duplas. Exemplo:

“1,2,3”,”aaa”

Temos um registro com dois campos. Sem as aspas teremos o caso:

1,2,3,aaaa

Temos um registro com 4 campos. Se você no seu Excel quer ver 1,2,3 na mesma célula e fica aparecendo cada número em uma coluna e “empurrando” os outros campos para a direita o motivo foi uma implementação errada do CSV. Da mesma forma se quiser colocar várias linhas dentro de um campo é só seguir o próprio exemplo da penúltima definição.

 

If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote. For example:

“aaa”,”b””bb”,”ccc”

 

Você deveria estar se perguntando como colocar aspas duplas nos campos já que ela tem um significado definido no CSV. Essa definição existe para resolver esse problema. É só colocar duas “” para aparecer uma “.

Beleza, acabamos de ver a definição. Parece legal bem redonda né?

CSV, na vida real

Tudo bem que o RFC só surgiu depois (2005) do uso do CSV ser bem difundido, mas não é possível que ninguém ainda conhece o RFC.

Lembro que estava com problema de abrir um CSV de 4gb para realizar um processamento a nível de coluna e um amigo da TI teve a ideia genial de fazer um programinha em Java para interpretar o CSV e aplicar meu filtro. Fiquei bem feliz, iam fazer meu trabalho pela primeira vez. Meu colega implementou simplesmente dividindo os campos pelo separador (tabulação). Obviamente deu merda e eu tive que usar uma biblioteca para aplicar o filtro.

Outro caso que eu não esqueço era a diferença que o mesmo arquivo CSV tinha dentro da mesma organização (GBIF). Os dados do GBIF são coletados pelo seu próprio software IPT e a comunidade internacional consegue ter acesso no próprio portal online do GBIF. Eu trabalhei com esses dados com frequência e ficava maravilhado de como um CSV produto do portal GBIF não conseguia ser interpretado corretamente novamente pelo software IPT (sua fonte original). O principal problema era o separador tabulação, a justificava era que a vírgula era muito comum e poderia “estragar” o CSV. Estragar como caralho!? O padrão define tratamento do separador padrão (vírgula) usando aspas duplas. Eles também geravam CSVs nesse formato:

“aaa”,”bb”b,ccc

“aaa”,”bb”b”,ccc

 

Que merda é essa?! Como que interpreta o segundo campo no primeiro registro? Ele é “bb”b? Ou é bbb? Não dá para saber e cada software (IPT, GBIF, LibreOffice, Excel) vai interpretar de um jeito o que gera o inferno na manipulação de dados. Parece bobeira quando você pensa no seu CSV de 20 linhas da graduação, mas quando você entra na escala de milhões de linhas os dados são automaticamente classificados como de “baixa qualidade” ou “lixo”. As coisas estão mudando aos poucos, no https://github.com/gbif/gbif-common/issues/1 eu reporto para o GBIF que a biblioteca deles não segue o RFC.

Grande parte da culpa é do Excel. Se você usa as configurações regionais do Windows em Português Brasileiro o seu CSV vai utilizar o separador “;”. A Microsoft não deve ter recursos suficientes para ler a desgraça do RFC. O CSV não precisa de outro separador para resolver o problema. O uso de aspas duplas resolve o problema!

Se você é um programador não implemente padrões sem ler. Não, faça melhor. Siga a regra de implementação de criptografia. Use uma boa biblioteca que implementa o que você precisa.

Se você utiliza CSV com frequência você vai se FUDER, eventualmente.

Baixando anime em massa utilizando R + IRC

Qual a missão? Baixar vários episódios em massa, com velocidade aceitável, sem fillers.

Minha primeira ideia foi utilizar o torrent mas dependendo do anime é difícil possuir seed e ainda tenho que filtrar manual cada filler. Então pensei no IRC onde isso já é possível a muito tempo.

IRC é uma tecnologia bem antiga de comunicação que lembra bastante salas da Uol. Mesmo estando bem fora de “moda” ainda continua popular em projetos abertos e anime.

channel_window

Exemplo do cliente HexChat na rede FreeNode. Incríveis 565 pessoas no #defocus!

A primeira coisa é encontrar uma packlist contendo todos os episódios. Utilizando o Naruto Shippuuden como exemplo o HorribleSubs Packlist possui uma lista bem completada dos episódios. Visitando o packlist e filtrando por Naruto é possível baixar toda a tabela e converter para o formato .CSV com o LibreOffice.

Screenshot-2018-1-3 Packlist

Packlist

Primeira tarefa é carregar a lista e verificar se realmente tem os 500 episódios prometidos.

shippuuden = read.csv('packlist.csv', stringsAsFactors = F)
# find the prefix common to all eps
lastCommon = ""
i = 1
while ( all(substr(shippuuden$name, 1, i) == substr(shippuuden$name[1], 1, i)) ) {
  lastCommon = substr(shippuuden$name, 1, i)
  i = i + 1
}
prefix = substr(shippuuden$name[1], 1, i - 1)

epPattern = ".+?(\\d+).+" # select only the ep number part
eps = sub(epPattern, "\\1", shippuuden$name)
numberFormat = sprintf("%%0%dd", min(nchar(eps))) # assuming eps start from ep 1 to X
# check if packlist is missing any ep
epsModel = list()
for(i in sprintf(numberFormat, 1:max(as.numeric(eps)) ) ) {
  if ( !any(eps == i) ) {
    epsModel[[ i ]] = list(miss = T)
    next
  }

  hasEp = which(eps == i)
  if ( length(hasEp) == 1 ) {
    epsModel[[ i ]] = list(miss = F,
                           bot = shippuuden[hasEp, 'bot'],
                           pack = shippuuden[hasEp, 'pack'],
                           size = shippuuden[hasEp, 'size'],
                           name = shippuuden[hasEp, 'name']
                           )
  } else if ( is.null(epsModel[[ i ]]) ) {
    # if multiple bots have the same ep, pick the first one
    myQuality = which(grepl(quality, shippuuden[hasEp, ]$name))
    myQuality = ifelse(length(myQuality) &amp;gt; 1, myQuality[1], myQuality)
    epsModel[[ i ]] = list(miss = F,
                           bot = shippuuden[hasEp[myQuality], 'bot'],
                           pack = shippuuden[hasEp[myQuality], 'pack'],
                           size = shippuuden[hasEp[myQuality], 'size'],
                           name = shippuuden[hasEp[myQuality], 'name']
    )
  }
}

O trecho acima monta uma lista de 1-500 com os seguintes elementos:

  • miss: se verdadeiro indica que o episódio não foi encontrado em nenhum bot e em nenhuma qualidade
  • bot: nick do bot que disponibiliza o download desse episódio
  • pack: número do arquivo na packlist do bot
  • size: tamanho do episódio
  • name: nome completo do episódio incluindo extensão

Removendo os fillers:

filler = c(57:70,
           91:112,
           144:151,
           170:171,
           176:196,
           223:242,
           257:260,
           271,
           279:281,
           284:295,
           303:320,
           327,
           349:361,
           376:377,
           388:390,
           394:413,
           416:417,
           419,
           422:423,
           427:457,
           460:462,
           464:468,
           480:483)
ep2download = do.call(rbind.data.frame, epsModel[-filler])

E finalmente gerando as msgs para colar no IRC:

by(ep2download[, c('bot','pack')], ep2download$bot, function(x) paste("/msg",unique(x$bot), "XDCC BATCH", paste(getRange(x$pack),collapse = ',' )) )

Exemplo da saída:

ep2download$bot: CR-BATCH|480p
[1] "/msg CR-BATCH|480p XDCC BATCH 759-814,829-837"
----------------------------------------------------------------------------------------------
ep2download$bot: CR-BATCH|720p
[1] "/msg CR-BATCH|720p XDCC BATCH 1773-1803,1812-1829,1832-1835,1857-1882,1903-1916,1921-1930,1932-1938,1942-1943,1956-1962,1981-1986,1988-2008,2022-2035,2038-2047,2051-2053,2074-2075,2078,2080-2081,2084-2097"
----------------------------------------------------------------------------------------------
ep2download$bot: Ginpachi-Sensei
[1] "/msg Ginpachi-Sensei XDCC BATCH 1618-1619,1623,1629-1639,1644-1660"

Copia e cola no IRC, configura o HexChat para aceitar automaticamente transferência de arquivos e GG. Lembrando que omiti algumas coisas como por exemplo a preferência pela resolução 720p e a verificação que o episódio já foi baixado.

Isso é tudo pessoal! Nerdizando nas férias em vez de descansar 😦