-
Notifications
You must be signed in to change notification settings - Fork 23
pam: web app access #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pam: web app access #151
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| package webapp | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "bytes" | ||
| "context" | ||
| "crypto/tls" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net" | ||
| "net/http" | ||
| "time" | ||
|
|
||
| "github.com/Infisical/infisical-merge/packages/pam/session" | ||
| "github.com/google/uuid" | ||
| "github.com/rs/zerolog/log" | ||
| ) | ||
|
|
||
| type WebAppProxyConfig struct { | ||
| TargetAddr string | ||
| Protocol string // "http" or "https" | ||
| TLSConfig *tls.Config | ||
| SessionID string | ||
| SessionLogger session.SessionLogger | ||
| } | ||
|
|
||
| type WebAppProxy struct { | ||
| config WebAppProxyConfig | ||
| } | ||
|
|
||
| func NewWebAppProxy(config WebAppProxyConfig) *WebAppProxy { | ||
| return &WebAppProxy{config: config} | ||
| } | ||
|
|
||
| func (p *WebAppProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { | ||
| defer clientConn.Close() | ||
|
|
||
| sessionID := p.config.SessionID | ||
| l := log.With().Str("sessionID", sessionID).Logger() | ||
| defer func() { | ||
| if err := p.config.SessionLogger.Close(); err != nil { | ||
| l.Error().Err(err).Msg("Failed to close session logger") | ||
| } | ||
| }() | ||
|
|
||
| l.Info(). | ||
| Str("targetAddr", p.config.TargetAddr). | ||
| Str("protocol", p.config.Protocol). | ||
| Msg("New WebApp connection for PAM session") | ||
|
|
||
| reader := bufio.NewReader(clientConn) | ||
|
|
||
| // Loop to handle multiple HTTP requests on the same keep-alive connection | ||
| for { | ||
| select { | ||
| case <-ctx.Done(): | ||
| l.Info().Msg("Context cancelled, closing WebApp proxy connection") | ||
| return ctx.Err() | ||
| default: | ||
| } | ||
|
|
||
| // Read request in a goroutine so we can cancel it | ||
| reqCh := make(chan *http.Request, 1) | ||
| errCh := make(chan error, 1) | ||
|
|
||
| go func() { | ||
| req, err := http.ReadRequest(reader) | ||
| if err != nil { | ||
| errCh <- err | ||
| } else { | ||
| reqCh <- req | ||
| } | ||
| }() | ||
|
|
||
| var req *http.Request | ||
| select { | ||
| case <-ctx.Done(): | ||
| l.Info().Msg("Context cancelled while reading HTTP request") | ||
| return ctx.Err() | ||
| case err := <-errCh: | ||
| if errors.Is(err, io.EOF) { | ||
| l.Info().Msg("Client closed connection") | ||
| return nil | ||
| } | ||
| l.Error().Err(err).Msg("Failed to read HTTP request") | ||
| return fmt.Errorf("failed to read HTTP request: %w", err) | ||
| case req = <-reqCh: | ||
| // Successfully received request | ||
| } | ||
|
|
||
| requestId := uuid.New().String() | ||
| l.Info(). | ||
| Str("url", req.URL.String()). | ||
| Str("method", req.Method). | ||
| Str("reqId", requestId). | ||
| Msg("Received HTTP request from tunnel") | ||
|
|
||
| // Read request body | ||
| reqBody, err := io.ReadAll(req.Body) | ||
| if err != nil { | ||
| l.Error().Err(err).Msg("Failed to read request body") | ||
| writeErrorResponse(clientConn, "failed to read request body") | ||
| continue | ||
| } | ||
| req.Body.Close() | ||
|
|
||
| // Log the request | ||
| if logErr := p.config.SessionLogger.LogHttpEvent(session.HttpEvent{ | ||
| Timestamp: time.Now(), | ||
| RequestId: requestId, | ||
| EventType: session.HttpEventRequest, | ||
| URL: req.URL.String(), | ||
| Method: req.Method, | ||
| Headers: req.Header, | ||
| Body: reqBody, | ||
| }); logErr != nil { | ||
| l.Error().Err(logErr).Msg("Failed to log HTTP request event") | ||
| } | ||
|
|
||
| // Connect to target and forward request | ||
| targetURL := fmt.Sprintf("%s://%s%s", p.config.Protocol, p.config.TargetAddr, req.URL.RequestURI()) | ||
|
|
||
| proxyReq, err := http.NewRequest(req.Method, targetURL, bytes.NewReader(reqBody)) | ||
| if err != nil { | ||
| l.Error().Err(err).Msg("Failed to create proxy request") | ||
| writeErrorResponse(clientConn, "failed to create proxy request") | ||
| continue | ||
| } | ||
| proxyReq.Header = req.Header.Clone() | ||
|
|
||
| transport := &http.Transport{ | ||
| DisableKeepAlives: false, | ||
| MaxIdleConns: 10, | ||
| IdleConnTimeout: 30 * time.Second, | ||
| TLSClientConfig: p.config.TLSConfig, | ||
| } | ||
| client := &http.Client{ | ||
| Transport: transport, | ||
| Timeout: 30 * time.Second, | ||
| // Don't follow redirects — let the client handle them | ||
| CheckRedirect: func(req *http.Request, via []*http.Request) error { | ||
| return http.ErrUseLastResponse | ||
| }, | ||
| } | ||
|
Comment on lines
+132
to
+145
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HTTP transport and client created per request A new
The transport and client should be created once per proxy instance (e.g., in // In WebAppProxy struct
type WebAppProxy struct {
config WebAppProxyConfig
transport *http.Transport
client *http.Client
}
func NewWebAppProxy(config WebAppProxyConfig) *WebAppProxy {
transport := &http.Transport{
DisableKeepAlives: false,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: config.TLSConfig,
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return &WebAppProxy{config: config, transport: transport, client: client}
} |
||
|
|
||
| resp, err := client.Do(proxyReq) | ||
| if err != nil { | ||
| l.Error().Err(err).Msg("Failed to forward request to target") | ||
| writeErrorResponse(clientConn, fmt.Sprintf("failed to reach target: %s", err.Error())) | ||
| continue | ||
| } | ||
|
|
||
| // Tee the body for logging | ||
| var bodyCopy bytes.Buffer | ||
| resp.Body = struct { | ||
| io.ReadCloser | ||
| }{ | ||
| ReadCloser: io.NopCloser(io.TeeReader(resp.Body, &bodyCopy)), | ||
| } | ||
|
|
||
| // Write response back to tunnel client | ||
| resp.Header.Del("Connection") | ||
| if err := resp.Write(clientConn); err != nil { | ||
| if errors.Is(err, io.EOF) { | ||
| l.Info().Msg("Client closed connection during response write") | ||
| } else { | ||
| l.Error().Err(err).Msg("Failed to write response to connection") | ||
| } | ||
| resp.Body.Close() | ||
| return fmt.Errorf("failed to write response: %w", err) | ||
| } | ||
| resp.Body.Close() | ||
|
|
||
| // Log the response | ||
| if logErr := p.config.SessionLogger.LogHttpEvent(session.HttpEvent{ | ||
| Timestamp: time.Now(), | ||
| RequestId: requestId, | ||
| EventType: session.HttpEventResponse, | ||
| Status: resp.Status, | ||
| Headers: resp.Header, | ||
| Body: bodyCopy.Bytes(), | ||
| }); logErr != nil { | ||
| l.Error().Err(logErr).Msg("Failed to log HTTP response event") | ||
| } | ||
|
|
||
| l.Info(). | ||
| Str("reqId", requestId). | ||
| Str("status", resp.Status). | ||
| Msg("Forwarded response back to tunnel") | ||
| } | ||
| } | ||
|
|
||
| func writeErrorResponse(conn net.Conn, message string) { | ||
| errResp := fmt.Sprintf( | ||
| "HTTP/1.1 502 Bad Gateway\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", | ||
| message, | ||
| ) | ||
| conn.Write([]byte(errResp)) | ||
| } | ||
|
Comment on lines
+194
to
+200
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSON injection in error response body The For example, the call at line 150: writeErrorResponse(clientConn, fmt.Sprintf("failed to reach target: %s", err.Error()))could produce: Use func writeErrorResponse(conn net.Conn, message string) {
type errBody struct {
Message string `json:"message"`
}
body, _ := json.Marshal(errBody{Message: "gateway: " + message})
errResp := fmt.Sprintf(
"HTTP/1.1 502 Bad Gateway\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s",
len(body), body,
)
conn.Write([]byte(errResp))
} |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No request body size limit (potential DoS)
io.ReadAll(req.Body)reads the entire request body into memory with no upper bound. A malicious or misbehaving client could send an extremely large body (e.g., a multi-GB upload) causing memory exhaustion on the gateway process. Consider wrapping the body withhttp.MaxBytesReaderbefore reading:Note:
http.MaxBytesReaderrequires anhttp.ResponseWriteras its first argument; if that doesn't fit here, you can instead useio.LimitReader: