Skip to content

Commit

Permalink
Update building process, refactor code, various cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippePitzClairoux committed Feb 5, 2024
1 parent d23fb49 commit 3cb755d
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 152 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out/
.idea/
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ FROM golang:latest

WORKDIR /opt

# Set default OS to linux and architecture to amd64
ENV GOOS=linux
ENV GOARCH=amd64

# mount project in workdir
ADD ./ ./
ADD internal internal
ADD go.mod go.mod
ADD go.sum go.sum
ADD OpenConnectSSO.go OpenConnectSSO.go

# create output dir
RUN mkdir out
Expand Down
75 changes: 17 additions & 58 deletions OpenConnectSSO.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"go-openconnect-sso/internal"
"log"
"net/http"
url2 "net/url"
"os"
"os/exec"
)
Expand All @@ -18,33 +17,17 @@ var server = flag.String("server", "", "server to connect to via openconnect")

func main() {
flag.Parse()
url := "https://" + *server
ctx, cancel := chromedp.NewContext(context.Background())

client := internal.NewHttpClient(*server)
defer cancel()
channel := make(chan string)
cookieFound := make(chan string)
targetUrl := internal.GetActualUrl(client, *server)
samlAuth := internal.AuthenticationInit(client, targetUrl)
ctx, closeBrowser := internal.CreateBrowserContext()

url = getActualUrl(client, *server)
samlAuth := internal.PostAuthInitRequest(client, url)
log.Println("Starting goroutine that searches for authentication cookie ", samlAuth.Auth.SsoV2TokenCookieName)
go internal.BrowserCookieFinder(ctx, cookieFound, samlAuth.Auth.SsoV2TokenCookieName)

opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false), // Set headless mode to false
chromedp.Flag("disable-gpu", false),
)

// Create an allocator
allocCtx, cancel := chromedp.NewExecAllocator(ctx, opts...)
defer cancel()

// Create a new browser context
ctx, cancel = chromedp.NewContext(allocCtx)
defer cancel()

//go listenBrowser(&ctx)
go internal.CookieFinder(ctx, channel, samlAuth.Auth.SsoV2TokenCookieName)
fmt.Println(samlAuth.Auth.SsoV2TokenCookieName)
// initialize login and wait for user to finish auth
log.Println("Open browser and navigate to SSO login page : ", samlAuth.Auth.SsoV2Login)
err := chromedp.Run(ctx, chromedp.Tasks{
chromedp.Navigate(samlAuth.Auth.SsoV2Login),
})
Expand All @@ -53,40 +36,15 @@ func main() {
}

// consume cookie and connect to vpn
consumeCookies(channel, client, samlAuth, url, cancel)
}

// thanks chatGPT for the refactoring of this function.
// i'm way too high to be doing this right now...!
func getActualUrl(client *http.Client, url string) string {
// Ensure the URL is properly formatted with the HTTPS protocol.
uri, err := url2.ParseRequestURI("https://" + url)
if err != nil {
log.Fatalf("Invalid URL format: %v", err)
}

// Create a new HTTP request.
r, err := http.NewRequest("GET", uri.String(), nil)
if err != nil {
log.Fatalf("Could not create HTTP request: %v", err)
}

// Correctly set the Host header to the domain part without the protocol.
r.Host = uri.Host

do, err := client.Do(r)
if err != nil {
log.Println(err)
log.Fatal("Could not perform request to fetch actual URL")
}

return do.Request.URL.String()
startVpnOnLoginCookie(cookieFound, client, samlAuth, targetUrl, closeBrowser)
}

func consumeCookies(channel chan string, client *http.Client, auth *internal.AuthInitRequestResponse, _url string, cancel context.CancelFunc) {
for cookie := range channel {
token, cert := internal.PostAuthConfirmLogin(client, auth, cookie, _url)
cancel() // close browser
// startVpnOnLoginCookie waits to get a cookie from the authenticationCookies channel before confirming
// the authentication process (to get token/cert) and then starting openconnect
func startVpnOnLoginCookie(authenticationCookies chan string, client *http.Client, auth *internal.AuthenticationInitExpectedResponse, targetUrl string, closeBrowser context.CancelFunc) {
for cookie := range authenticationCookies {
token, cert := internal.AuthenticationConfirmation(client, auth, cookie, targetUrl)
closeBrowser() // close browser

command := exec.Command("sudo",
"openconnect",
Expand All @@ -98,13 +56,14 @@ func consumeCookies(channel chan string, client *http.Client, auth *internal.Aut
token,
"--servercert",
cert,
_url,
targetUrl,
)

log.Println("Command : ", command.String())
command.Stdout = os.Stdout
command.Stderr = os.Stdout
command.Stdin = os.Stdin

log.Println("Starting openconnect: ", command.String())
err := command.Run()
if err != nil {
log.Fatal("Could not start command : ", err)
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ In order to build this project, you only need docker.
./build.sh
```

You can set env variables with the name GOOS and GOARCH in order to change
the OS and cpu architecture of the build.
```bash
GOOS=darwin GOARCH=arm ./build.sh
```

## Usage
```
./go-openconnect-sso --server vpn.host.com
Expand Down
26 changes: 24 additions & 2 deletions build.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
#!/usr/bin/env bash

# Remove old instance
echo -e "Removing old builds (if present)\n"
docker rm builder
docker rmi openconnect-sso-builder
rm -rf out

# Tag dockerfile
echo -e "Build and tag container\n"
docker build -t openconnect-sso-builder .

OVERRIDE_GO_VARIABLES=""

if [ -n "${GOOS}" ]; then
OVERRIDE_GO_VARIABLES+=" -e \"GOOS=${GOOS}\""
fi

if [ -n "${GOARCH}" ]; then
OVERRIDE_GO_VARIABLES+=" -e \"GOARCH=${GOARCH}\""
fi

# Run builder to create executable
docker run --name builder openconnect-sso-builder
echo -e "Run container and assign it the name \"builder\"\n"
sh -c "docker run ${OVERRIDE_GO_VARIABLES} --name builder openconnect-sso-builder"

# Copy output directory to current directory
docker cp builder:/opt/out ./out
echo -e "Copy executable from container to ./out directory\n"
docker cp builder:/opt/out ./out

#echo "Copying ./out/go-openconnect-sso to /usr/bin/go-openconnect-sso"
#sudo cp ./out/go-openconnect-sso /usr/bin/go-openconnect-sso
99 changes: 59 additions & 40 deletions internal/http-calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,51 @@ package internal
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"regexp"
)

// Data extractors used instead of creating a struct to parse the content of an XML in AuthenticationConfirmation
var sessionToken = regexp.MustCompile("<session-token>(.*)</session-token>")
var serverCert = regexp.MustCompile("<server-cert-hash>(.*)</server-cert-hash>")

const postAuthConfirmLoginPayload = `<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="auth-reply" aggregate-auth-version="2">
<version who="vpn">%s</version>
<device-id>linux-64</device-id>
<session-token/>
<session-id/>
<opaque is-for="sg">
<tunnel-group>%s</tunnel-group>
<aggauth-handle>%s</aggauth-handle>
<auth-method>single-sign-on-v2</auth-method>
<config-hash>%s</config-hash>
</opaque>
<auth>
<sso-token>%s</sso-token>
</auth>
</config-auth>`

const postAuthInitRequestPayload = `<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="init" aggregate-auth-version="2">
<version who="vpn">%s</version>
<device-id>linux-64</device-id>
<group-select></group-select>
<group-access>%s</group-access>
<capabilities>
<auth-method>single-sign-on-v2</auth-method>
</capabilities>
</config-auth>`

const VERSION = "4.7.00136"

type AuthInitRequestResponse struct {
// AuthenticationInitExpectedResponse is a struct used to parse the XML payload
// we receive during the AuthenticationInit function. It contains valuable
// information that will be used throughout various parts of the program
type AuthenticationInitExpectedResponse struct {
XMLName xml.Name `xml:"config-auth"`
Opaque struct {
TunnelGroup string `xml:"tunnel-group"`
Expand Down Expand Up @@ -48,23 +81,9 @@ type AuthInitRequestResponse struct {
AggregateAuthVersion string `xml:"aggregate-auth-version,attr"`
}

func PostAuthConfirmLogin(client *http.Client, auth *AuthInitRequestResponse, ssoToken, _url string) (string, string) {
payload := `<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="auth-reply" aggregate-auth-version="2">
<version who="vpn">` + VERSION + `</version>
<device-id>linux-64</device-id>
<session-token/>
<session-id/>
<opaque is-for="sg">
<tunnel-group>` + auth.Opaque.TunnelGroup + `</tunnel-group>
<aggauth-handle>` + auth.Opaque.AggauthHandle + `</aggauth-handle>
<auth-method>single-sign-on-v2</auth-method>
<config-hash>` + auth.Opaque.ConfigHash + `</config-hash>
</opaque>
<auth>
<sso-token>` + ssoToken + `</sso-token>
</auth>
</config-auth>`
// AuthenticationInit sends a http request to _url to get the actual URL and initiate SAML login request
func AuthenticationInit(client *http.Client, _url string) *AuthenticationInitExpectedResponse {
payload := fmt.Sprintf(postAuthInitRequestPayload, VERSION, _url)

post, err := client.Post(_url, `application/x-www-form-urlencoded`, bytes.NewBuffer([]byte(payload)))
if err != nil {
Expand All @@ -77,27 +96,26 @@ func PostAuthConfirmLogin(client *http.Client, auth *AuthInitRequestResponse, ss
log.Fatal(err)
}

token := sessionToken.FindStringSubmatch(string(body))
cert := serverCert.FindStringSubmatch(string(body))

if len(token) != 2 || len(cert) != 2 {
log.Fatal("There was an issue while trying to extract token/cert...")
var response AuthenticationInitExpectedResponse
err = xml.Unmarshal(body, &response)
if err != nil {
log.Fatal(err)
}

return token[1], cert[1]
return &response
}

func PostAuthInitRequest(client *http.Client, _url string) *AuthInitRequestResponse {
payload := `<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="init" aggregate-auth-version="2">
<version who="vpn">` + VERSION + `</version>
<device-id>linux-64</device-id>
<group-select>` + `</group-select>
<group-access>` + _url + `</group-access>
<capabilities>
<auth-method>single-sign-on-v2</auth-method>
</capabilities>
</config-auth>`
// AuthenticationConfirmation sends a http request to _url confirming the authentication was successfull
// (means we got the cookie and we're ready to start the next phase)
func AuthenticationConfirmation(client *http.Client, auth *AuthenticationInitExpectedResponse, ssoToken, _url string) (string, string) {
payload := fmt.Sprintf(
postAuthConfirmLoginPayload,
VERSION,
auth.Opaque.TunnelGroup,
auth.Opaque.AggauthHandle,
auth.Opaque.ConfigHash,
ssoToken,
)

post, err := client.Post(_url, `application/x-www-form-urlencoded`, bytes.NewBuffer([]byte(payload)))
if err != nil {
Expand All @@ -110,11 +128,12 @@ func PostAuthInitRequest(client *http.Client, _url string) *AuthInitRequestRespo
log.Fatal(err)
}

var response AuthInitRequestResponse
err = xml.Unmarshal(body, &response)
if err != nil {
log.Fatal(err)
token := sessionToken.FindStringSubmatch(string(body))
cert := serverCert.FindStringSubmatch(string(body))

if len(token) != 2 || len(cert) != 2 {
log.Fatal("There was an issue while trying to extract token/cert...")
}

return &response
return token[1], cert[1]
}
61 changes: 61 additions & 0 deletions internal/http-client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package internal

import (
"crypto/tls"
"crypto/x509"
"log"
"net/http"
)

// CustomHeaderTransport is used to add our custom http headers to http requests
type CustomHeaderTransport struct {
Transport http.RoundTripper
Headers *map[string]string
}

// RoundTrip adds our custom headers and then uses the default http transport to RoundTrip
func (cht *CustomHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {

if cht.Headers != nil {
for key, value := range *cht.Headers {
req.Header.Add(key, value)
}
}
return cht.Transport.RoundTrip(req)
}

// NewHttpClient creates a new HTTP client and adds serverName to the TLS configuration.
// this part is important to have accurate and valid TLS sessions (solved an old issue with DTLS).
func NewHttpClient(serverName string) *http.Client {
defaultTransport := http.DefaultTransport.(*http.Transport)
systemCerts, err := x509.SystemCertPool()
if err != nil {
log.Fatal("Could not load system cert pool.")
}

defaultTransport.TLSClientConfig = &tls.Config{
ServerName: serverName,
InsecureSkipVerify: false,
RootCAs: systemCerts,
}

return &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
log.Println("Redirected from", via[len(via)-1].URL.String(), "to", req.URL.String())
return nil
},
Transport: &CustomHeaderTransport{
Transport: defaultTransport,
Headers: &map[string]string{
"User-Agent": "AnyConnect Linux_64 4.7.00136",
"Accept": "*/*",
"Accept-Encoding": "identity",
"X-Transcend-Version": "1",
"X-Aggregate-Auth": "1",
"X-Support-HTTP-Auth": "true",
"Content-Type": "application/x-www-form-urlencoded",
"Connection": "keep-alive",
},
},
}
}
Loading

0 comments on commit 3cb755d

Please sign in to comment.