DEV Community

Jefferson Quesado
Jefferson Quesado

Posted on • Originally published at computaria.gitlab.io

2

Fazendo um proxy reverso em Go

Vamos fazer um proxy reverso em Gox?

Objetivos

  • Recebe e envia HTTP 1.1
  • Não se preocupa com método de requisição
    • Por exemplo, GET não tem body
    • Métodos HTTP fora do standard (MKCOL, por exemplo, ou QUERY, futuro)
  • Não fazer buffer a priori de todo o body antes de fazer a requisição
  • reencaminhar parte significativa do PATH
  • Zero dependências externas

Hello world

Existem algumas maneiras para se estabelecer um servidor em Go para servir conexões HTTP. As mais simples dela envolvem o server padrão, o DefaultServerMux.

Basicamente, você simplesmente pede para manusear a chamada HTTP, e pode ser de dois jeitos:

  • http.HandleFunc: aqui você só passa uma função que vai lidar com suas questões
  • http.Handle: aqui você passa um objeto que possui o método público ServeHTTP adequado para a interface http.Handler

Depois de você pedir para o servidor HTTP lidar com as requisições passando o handler, você precisa criar um tipo adequado. Por exemplo, adaptado da documentação:

package main

import (
    "fmt"
    "html"
    "log"
    "net/http"
)

type hello struct {
}

func (s hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path))
}

func main() {
    fooHandler := hello{}
    http.Handle("/oie", fooHandler)

    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Go até aceita que você cria structs anônimas, e pode atribuir a elas campos como funções. Mas nesse caso aqui uma interface não espera um campo (diferente de uma interface no TS), mas sim métodos:

package main

import (
    "fmt"
    "html"
    "log"
    "net/http"
)

func main() {

    fooHandler2 := struct{
        ServeHTTP func (w http.ResponseWriter, r *http.Request)
    } {
        func (w http.ResponseWriter, r *http.Request) {
            fmt.Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path))
        },
    }
    http.Handle("/oie", fooHandler2)

    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Isso gera esse erro:

Não pode usar fooHandler2 porque não implementa o método ServeHTTP (ServeHTTP é um campo, não um método)

Escutar e servir

Após adicionar os http.Handle ou http.HandleFunc, pedir por http.ListenAndServe irá levantar o servidor na porta adequada.

Claro que você também tem a opção de prover a sua própria implementação do servidor HTTP, não restrito ao DefaultServerMux. Por exemplo:

s := &http.Server{
    Addr: ":8080", 
    //... params
}

log.Fatal(s.ListenAndServe())
Enter fullscreen mode Exit fullscreen mode

Então, temos este "endereço". Que no http.ListenAndServe é passado como parâmetro, já no (*http.Server) ListenAndServe é um campo de http.Server. Ele serve para indicar o quê, afinal?

Além de indicar a porta, ele também indica como o servidor pode ser acessado. Por exemplo:

log.Fatal(http.ListenAndServe(":8080", nil))
Enter fullscreen mode Exit fullscreen mode

Torna o servidor acessível através do meu celular, na mesma rede:

Resposta 'Hello,

Porém, uma pequena mudança torna isso inacessível:

O Safari não pode abrir a página

E qual foi a mudança que fez isso? Simplesmente colocar como o endereço localhost:8080:

log.Fatal(http.ListenAndServe("localhost:8080", nil))
Enter fullscreen mode Exit fullscreen mode

Isso serve para fechar o servidor a escutar uma requisição pelo endereço usado para identificar o servidor. Colocar o algo antes da porta, eu impeço que seja escutado quando a requisição não chega usando o DNS adequado.

Uma revisão em HTTP

Recentemente escrevi um post sobre HTTP, mas o foco da postagem foi mais uma pegada geral sobre solução de problemas e estratégia de estudo. Em breve retornarei a escrever sobre HTTP em minúncias, especificando o que implementei e como.

Uma mensagem HTTP é basicamente dividida em duas partes:

  • uma requisição
  • uma resposta

Ambas as partes da mensagem HTTP1.1 são divididas, em grosso modo, da mesma maneira:

  • uma primeira linha (aqui requisição e resposta são absurdamente distintas)
  • headers (até uma linha em branco)
  • body
  • headers de fim (apenas para chunked encoding, omitido na explicação abaixo)

