
Una biblioteca de proxy HTTP/HTTPS personalizable para Go que admite reenvío regular, túneles CONNECT, intercepción TLS MITM y modificación programática de solicitudes/respuestas.
GoProxy es una librería para crear un servidor proxy HTTP/HTTPS personalizado usando
Go (también conocido como Golang), con varios ajustes configurables disponibles.
El objetivo de este proyecto es ofrecer un servidor proxy optimizado, utilizable con
una cantidad razonable de tráfico, pero personalizable y programable.
El proxy en sí mismo es simplemente un handler de net/http, por lo que puedes añadir múltiples
middlewares (recuperación de pánico, registro de logs, compresión, etc.) sobre él. Puede
integrarse fácilmente con cualquier otra librería de red HTTP.
Para usar goproxy, se debe configurar el navegador (o cualquier otro cliente)
para que use goproxy como proxy HTTP.
Así es como se hace en Chrome
y en Firefox.
Si decides empezar con el ejemplo base, la URL que debes usar como
proxy es localhost:8080, que es la predeterminada en nuestro ejemplo.
También tienes que confiar
en el certificado CA del proxy, para evitar cualquier problema de certificado en los clientes.
hosts específicos, con una única comparación de igualdad o con evaluación de expresiones regularespeticiones y respuestas antes de enviarlas al navegadorhttp.Transport personalizado para realizar peticiones al servidor de destinocaché de certificados MITM, para reutilizarlos más tarde en otras peticiones al mismo host, ahorrando así CPU. No está habilitada por defecto, ¡pero deberías usarla en producción!handler personalizado, cuando el destino es una ruta relativa (p. ej. /ping)Loggerdeshabilitar la canonicalización de las cabeceras HTTP de las peticiones, estableciendo en true¿Tu empresa usa GoProxy? ¡Ayúdanos a mantener el proyecto activo y en buen estado! Apoyar a GoProxy nos permite dedicar más tiempo a corregir errores y añadir nuevas funciones. A cambio, si eliges un plan Gold Supporter o Enterprise, mostraremos con orgullo el logo de tu empresa aquí.
Si necesitas integrar GoProxy en tu proyecto, o necesitas algunas funciones personalizadas que mantener en tu fork, puedes contactar por correo electrónico con Erik (el mantenedor actual) y acordar con él cómo puede ayudarte como consultor independiente de pago.
Si tienes algún problema, sugerencia o encuentras un error, no dudes en
ponerte en contacto abriendo un issue de GitHub.
Este es un proyecto open source gestionado por voluntarios, y estaremos
encantados de discutir cualquier cosa que pueda mejorarlo.
Asegúrate de explicarlo todo, incluyendo el motivo del issue
y lo que quieres cambiar, para que el problema sea más fácil de entender.
También puedes abrir directamente un Pull Request, si es un cambio de código pequeño, pero
debes explicar todo en la descripción.
Si abres un pull request llamado refactoring con 5,000 líneas cambiadas,
no lo fusionaremos... :D
El código de este proyecto se publica bajo la licencia BSD 3-Clause,
lo que lo hace útil también para usos comerciales.
Así que has introducido e integrado GoProxy en uno de tus proyectos personales o en un proyecto de la empresa en la que trabajas.
Estaremos encantados de conocer nuevas soluciones creativas hechas con esta librería,
así que no dudes en contactar por correo electrónico con el mantenedor indicado arriba para explicar
por qué encontraste este proyecto útil para tus necesidades.
Si has firmado un Acuerdo de Confidencialidad (NDA) con la empresa, puedes
proponerles que escriban un post en su sitio web oficial sobre
este tema, de modo que esta información será pública por decisión suya, y podrás
compartir el enlace del post con nosotros :)
El propósito de los casos de estudio es compartir con la comunidad por qué todos los
contribuyentes de este proyecto están mejorando el mundo con su ayuda y
qué está construyendo la gente con él.
El código base utiliza una comprobación automática de lint sobre el código de tu Pull Request. Antes de abrirlo, deberías comprobar si tus cambios lo respetan, ejecutando el linter en tu máquina local, para no llevarte ninguna sorpresa.
Para instalar el linter:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
Esto creará un ejecutable en tu carpeta $GOPATH/bin
($GOPATH es una variable de entorno; normalmente
su valor equivale a ~/go; comprueba su valor en tu máquina si no
estás seguro de ello).
Asegúrate de incluir la carpeta bin en el path de tu shell, para poder
usar directamente el comando golangci-lint run.
Para hacerte una idea de goproxy, aquí tienes un proxy HTTP/HTTPS básico
que simplemente reenvía los datos al destino:
package main
import (
"log"
"net/http"
"github.com/elazarl/goproxy"
)
func main() {
proxy := goproxy.NewProxyHttpServer()
proxy.Verbose = true
log.Fatal(http.ListenAndServe(":8080", proxy))
}
Esta línea añadirá la cabecera X-GoProxy: yxorPoG-X a todas las peticiones enviadas a través del proxy,
antes de enviarlas al destino:
proxy.OnRequest().DoFunc(
func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) {
r.Header.Set("X-GoProxy","yxorPoG-X")
return r,nil
})
Cuando la entrada de OnRequest() está vacía, la función especificada en DoFunc
procesará todas las peticiones entrantes al proxy. En este caso, añadirá
una cabecera a la petición y la devolverá al llamante.
El proxy enviará la petición modificada al destino.
También puedes usar Do en lugar de DoFunc, si implementas la interfaz
especificada en tu tipo.
⚠️ Nota: devolvimos un valor nil como respuesta. Si la respuesta devuelta no es nil, goproxy descartará la petición y enviará la respuesta especificada al cliente.
Rechaza las conexiones a www.reddit.com entre las 8 y las 17 en la zona horaria local del servidor:
proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc(
func(req *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) {
if h,_,_ := time.Now().Clock(); h >= 8 && h <= 17 {
resp := goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "Don't waste your time!")
return req, resp
}
return req, nil
})
DstHostIs devuelve una ReqCondition, que es una función que recibe un *http.Request
y devuelve un booleano que comprueba si la petición satisface la condición (y que será procesada).
DstHostIs("www.reddit.com") devolverá una ReqCondition que devuelve true
cuando la petición está dirigida a "www.reddit.com".
La comprobación de igualdad del host es insensible a mayúsculas y minúsculas (case-insensitive), para reflejar el comportamiento de los resolvedores
DNS, de modo que incluso si el usuario escribe "www.rEdDit.com", la comparación
satisfará la condición.
Cuando la hora está entre las 8:00 y las 17:59, devolvemos directamente
una respuesta en DoFunc(), de modo que el destino remoto no recibirá la
petición y el cliente recibirá la respuesta "Don't waste your time!".
import "github.com/elazarl/goproxy"
Hay algunos ejemplos de uso del proxy en la carpeta examples, que
cubren los casos más comunes. ¡Échales un vistazo y buena suerte!
Hay 3 tipos diferentes de handlers para manipular el comportamiento del proxy, como se indica a continuación:
// handler called after receiving HTTP CONNECT from the client, and
// before proxy establishes connection with the destination host
httpsHandlers []HttpsHandler
// handler called before proxy sends HTTP request to destination host
reqHandlers []ReqHandler
// handler called after proxy receives HTTP Response from destination host,
// and before proxy forwards the Response to the client
respHandlers []RespHandler
Dependiendo de lo que quieras manipular, las formas de añadir handlers a cada una de las listas anteriores son:
// Add handlers to httpsHandlers
proxy.OnRequest(some ReqConditions).HandleConnect(YourHandlerFunc())
// Add handlers to reqHandlers
proxy.OnRequest(some ReqConditions).Do(YourReqHandlerFunc())
// Add handlers to respHandlers
proxy.OnResponse(some RespConditions).Do(YourRespHandlerFunc())
Ejemplo:
// This rejects the HTTPS request to *.reddit.com during HTTP CONNECT phase.
// Reddit URL check is case-insensitive because of (?i), so the block will work also if the user types something like rEdDit.com.
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("(?i)reddit.*:443$"))).HandleConnect(goproxy.AlwaysReject)
// Be careful about this example! It shows you a common error that you
// need to avoid.
// This will NOT reject the HTTPS request with URL ending with .gif because,
// if the scheme is HTTPS, the proxy will receive only URL.Hostname
// and URL.Port during the HTTP CONNECT phase.
proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).HandleConnect(goproxy.AlwaysReject)
// To fix the previous example, here there is the correct way to manipulate
// an HTTP request using URL.Path (target path) as a condition.
proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).Do(YourReqHandlerFunc())
Esto rechaza la petición HTTPS a *.reddit.com durante la fase de HTTP CONNECT. La comprobación de la URL de Reddit no distingue mayúsculas de minúsculas gracias a (?i), por lo que el bloqueo funcionará también si el usuario escribe algo como rEdDit.com.
¡Ten cuidado con este ejemplo! Te muestra un error común que debes evitar. Esto NO rechazará la petición HTTPS con URL que termine en .gif porque, si el esquema es HTTPS, el proxy recibirá solo URL.Hostname y URL.Port durante la fase de HTTP CONNECT.
Para corregir el ejemplo anterior, aquí está la forma correcta de manipular una petición HTTP usando URL.Path (ruta de destino) como condición.
Si se produce un error al manejar una petición a través del proxy, por defecto
el proxy devuelve el error HTTP 500 (Internal Server Error) con el mensaje de error como contenido del body.
Si quieres anular este comportamiento, puedes definir tu propio
RespHandler que cambie la respuesta de error.
Entre los parámetros del contexto, ctx.Error contiene el error ocurrido,
si lo hay, o el valor nil, si no ocurrió ningún error.
Puedes manejarlo como quieras, incluso devolviendo un JSON personalizado como cuerpo. Ejemplo de un manejador de errores:
proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
var dnsError *net.DNSError
if errors.As(ctx.Error, &dnsError) {
// Do not leak our DNS server's address
dnsError.Server = "<server-redacted>"
return goproxy.NewResponse(ctx.Req, goproxy.ContentTypeText, http.StatusBadGateway, dnsError.Error())
}
return resp
})
Si se produce un error al enviar datos al servidor remoto de destino (o al
cliente del proxy), se llamará a proxy.ConnectionErrHandler para manejar el
error, si está definido; de lo contrario, se usará un handler por defecto.
El error se pasa como parámetro de función y no dentro del contexto del proxy,
por lo que no tienes que comprobar el campo ctx.Error en este handler.
En este handler tienes acceso a la conexión bruta con el cliente del proxy
(como un io.Writer), por lo que podrías enviar cualquier dato HTTP a través de ella,
si fuera necesario, que contenga los datos del error.
No hay garantía de que la conexión no se haya cerrado ya, por lo que
Write() podría devolver un error.
La conexión será cerrada automáticamente por la librería del proxy después de la
llamada al manejador de errores, así que no tienes que preocuparte por ello.
Este proyecto fue creado hace 10 años y ha alcanzado una etapa de
madurez. Puede usarse de forma segura en producción, y muchos proyectos
ya lo hacen.
Si en el futuro hay algún cambio incompatible (breaking change), se publicará una nueva versión del
módulo Go (p. ej. v2).
PreventCanonicalization