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)) } 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)) } Isso gera esse erro: 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()) 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)) Torna o servidor acessível através do meu celular, na mesma rede: Porém, uma pequena mudança torna isso inacessível: E qual foi a mudança que fez isso? Simplesmente colocar como o endereço localhost:8080: log.Fatal(http.ListenAndServe("localhost:8080", nil)) 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 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 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 E o header pode aparecer várias vezes durante

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, ouQUERY
, 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úblicoServeHTTP
adequado para a interfacehttp.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))
}
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))
}
Isso gera esse erro:
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())
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))
Torna o servidor acessível através do meu celular, na mesma rede:
Porém, uma pequena mudança torna isso inacessível:
E qual foi a mudança que fez isso? Simplesmente colocar como o endereço localhost:8080
:
log.Fatal(http.ListenAndServe("localhost:8080", nil))
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
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
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
E o header pode aparecer várias vezes durante a listagem:
header1: value
header2: value for 2
header1: other value for 1
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"
}
ou assim:
{
header1: "value, other value for 1",
header2: "value for 2"
}
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))
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
leitura parcial
e aí? body <"tralala">
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)
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
. TheHeader
map also is the mechanism with whichHandler
implementations can set HTTP trailers.Changing the header map after a call to
ResponseWriter.WriteHeader
(orResponseWriter.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))
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
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
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)
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{}
}
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))
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
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")
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}
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)
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))
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
}
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))
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)
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)
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))
}