Na requisição, a primeira linha consiste das seguintes partes (separadas por espaço):

  • método
  • path
  • protocolo

Por exemplo:

GET /oie HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

Isso significa que o agente (normalmente o navegador) está requisitando com GET a página de caminho /oie para o servidor, usando o protocolo HTTP/1.1 (que eu grafei anteriormente como HTTP1.1, mas aqui é mais estrito o uso).

Isso serve para diferenciar, por exemplo, de quando a requisição é o clássico HTTP/1.0. Para simplesmente receber uma requisição de modo similar ao HTTP 1.X, o HTTP2 usa como primeira linha:

PRI * HTTP/2.0
Enter fullscreen mode Exit fullscreen mode

Que não tem nenhuma semântica. HTTP2 usa outra estratégia para as informações de requisição, nominalmente chamdas de pseudo-header :method e :path.

Após essa linha, vem os headers. Um header tem pela RFC um jeito bem flexível de ser declarado, mas costumeiramente é contido em uma linha (inclusive descobri escrevendo este post que um header ocupando diversas linhas foi marcado como deprecado na RFC 7230).

Basicamente, um header tem o seguinte formato:

header: value
Enter fullscreen mode Exit fullscreen mode

E o header pode aparecer várias vezes durante a listagem:

header1: value
header2: value for 2
header1: other value for 1
Enter fullscreen mode Exit fullscreen mode

No caso de envios "repetidos" de headers, o recipiente da mensagem precisa interpretar como se fosse uma lista continuada, algo mais ou menos assim:

{
    header1: ["value", "other value for 1"],
    header2: "value for 2"
}
Enter fullscreen mode Exit fullscreen mode

ou assim:

{
    header1: "value, other value for 1",
    header2: "value for 2"
}
Enter fullscreen mode Exit fullscreen mode

E como se chega no final dos headers? Com uma linha em branco. Após a linha em branco, se tiver alguma mensagem para ser enviada, ela estará presente.

Então, após a linha em branco, vemos o corpo. E o modo como o corpo será transportado vai ter algumas características próprias. Basicamente, as 3 estratégias são:

  • tamanho conhecido
  • chunked
  • multipart boundary

O envio de tamanho conhecido é basicamente informar que vai enviar N bytes e enviar esses N bytes. O chunked é enviar em pequenos pedaços, em chunks, indicando um chunk especial de tamanho 0, similar ao caracter nulo que termina strings em C. Em transferências multipart, é usado o limitador (boundary) para separar uma parte de outra, e um indicador no boundary é usado para indicar o final da mensagem, mas transferências multipart são usadas dentro do contexto de tamanho conhecido, sendo os boundaries computados para o tamanho total do conteúdo.

E, bem, e quanto à resposta do HTTP? Basicamente tudo que foi dito continua válido para a resposta, mas aqui precisamos distinguir uma coisa: a primeira linha.

Enquanto que na requisição a primeira linha consistia de método, caminho e protocolo, aqui a primeira linha consiste de 3 partes:

  • protocolo
  • código de status
  • uma razão

Na prática, a razão muitas vezes é um mnemônico relativo ao status code. Por exemplo, 200 OK.

Headers especiais

Meu foco é HTTP 1.1. E ele possui alguns headers especiais que fazem parte do protocolo, ou de negociação de conteúdo. Alguns desses headers já foram aludidos:

  • Content-length
  • Transfer-encoding

O Content-length vai indicar o tamanho do conteúdo sendo trafegado, exceto se for usado Transfer-encoding: chunked, onde o tamanho não é determinado priori e se precisa usar uma estratégia de streaming específica.

