mps
mps copied to clipboard
Migrate request context on mitmHandler
Hi, so I have a TLS listener with my mps proxy.
During handshake, I generate a variable and need to pass this to my .OnRequest(...)
function.
However, anything I set on req Context with the following function is not propagated?
func getInsertor(proxy *mps.HttpProxy) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req * http.Request) {
logger := log.WithField("ip", req.RemoteAddr).WithField("host", req.Host)
testID, err := pkg.ExtractTestID(req)
if testID <= 0 || err != nil {
logger.WithError(err).Error("could not extract testid")
_, _ = rw.Write([]byte("invalid credentials provided"))
rw.WriteHeader(http.StatusForbidden)
return
}
proxy.ServeHTTP(rw, req.WithContext(context.WithValue(req.Context(), "testid", testID)))
})
}
proxyInstance := mps.NewHttpProxy()
mitmHandler := mps.NewMitmHandler()
mitmHandler.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response){
testID, ok := req.Context().Value("testid").(uint64) // This will always be nil/zero
Same with ctx.Request.Context()
or ctx.Context
And strangely enough, any headers set in getInsertor
does not seem to propagate to the OnRequest
.
Edit: I guess this is because getInsertor
only occurs on the CONNECT
call, and does not modify the actual request embedded within.
You should instantiate mitmHandler as follows:
proxyInstance := mps.NewHttpProxy()
// Use the same mps.Context instance
mitmHandler := mps.NewMitmHandlerWithContext(proxyInstance.Ctx)
// Process the CONNECT request
proxyInstance.HandleConnect = mitmHandler
Because middleware is implemented by mps.Context
, Filter
is based middleware
As middleware is implemented by mps.Context
, middleware
and filter
won't work if you don't use the same mps.Context
, which is a bit bad, so I wanted to find a better way to middleware
.
Ahh. not quite sure how I missed that, thanks @telanflow ! Loving the rewrite.
@telanflow can you elaborate on which context I should set/get the value to get it to pass from CONNECT
to the followup GET
or POST
request?
It doesn't seem to pass on either req.Context
or ctx.Context
.
I am using mitmHandler.OnRequest().DoFunc
, but same with proxyInstance.OnRequest().DoFunc
.
if req.Method == http.MethodConnect {
testID, err = pkg.ExtractTestID(req)
} else {
// if not, we really need a testID to be present in the context to continue
var ok bool
testID, ok = ctx.Context.Value("testid").(uint64)
if !ok { err = errors.Errorf("invalid testid in context: %+v", req.Context().Value("testid")) }
}
if testID <= 0 || err != nil {
logger.WithError(err).Error("could not extract testid")
return req, newHttpResponse(req, http.StatusForbidden, "Forbidden", "invalid credentials provided")
}
if req.Method == http.MethodConnect {
ctx.Context = context.WithValue(ctx.Context, "testid", testID)
return req.WithContext(context.WithValue(req.Context(), "testid", testID)), nil
}
I tried
req = req.WithContext(context.WithValue(req.Context(), "testid", testID))
ctx = ctx.WithRequest(req)
ctx.Context = context.WithValue(ctx.Context, "testid", testID)
return req, nil
I'm sorry, i went out to play on China National Day.
you can do that:
// set value
ctx.Context = context.WithValue(ctx.Context, "testid", testID)
// get value
ctx.Context.Value("testId")
Or
// set value
req.WithContext(context.WithValue(req.Context(), "testid", testID))
// Get value
req.Context().Value("testId")
@telanflow is it possible it won't propagate to the actual requests because the GET request is inside the CONNECT tunnel, hence a different proxy context perhaps?
e.g.
CONNECT website.com:443
// setting context
GET /foo (in CONNECT tunnel)
// getting context -> nil
I am doing it as you stipulated:
ctx.Context = context.WithValue(ctx.Context, "testid", testID)
yes, It's best to set it above the request Context
@telanflow can you perhaps elaborate? Not sure what you mean;
proxyInstance.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
logger := log.WithField("ip", req.RemoteAddr)
var testID uint64
var err error
// if this is a CONNECT, scrape the testID from the request TLS metadata
if req.Method == http.MethodConnect {
testID, err = pkg.ExtractTestID(req)
} else {
// if not, we really need a testID to be present in the context to continue
var ok bool
testID, ok = ctx.Request.Context().Value("testid").(uint64)
if !ok { err = errors.Errorf("invalid testid in context: %+v", req.Context().Value("testid")) }
}
if testID <= 0 || err != nil {
logger.WithError(err).Error("could not extract testid")
return req, newHttpResponse(req, http.StatusForbidden, "Forbidden", "invalid credentials provided")
}
// if this is a request to our proxy, set context and pass for the subsequent real requests to work
if req.Method == http.MethodConnect {
req = req.WithContext(context.WithValue(req.Context(), "testid", testID))
ctx = ctx.WithRequest(req)
ctx.Context = context.WithValue(ctx.Context, "testid", testID)
return req, nil
}
....
}
Each request spawns a new Handler for the execution middleware (MITMHandler, TunnelHandler)
ctx := mitm.Ctx.WithRequest(req)
resp, err := ctx.Next(req)
eg.
https://github.com/telanflow/mps/blob/00583b5f72a7c282848f6288036ca915813408d1/mitm_handler.go#L94-L108
https://github.com/telanflow/mps/blob/00583b5f72a7c282848f6288036ca915813408d1/tunnel_handler.go#L44-L58
But proxy context should pass on between those handlers, right?
Take a look at the latest submission so that should work @hazcod
@telanflow thanks for the quick response, but still returns nil:
proxyInstance.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
logger := log.WithField("ip", req.RemoteAddr)
var testID uint64
var err error
// if this is a CONNECT, scrape the testID from the request TLS metadata
if req.Method == http.MethodConnect {
testID, err = pkg.ExtractTestID(req)
} else {
// if not, we really need a testID to be present in the context to continue
var ok bool
log.Info(req.Context().Value("testid"))
testID, ok = ctx.Context.Value("testid").(uint64)
if !ok { err = errors.Errorf("invalid testid in context: %+v", req.Context().Value("testid")) }
}
if testID <= 0 || err != nil {
logger.WithError(err).Error("could not extract testid")
return req, newHttpResponse(req, http.StatusForbidden, "Forbidden", "invalid credentials provided")
}
// if this is a request to our proxy, set context and pass for the subsequent real requests to work
if req.Method == http.MethodConnect {
ctx.Context = context.WithValue(ctx.Context, "testid", testID)
return req, nil
}
...
}
Returns:
proxy_1 | [00] INFO[0004] validating fresh client test_id=595825677061881857 (CONNECT)
proxy_1 | [00] INFO[0004] <nil> (GET)
proxy_1 | [00] ERRO[0004] could not extract testid error="invalid testid in context: <nil>" (GET)
Is this row not getting the value?
log.Info(req.Context().Value("testid"))
Where do they set a testid?
one thing to note:
set testid
middleware has to come in front of OnRequest
@telanflow I've split it up to make it more clear;
proxyInstance.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
logger := log.WithField("ip", req.RemoteAddr).WithField("method", req.Method)
if req.Method != http.MethodConnect {
return req, nil
}
// if this is a CONNECT, scrape the testID from the request TLS metadata
testID, err := pkg.ExtractTestID(req)
if err != nil {
logger.WithError(err).Error("could not extract testid")
return req, newHttpResponse(req, http.StatusForbidden, "Forbidden", "invalid credentials provided")
}
ctx.Context = context.WithValue(ctx.Context, "testid", testID)
return req, nil
})
proxyInstance.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
logger := log.WithField("ip", req.RemoteAddr).WithField("method", req.Method)
// if not, we really need a testID to be present in the context to continue
var ok bool
logger.Info(req.Context().Value("testid"), ctx.Context.Value("testid"))
testID, ok := ctx.Context.Value("testid").(uint64)
if !ok { err = errors.Errorf("invalid testid in context: %+v", req.Context().Value("testid")) }
if testID <= 0 || err != nil {
logger.WithError(err).Error("could not extract testid")
return req, newHttpResponse(req, http.StatusForbidden, "Forbidden", "invalid credentials provided")
}
logger = logger.WithField("testid", testID)
// if this is a request to our proxy, set context and pass for the subsequent real requests to work
if req.Method == http.MethodConnect {
return req, nil
}
return req, nil
})
And the logs, showing the testID context being set the first time
proxy_1 | [00] INFO[0010] validating fresh client ip="172.19.0.1:33034" method=CONNECT test_id=595825677061881857
proxy_1 | [00] INFO[0010] <nil> 595825677061881857 ip="172.19.0.1:33034" method=CONNECT
proxy_1 | [00] INFO[0010] <nil> <nil> ip="172.19.0.1:33034" method=GET
proxy_1 | [00] ERRO[0010] could not extract testid error="invalid testid in context: <nil>" ip="172.19.0.1:33034" method=GET
@telanflow : this made me think, will Context
be propagated through HandleConnect
and HtttpHandler
?
proxyInstance := mps.NewHttpProxy()
mitmHandler := mps.NewMitmHandlerWithContext(proxyInstance.Ctx)
proxyInstance.HandleConnect = mitmHandler
proxyInstance.HttpHandler = mitmHandler
proxyInstance.OnRequest().DoFunc(...)
Hmm, same result with:
proxyInstance := mps.NewHttpProxy()
proxyInstance.HandleConnect = mps.NewMitmHandlerWithContext(proxyInstance.Ctx)
proxyInstance.UseFunc(func(req *http.Request, ctx *mps.Context) (*http.Response, error) {
...
}
@telanflow sorry for bugging, but any idea?
I'm sorry. I've been a little busy lately.
The code is as follows:
func main() {
proxy := mps.NewHttpProxy()
proxy.HandleConnect = mps.NewMitmHandlerWithContext(proxy.Ctx)
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
// request Context with value
reqCtx := context.WithValue(req.Context(), "testid", "1")
req = req.WithContext(reqCtx)
// mps.Context with value
ctx.Context = context.WithValue(ctx.Context, "testid", "2")
return req, nil
})
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
// get req.Context() value
reqVal := req.Context().Value("testid")
log.Printf("Method: %s URL: %s req:testid: %v", req.Method, req.URL, reqVal)
// get mps.Context.Context value
ctxVal := ctx.Context.Value("testid")
log.Printf("Method: %s URL: %s ctx:testid: %v", req.Method, req.URL, ctxVal)
return req, nil
})
_ = http.ListenAndServe(":8888", proxy)
}
logs:
2020/10/14 17:01:25 Method: CONNECT URL: //httpbin.org:443 req:testid: 1
2020/10/14 17:01:25 Method: CONNECT URL: //httpbin.org:443 ctx:testid: 2
2020/10/14 17:01:25 Method: GET URL: https://httpbin.org:443/get req:testid: 1
2020/10/14 17:01:25 Method: GET URL: https://httpbin.org:443/get ctx:testid: 2
@telanflow thanks for the support, but this not seem to propagate with your latest mps release.
Could it be because of my TLS listener with http.Serve(liTLS, proxyInstance)
?
liTLS := tls.NewListener(li, &tls.Config{
Certificates: []tls.Certificate{*serverCrt, caCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: clientCACertPool,
PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
Renegotiation: tls.RenegotiateNever,
SessionTicketsDisabled: true,
NextProtos: []string{"http/1.1", "http/1.0"},
})
Because I get context.Background()
for req.Context()
and ctx.Context()
in the second DoFunc
: WARN[0002] testid not found in context ip="172.19.0.1:51254" method=GET proxy_ctx=context.Background req_ctx=context.Background testid=0
EDIT: in your case, you are -always- setting testID because your are not skipping the first handler if is -not- CONNECT.
try with:
func main() {
proxy := mps.NewHttpProxy()
proxy.HandleConnect = mps.NewMitmHandlerWithContext(proxy.Ctx)
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
if req.Method != http.MethodConnect { return req, nil }
// request Context with value
reqCtx := context.WithValue(req.Context(), "testid", "1")
req = req.WithContext(reqCtx)
// mps.Context with value
ctx.Context = context.WithValue(ctx.Context, "testid", "2")
return req, nil
})
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
if req.Method == http.MethodConnect { return req, nil }
// get req.Context() value
reqVal := req.Context().Value("testid")
log.Printf("Method: %s URL: %s req:testid: %v", req.Method, req.URL, reqVal)
// get mps.Context.Context value
ctxVal := ctx.Context.Value("testid")
log.Printf("Method: %s URL: %s ctx:testid: %v", req.Method, req.URL, ctxVal)
return req, nil
})
_ = http.ListenAndServe(":8888", proxy)
}
you should know that an HTTP request is executed sequentially to the middleware, and if middleware 1 skips it, middleware 2 will not get the value.
GET -> middleware1 (context set value) -> middleware2 (get context value)
POST -> middleware1 (jump) -> middleware2 (not get context value)
CONNECT -> middleware1 -> middleware2
You can set MPS Proxy as a property of another object to uniformly set your own values.
package main
import (
"context"
"errors"
"github.com/telanflow/mps"
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
type Srv struct {
server *http.Server
proxyHandler *mps.HttpProxy
ctx context.Context
signChan chan os.Signal
}
func NewSrv(addr string) *Srv {
proxyHandler := mps.NewHttpProxy()
proxyHandler.HandleConnect = mps.NewMitmHandlerWithContext(proxyHandler.Ctx)
return &Srv{
signChan: make(chan os.Signal),
proxyHandler: proxyHandler,
ctx: context.Background(),
server: &http.Server{
Addr: addr,
Handler: proxyHandler,
},
}
}
func (s *Srv) Run() {
// proxy on request
s.proxyHandler.OnRequest().DoFunc(s.handleRequest1)
s.proxyHandler.OnRequest().DoFunc(s.handleRequest2)
// listen quit notify
signal.Notify(s.signChan, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGQUIT)
// start server
go func() {
log.Println("proxy server started")
err := s.server.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
return
}
if err != nil {
s.signChan <- syscall.SIGKILL
log.Fatalf("Http Fail: %v", err)
}
}()
// stop
<-s.signChan
s.server.Shutdown(context.Background())
log.Fatal("proxy server exit.")
}
// Handle Request 1
func (s *Srv) handleRequest1(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
if req.Method != http.MethodConnect { return req, nil }
// mps.Context with value
s.ctx = context.WithValue(s.ctx, "testid", "3")
return req, nil
}
// Handle Request 2
func (s *Srv) handleRequest2(req *http.Request, ctx *mps.Context) (*http.Request, *http.Response) {
if req.Method == http.MethodConnect { return req, nil }
// get mps.Context.Context value
ctxVal := s.ctx.Value("testid")
log.Printf("Method: %s URL: %s s.ctx:testid: %v", req.Method, req.URL, ctxVal)
return req, nil
}
func main() {
// start proxy server
srv := NewSrv("127.0.0.1:8888")
srv.Run()
}
logs:
2020/10/14 21:43:02 proxy server started
2020/10/14 21:43:10 Method: GET URL: https://httpbin.org:443/get s.ctx:testid: 3
2020/10/14 21:43:52 proxy server exit.
Sorry, I'm not good at English, haha
@telanflow but wouldn't that create a race condition on Srv mps.Context
?
Also, the first middleware will only need to run on CONNECT while a second can only verify a non-CONNECT.
I'm sorry, I don't really understand what you want to do. Can you describe it?
can you give a complete example of a failed requirement?
@hazcod
@telanflow yeah sorry for not specifying myself clearly enough. This is the problem case: https://gist.github.com/hazcod/20e5050c5098bcb1d4da087254b0821c So test it like this:
% go run main.go
INFO[0000] running listener="127.0.0.1:9999"
Issue a test request:
% ALL_PROXY="http://localhost:9999" curl -k -L https://google.be/
out of scope request%
And you'll see the issue:
INFO[0001] request method=CONNECT url="//google.be:443"
INFO[0001] set testid ip="127.0.0.1:58652" method=CONNECT testid=foo
INFO[0001] request method=GET url="https://google.be:443/"
WARN[0001] testid not found in context ip="127.0.0.1:58652" method=GET proxy_ctx=context.Background req_ctx=context.Background testid=0
but if you would use your Srv
solution, if you have concurrent requests, they can both write/read to the same mps.Context
object, causing a race condition.
you should be aware of the HTTPS proxy process:
1 step:
brower <-> CONNECT request <-> mps (SSL)
2 step:
brower <-> (GET | POST and more method) <-> mps <-> origin
the lifetime of the Context and the request binding, you can't get the Context from the last request in the next request.
CONNECT request【Context 1】 -> mps
GET request【Context 2】 -> mps
you can avoid concurrency problems by lock
Hmm, but if the CONNECT
connection stays open the lock will never release.