package openconnect import ( "net/http" "slices" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-openconnect" "github.com/sagernet/sing/common" "github.com/sagernet/sing/service" ) var _ adapter.OpenConnectEndpoint = (*Endpoint)(nil) func (e *Endpoint) OpenConnectStatus() adapter.OpenConnectStatus { var status adapter.OpenConnectStatus clientState := e.state.Load() authChallenge := e.client.PendingAuthChallenge() e.statusAccess.Lock() status.Error = e.terminalError e.statusAccess.Unlock() if authChallenge != nil { challenge := &adapter.OpenConnectAuthChallenge{ ID: authChallenge.ID, Banner: authChallenge.Banner, Message: authChallenge.Message, Error: authChallenge.Error, } if authChallenge.Form != nil { challenge.Form = &adapter.OpenConnectAuthForm{ Fields: common.Map(authChallenge.Form.Fields, func(field openconnect.AuthFormField) adapter.OpenConnectAuthFormField { return adapter.OpenConnectAuthFormField{ SubmissionKey: field.SubmissionKey, Name: field.Name, Label: field.Label, Kind: field.Kind, Value: field.Value, Options: common.Map(field.Options, func(choice openconnect.AuthFormChoice) adapter.OpenConnectAuthFormChoice { return adapter.OpenConnectAuthFormChoice{ Value: choice.Value, Label: choice.Label, } }), } }), } } if authChallenge.Browser != nil { var cacheID string cacheFile := service.FromContext[adapter.CacheFile](e.loopContext) if cacheFile != nil { cacheID = cacheFile.CacheID() } challenge.Browser = &adapter.OpenConnectBrowserRequest{ URL: authChallenge.Browser.URL, FinalURL: authChallenge.Browser.FinalURL, CookieNames: slices.Clone(authChallenge.Browser.CookieNames), EarlyCookieNames: slices.Clone(authChallenge.Browser.EarlyCookieNames), HeaderNames: slices.Clone(authChallenge.Browser.HeaderNames), CallbackURLPrefixes: slices.Clone(authChallenge.Browser.CallbackURLPrefixes), CacheID: cacheID, } } status.AuthChallenge = challenge } switch { case status.AuthChallenge != nil: status.State = adapter.OpenConnectStateAuthPending case status.Error != "": status.State = adapter.OpenConnectStateError case clientState.started && clientState.tunnelConfigured && e.client.Ready(): status.State = adapter.OpenConnectStateConnected tunnelInfo := clientState.tunnelInfo tunnelInfo.IPv4 = slices.Clone(tunnelInfo.IPv4) tunnelInfo.IPv6 = slices.Clone(tunnelInfo.IPv6) tunnelInfo.DNS = slices.Clone(tunnelInfo.DNS) status.TunnelInfo = &tunnelInfo default: status.State = adapter.OpenConnectStateConnecting } return status } func (e *Endpoint) StatusUpdated() <-chan struct{} { e.statusAccess.Lock() defer e.statusAccess.Unlock() return e.statusUpdated } func (e *Endpoint) CompleteAuthChallenge(challengeID string, response adapter.OpenConnectAuthResponse) error { var authResponse openconnect.AuthResponse if response.Form != nil { authResponse.Form = &openconnect.AuthFormResponse{Values: response.Form.Values} } if response.Browser != nil { browserResult := &openconnect.BrowserResult{ FinalURL: response.Browser.FinalURL, Cookies: common.Map(response.Browser.Cookies, func(cookie adapter.OpenConnectBrowserCookie) openconnect.BrowserCookie { return openconnect.BrowserCookie{Name: cookie.Name, Value: cookie.Value} }), Header: make(http.Header), } for _, header := range response.Browser.Headers { for _, value := range header.Values { browserResult.Header.Add(header.Name, value) } } authResponse.Browser = browserResult } return e.client.CompleteAuthChallenge(challengeID, authResponse) } func (e *Endpoint) CancelAuthChallenge(challengeID string) error { return e.client.CancelAuthChallenge(challengeID) } func (e *Endpoint) notifyStatusUpdated() { e.statusAccess.Lock() e.notifyStatusUpdatedLocked() e.statusAccess.Unlock() } func (e *Endpoint) notifyStatusUpdatedLocked() { close(e.statusUpdated) e.statusUpdated = make(chan struct{}) } func (e *Endpoint) setTerminalError(err error) { e.statusAccess.Lock() e.terminalError = err.Error() e.notifyStatusUpdatedLocked() e.statusAccess.Unlock() } func (e *Endpoint) watchAuthForms() { defer close(e.authFormLoopDone) var loggedAuthChallengeID string for { authChallengeUpdated := e.client.AuthChallengeUpdated() authChallenge := e.client.PendingAuthChallenge() if authChallenge != nil && authChallenge.ID != loggedAuthChallengeID { loggedAuthChallengeID = authChallenge.ID if authChallenge.Browser != nil { e.logger.Info("waiting for browser authentication") } else { e.logger.Info("waiting for authentication") } } e.notifyStatusUpdated() select { case <-e.loopContext.Done(): return case <-authChallengeUpdated: } } } func (e *Endpoint) watchActiveTransport() { defer close(e.activeTransportLoopDone) for { transportUpdated := e.client.ActiveTransportUpdated() transport := e.client.ActiveTransport() e.stateAccess.Lock() e.updateState(func(state *clientState) { state.tunnelInfo.Transport = transport }) e.stateAccess.Unlock() e.notifyStatusUpdated() select { case <-e.loopContext.Done(): return case <-transportUpdated: } } }