Outro header que pode alterar o como as coisas são transferidas é o Content-type, quando o tipo é multipart/*. Nesse caso, é usada uma estratégia de streaming própria. Mas, as partes relativas ao boundary são partes inerentes da transferência, computado no tamanho total. Portanto, posso ignorar totalmente isso e considerar apenas como um grande transporte.

Outro header especial é a requisição Connection: keep-alive. Basicamente isso quer dizer que o cliente quer manter a conexão aberta, inclusive isso foi utilizado para resolver problemas de performance web, vide O dia em que precisei estudar HTTP, para otimizar a aplicação.

Junto do connection: keep-alive da requisição, temos o connection: closed da resposta. Isto é definido salto a salto, então este header não precisa ser repassado adiante, apenas gerenciado internamente.

Devido a questões de virtual servers e uso da mesma máquina para responder diversas requisições, também tem a questão de enviar o Host como header mandatório.

O header TE tem a particularidade no contexto da requisição. Ao ser usado o valor trailers, indica que o cliente aceita receber headers após o body. Então, como eu não quero lidar com isso, vou tratar de suprimir esse valor.

Tem também o Forwarded, indicado para proxies que querem permitir que o servidor que está recebendo a requisição saiba de onde ela partiu, para a partir disso podeer tomar alguma decisão. Normalmente essa informação é injetada pelo agente de proxy (RFC 7239).

Lista de headers

  • Content-length
  • Transfer-encoding
    • Valor: chunked
  • Connection
    • Valor: keep-alive, close
  • Host
  • TE: trailers
  • Forwarded
    • injetado pelo proxy

Como funciona em Go? Servidor

Vamos começar com a requisição. Não precisamos nos preocupar exatamente com o como as informações são empacotadas, e como é a requisição não preciso nem me preocupar na ordem em que elas são apresentadas.

Em Go, usando o servidor HTTP padrão provido pela linguagem, eu posso simplesmente encaminhar a requisição. Para isso eu preciso ter um cliente HTTP, mas isso será visto mais tarde. Preciso me atentar a algumas coisas na hora de passar a requisição adiante:

  • devo ignorar totalmente a questão do Connection, isso é algo salto a salto
  • lidar corretamente com o streaming de dados chunked
  • header de Host adequado para o encaminhamento

Além disso, a RFC sugere fortemente o Forwarded ao usar proxies, para que o server ainda tenha condição de saber quem fez a requisição original.

Além disso, vem bem a calhar a suprimir TE: trailers. Ou então lidar com isso, mas agora parece uma complicação opcional a mais. Quem sabe um outro momento?

Como servidor, eu tenho acesso ao cmapo request.Body, que implementa a interface io.Reader. E posso usar o método Read dele. Exemplificando uma leitura completa:

body := r.Body

size := 0
buffer := make([]byte, 512*1024)
completo := make([]byte, 0)

for {
    n, err := body.Read(buffer)

    fmt.Printf("leitura parcial <%s>\n", string(buffer[:n]))

    if n > 0 {
        completo = append(completo, buffer[:n]...)
        size += n
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        return
    }
}
fmt.Printf("body completo <%q>\n", string(completo))
Enter fullscreen mode Exit fullscreen mode

Adaptado de Golang Cafe

Basicamente a ideia é ler em um buffer até que nenhum byte mais esteja presente. A leitura vai ocorrer até que err == EOF, situação em que Go indica final da leitura.

No meu caso, eu fiquei concatenando a leitura no slice completo. Para fazer isso corretamente, eu preciso chamar append(slice, elements...). Mas o buffer que estou guardando as coisas é um slice, não um .... Como resolver isso? Usando o spread: completo = append(completo, buffer[:n]...). Adaptei desta resposta no StackOverflow. O Geeks for Geeks oferece também algumas estratégias de fazer a cópia de um slice em outro, e explica alguns detalhes no caminho, How to copy one slice into another slice in Golang. Note que não pego buffer inteiro, apenas a parte que foi lida de buffer, que é o slice buffer[:n].

Fazendo uma leitura enviando 5 bytes por vez, com o corpo tralala, obtive esses logs:

leitura parcial <trala>
leitura parcial <la>
e aí? body <"tralala">
Enter fullscreen mode Exit fullscreen mode

Para pegar o método, eu posso pedir para a requisição r.Method. Ele retorna até métodos fora do convencional. Por exemplo, você pode pedir para o curl fazer uma requisição HTTP usando um método totalmente inovador usando a opção de CLI -X método. No caso, testei com -X PRRRR, gerando o método PRRRR.

Já na hora de escrever a resposta eu preciso me preocupar. Para escrever:

  • protocolo + status + razão
  • headers
  • body

O protocolo vai ser inserido automaticamente pela biblioteca padrão, então preciso de uma alternativa para o código de status HTTP. E o ResponseWriter tem uma alternativa pra isso. Por exemplo, simulando um status de Forbidden:

w.WriteHeader(http.StatusForbidden)
Enter fullscreen mode Exit fullscreen mode

O cuidado a se tomar é que chamar isto deve ser a primeira coisa. Logo depois, vem os headers. Certo isso? Bem... descobri que não. Da documentação de ResponseWriter.Header():

Header returns the header map that will be sent by ResponseWriter.WriteHeader. The Header map also is the mechanism with which Handler implementations can set HTTP trailers.

Changing the header map after a call to ResponseWriter.WriteHeader (or ResponseWriter.Write) has no effect unless the HTTP status code was of the 1xx class or the modified headers are trailers.

Ou seja: os headers vão ser escritos junto do status com WriteHeader.

Usando o exemplo acima, para fazer uma transmissão com Transfer-Encoding: chunked:

w.Header().Add("transfer-encoding", "chunked")
w.WriteHeader(http.StatusForbidden)

w.Write([]byte("lalala\n"))
w.Write([]byte("lelele\n"))
w.Write([]byte("lilili\n"))
fmt.Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path))
Enter fullscreen mode Exit fullscreen mode

Fazendo a leitura em curl:

> curl localhost:8080/oie -v -X PRRRR -d "tralala"
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> PRRRR /oie HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 7 bytes
< HTTP/1.1 403 Forbidden
< Date: Tue, 18 Mar 2025 02:56:14 GMT
< Transfer-Encoding: chunked
< 
lalala
lelele
lilili
Hello, "/oie"
* Connection #0 to host localhost left intact
Enter fullscreen mode Exit fullscreen mode

Hmmm, mas isso não me dá a visão dos chunks. Uma resposta do próprio Bagder indica como obter os chunks, só usar --raw:

> curl localhost:8080/oie -v -X PRRRR -d "tralala" --raw
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> PRRRR /oie HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 7 bytes
< HTTP/1.1 403 Forbidden
< Date: Tue, 18 Mar 2025 02:56:14 GMT
< Transfer-Encoding: chunked
< 
23
lalala
lelele
lilili
Hello, "/oie"

0

* Connection #0 to host localhost left intact
Enter fullscreen mode Exit fullscreen mode

Hmmm, não mandou chunks pequenos... e se eu quiser chunks pequenos? Bem, segundo esta resposta, o ResponseWriter que nos é informado normalmente também implementa http.Flusher. Posso usar o casting para esse fim:

flusher := w.(http.Flusher)
Enter fullscreen mode Exit fullscreen mode

Ou então daria para também se preparar no caso de não ser um http.Flusher:

type fakeflusher struct {
}

func (f fakeflusher) Flush() {
}

flusher, ok := w.(http.Flusher)

if !ok {
    fmt.Println("response not a flusher")
    flusher = fakeflusher{}
}
Enter fullscreen mode Exit fullscreen mode

E aqui estou garantindo que eu possa sempre chamara os métodos de Flush(), pois estou criando um objeto que implementa a interface http.Flusher, afinal ele é da struct fakeFlusher.

Com o flusher em mãos, podemos pedir para que ele dê um flush no que tá retido:

w.Header().Add("transfer-encoding", "chunked")
w.WriteHeader(http.StatusForbidden)

w.Write([]byte("lalala\n"))
flusher.Flush()
w.Write([]byte("lelele\n"))
flusher.Flush()
w.Write([]byte("lilili\n"))
flusher.Flush()

fmt.Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path))
Enter fullscreen mode Exit fullscreen mode

E isso me dá a seguinte chamada curl:

> curl localhost:8080/oie -v -X PRRRR -d "tralala" --raw
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> PRRRR /oie HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 7 bytes
< HTTP/1.1 403 Forbidden
< Date: Tue, 18 Mar 2025 02:56:14 GMT
< Transfer-Encoding: chunked
< 
7
lalala

7
lelele

7
lilili

e
Hello, "/oie"

0

* Connection #0 to host localhost left intact
Enter fullscreen mode Exit fullscreen mode

E agora foi possível perceber perfeitamente a separação em cada um dos chunks transferido, e o chunk terminador de tamanho 0.

Como funciona em Go? Cliente

Para criar uma conexão, a documentação indica usar http.NewRequest e adicionar headers usando req.Header.Add():

req, err := http.NewRequest("GET", "http://example.com", nil)
//                           método                      reader do body
//                                  url alvo da API
req.Header.Add("Header", "value for header")
Enter fullscreen mode Exit fullscreen mode

Para fazer uma requisição completamente arbitrária, é preciso criar um http.Client, e também na documentação ele cita sobre passar o net.Transport para construir o client:

tr := &http.Transport{
    MaxIdleConns:       10,
    IdleConnTimeout:    30 * time.Second,
    DisableCompression: true,
}
client := &http.Client{Transport: tr}
Enter fullscreen mode Exit fullscreen mode

E a requisição é um simples client.Do(req). Por exemplo, para fazer uma chamada para o Computaria em ambiente de desenvolvimento:

tr := &http.Transport{
    MaxIdleConns:       10,
    IdleConnTimeout:    30 * time.Second,
    DisableCompression: true,
}
client := &http.Client{Transport: tr}

func fullRead(r io.Reader) (string, error) {
    size := 0
    buffer := make([]byte, 512*1024)
    completo := make([]byte, 0)

    for {
        fmt.Println("entrando na leitura")
        n, err := r.Read(buffer)

        fmt.Printf("leitura parcial %s\n", string(buffer[:n]))

        if n > 0 {
            completo = append(completo, buffer[:n]...)
            size += n
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return string(completo), err
        }
    }
    return string(completo), nil
}

req, err := http.NewRequest("GET", "http://localhost:4000/blog/", nil)
if err != nil {
    fmt.Println("deu ruim criando a req")
    return
}
rcvd, err := client.Do(req)
if err != nil {
    fmt.Println("deu ruim lendo a resposta")
    return
}
str, err := fullRead(rcvd.Body)
if err != nil {
    fmt.Println("não leu tudo, portencialmente", str)
    return
}
fmt.Print(str)
Enter fullscreen mode Exit fullscreen mode

E voi là, a página foi escrita no console.

Escrevendo o proxy

Como o Computaria está todo pronto para responder usando como primeiro caminho /blog, vou manter isso no meu proxy. Reescrever essa parte seria bem complicado, pois envolveria ou interceptar a resposta e identificar os links dentro da resposta ou tornar o blog aware de que responderia com outra path base.

O proxy vai implementar http.Handler. Vai bem a calhar que o proxy tenha 3 atributos:

  • o prefixo que ele vai servir (no caso, /blog/)
  • o endereço que vai bater (no caso, http://localhost:4000/blog/)
  • um cliente HTTP já montado

Assim, temos a estrutura proxy definida assim:

type proxy struct {
    client  *http.Client
    target  string
    preffix string
}

tr := &http.Transport{
    MaxIdleConns:       10,
    IdleConnTimeout:    30 * time.Second,
    DisableCompression: true,
}
client := &http.Client{Transport: tr}

proxyHandler := proxy{client, "http://localhost:4000/blog/", "/blog/"}
http.Handle("/blog/", proxyHandler)

log.Fatal(http.ListenAndServe(":8080", nil))
Enter fullscreen mode Exit fullscreen mode

Ok, agora preciso definir como lidar com a requisição. De modo geral, vou pegar as coisas de uma requisição recebida e escrever na requisição a ser feita. Isso inclui método, path (após o prefixo), body, etc. No caso de headers que preciso tomar um pouco de cuidado: quero poder ignorar headers de trailer (portanto vou remover o TE: trailers quando presente), e preciso ignorar Connection e também Host.

É de bom tom adicionar o Forwarded. Na requisição tem o campo r.RemoteAddr, que traz o endereço de quem se conectou e também a porta utilizada. Segundo a especificação, essa porta não é relevante, então preciso limpar ela:

func noPort(remoteAddr string) string {
    for idx, c := range remoteAddr {
        if c == ':' {
            return remoteAddr[:idx]
        }
    }
    return remoteAddr
}
Enter fullscreen mode Exit fullscreen mode

A estratégia que usei para limpar a porta foi iterar até encontrar o :. A iteração do tipo "for-each" em uma string, ou vetor, ou slice, é feita usando o range value, com o primeiro elemento retornado sendo o índice e o segundo elemento o valor em si. No caso acima, idx é o índice e c o caracter da string. Ao localizar o :, pego um "slice" da string até aquele índice. Ou na ausência, retorna o que recebeu.

Para iterar um mapa usamos também o range mapValue, mas a diferença é que, no lugar do primeiro elemento ser um índice, ele é a chave do mapa, e o segundo elemento é o valor associado àquela chave. Isso é bom para iterar nos headers:

for reqHeader, value := range r.Header {
    fmt.Println(reqHeader, value)

    if strings.ToLower(reqHeader) == "connection" || strings.ToLower(reqHeader) == "host" {
        continue
    }
    if strings.ToLower(reqHeader) == "te" {
        valueClean := []string{}
        for _, teValue := range value {
            if strings.ToLower(teValue) == "trailers" {
                continue
            }
            valueClean = append(valueClean, teValue)
        }
        if len(valueClean) == 0 {
            continue
        }
        value = valueClean
    }
    req.Header.Add(reqHeader, strings.Join(value, ", "))
}
req.Header.Add("Forwarded", "for="+noPort(r.RemoteAddr))
Enter fullscreen mode Exit fullscreen mode

A criação da requisição é bem direta, basicamente encaminhando o que veio do cliente:

reqPath := r.URL.Path

relevantPath := reqPath[len(s.preffix):]
req, err := http.NewRequest(r.Method, s.target+relevantPath, r.Body)
Enter fullscreen mode Exit fullscreen mode

A única adaptação feita é a questão de que o relevantPath é computado de acordo com o path da requisição e o prefixo do proxy.

Então, depois de toda essa informação extraída, mandamos a conexão e copiamos os headers da resposta (com exceção do Connection):

rcvd, err := s.client.Do(req)
if err != nil {
    fmt.Println("deu ruim lendo a resposta")
    return
}
for responseHeader, value := range rcvd.Header {
    fmt.Println(responseHeader, value)
    if strings.ToLower(responseHeader) == "connection" {
        continue
    }
    w.Header().Add(responseHeader, string(strings.Join(value, ", ")))
}
w.WriteHeader(rcvd.StatusCode)
io.Copy(w, rcvd.Body)
Enter fullscreen mode Exit fullscreen mode

O código inteiro fica assim:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "strings"
    "time"
)

type proxy struct {
    client  *http.Client
    target  string
    preffix string
}

func (s proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    reqPath := r.URL.Path
    fmt.Println(reqPath)

    relevantPath := reqPath[len(s.preffix):]
    fmt.Println(relevantPath)
    req, err := http.NewRequest(r.Method, s.target+relevantPath, r.Body)
    if err != nil {
        fmt.Println("deu ruim criando a req")
        return
    }

    for reqHeader, value := range r.Header {
        fmt.Println(reqHeader, value)

        if strings.ToLower(reqHeader) == "connection" || strings.ToLower(reqHeader) == "host" {
            continue
        }
        if strings.ToLower(reqHeader) == "te" {
            valueClean := []string{}
            for _, teValue := range value {
                if strings.ToLower(teValue) == "trailers" {
                    continue
                }
                valueClean = append(valueClean, teValue)
            }
            if len(valueClean) == 0 {
                continue
            }
            value = valueClean
        }
        req.Header.Add(reqHeader, strings.Join(value, ", "))
    }
    req.Header.Add("Forwarded", "for="+noPort(r.RemoteAddr))

    fmt.Println(req.Header)
    fmt.Println()

    rcvd, err := s.client.Do(req)
    if err != nil {
        fmt.Println("deu ruim lendo a resposta")
        return
    }
    for responseHeader, value := range rcvd.Header {
        fmt.Println(responseHeader, value)
        if strings.ToLower(responseHeader) == "connection" {
            continue
        }
        w.Header().Add(responseHeader, string(strings.Join(value, ", ")))
    }
    w.WriteHeader(rcvd.StatusCode)
    io.Copy(w, rcvd.Body)
}

func noPort(remoteAddr string) string {
    for idx, c := range remoteAddr {
        if c == ':' {
            return remoteAddr[:idx]
        }
    }
    return remoteAddr
}

func main() {
    fmt.Println("olá")

    tr := &http.Transport{
        MaxIdleConns:       10,
        IdleConnTimeout:    30 * time.Second,
        DisableCompression: true,
    }
    client := &http.Client{Transport: tr}

    proxyHandler := proxy{client, "http://localhost:4000/blog/", "/blog/"}
    http.Handle("/blog/", proxyHandler)

    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (2)

Collapse
 
jessilyneh profile image
Jessilyneh

Perdi o horario 😭 nao fui a primeira a comentar hahaah

Collapse
 
henrique_loboweissmann profile image
Henrique Lobo Weissmann (MundoKico)

Que post foda!

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay