176 lines
5.3 KiB
Go
176 lines
5.3 KiB
Go
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:
|
|
}
|
|
}
|
|
}
|