Skip to content

Commit

Permalink
Merge pull request #9 from PhilippePitzClairoux/feature/major-code-re…
Browse files Browse the repository at this point in the history
…factoring

Feature/major code refactoring
  • Loading branch information
PhilippePitzClairoux authored Feb 20, 2024
2 parents 3d11036 + c9c6958 commit 947188b
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 150 deletions.
107 changes: 8 additions & 99 deletions OpenConnectSSO.go
Original file line number Diff line number Diff line change
@@ -1,118 +1,27 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"

"github.com/PhilippePitzClairoux/openconnect-sso/internal"
"github.com/chromedp/chromedp"
"log"
)

// flags
var server = flag.String("server", "", "Server to connect to via openconnect")
var username = flag.String("username", "", "Username to inject in login form")
var password = flag.String("password", "", "Password to inject in login form")
var extraArgs = flag.String("extra-args", "", "Extra args for openconnect (will not override pre-existing ones)")

func main() {
flag.Parse()

// Register kill/interrupt signals
exit := make(chan os.Signal)
signal.Notify(exit, os.Kill, os.Interrupt)

// Initialize http clients and start authentication process
client := internal.NewHttpClient(*server)
cookieFound := make(chan string)
targetUrl := internal.GetActualUrl(client, *server)
samlAuth := internal.AuthenticationInit(client, targetUrl)
ctx, closeBrowser := internal.CreateBrowserContext()

// Here we setup a listener to catch the event of a user closing their browser.
chromedp.ListenTarget(ctx, func(ev interface{}) {
internal.CloseBrowserOnRenderProcessGone(ev, exit)
})

// generate tasks
tasks := generateDefaultBrowserTasks(samlAuth)

// close browser at the end - no matter what happens
defer closeBrowser()

// handle exit signal
go handleExit(exit, closeBrowser)

log.Println("Starting goroutine that searches for authentication cookie ", samlAuth.Auth.SsoV2TokenCookieName)
go internal.BrowserCookieFinder(ctx, cookieFound, samlAuth.Auth.SsoV2TokenCookieName)

log.Println("Open browser and navigate to SSO login page : ", samlAuth.Auth.SsoV2Login)
err := chromedp.Run(ctx, tasks)
if err != nil {
log.Fatal(err)
}

// consume cookie and connect to vpn
startVpnOnLoginCookie(cookieFound, client, samlAuth, targetUrl, closeBrowser)
}

func handleExit(exit chan os.Signal, browser context.CancelFunc) {
sig := <-exit
log.Printf("Got an exit signal (%s)! Cya!", sig.String())
browser()
os.Exit(0)
}

func generateDefaultBrowserTasks(samlAuth *internal.AuthenticationInitExpectedResponse) chromedp.Tasks {
var tasks chromedp.Tasks

// create list of tasks to be executed by browser
tasks = append(tasks, chromedp.Navigate(samlAuth.Auth.SsoV2Login))
addAutofillTaskOnValue(&tasks, *password, "#passwordInput")
addAutofillTaskOnValue(&tasks, *username, "#userNameInput")

return tasks
}

func addAutofillTaskOnValue(actions *chromedp.Tasks, value, selector string) {
if value != "" {
*actions = append(
*actions,
chromedp.WaitVisible(selector, chromedp.ByID),
chromedp.SendKeys(selector, value, chromedp.ByID),
)
if *server == "" {
log.Println("missing mandatory parameter --server")
flag.PrintDefaults()
}
}

// 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",
fmt.Sprintf("--useragent=AnyConnect Linux_64 %s", internal.VERSION),
fmt.Sprintf("--version-string=%s", internal.VERSION),
fmt.Sprintf("--cookie=%s", token),
fmt.Sprintf("--servercert=%s", cert),
targetUrl,
)

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)
}
openconnect := internal.NewOpenconnectCtx(*server, *username, *password)
err := openconnect.Run()
if err != nil {
log.Fatal("Could not run openconnect-sso : ", err)
}
}
74 changes: 74 additions & 0 deletions internal/browser-utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package internal

import (
"context"
"github.com/chromedp/cdproto/inspector"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
"os"
"strings"
)

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

// createBrowserContext creates new chromedp context and exec allocator
func createBrowserContext() (context.Context, context.CancelFunc) {
ctx, _ := chromedp.NewContext(context.Background())
allocCtx, _ := chromedp.NewExecAllocator(ctx, opts...)

return chromedp.NewContext(allocCtx)
}

// closeBrowserOnRenderProcessGone sends an exit signal when the
// "Render process gone" is found (user manually closes the browser).
func closeBrowserOnRenderProcessGone(ev interface{}, exit chan os.Signal) {
ins, ok := ev.(*inspector.EventDetached)
if ok {
if strings.Contains(ins.Reason.String(), "Render process gone.") {
exit <- os.Kill
}
}
}

// browserCookieFinder setup's a chromedp listener in order to look
// through the cookies channel for a cookie that matches name
func (oc *OpenconnectCtx) browserCookieFinder(name string) {
chromedp.ListenTarget(oc.browserCtx, func(ev interface{}) {
switch ev := ev.(type) {
case *network.EventRequestWillBeSentExtraInfo:
for _, cookie := range ev.AssociatedCookies {
if cookie.Cookie.Name == name {
oc.cookieFoundChan <- cookie.Cookie.Value
}
}
}
})
}

// generateDefaultBrowserTasks adds a task to inject username and/or password if the argument is present.
// also adds the initial Navigate command to open the browser on the right window
func (oc *OpenconnectCtx) generateDefaultBrowserTasks(samlAuth *AuthenticationInitExpectedResponse) chromedp.Tasks {
var tasks chromedp.Tasks

// create list of tasks to be executed by browser
tasks = append(tasks, chromedp.Navigate(samlAuth.Auth.SsoV2Login))
addAutofillTaskOnValue(&tasks, oc.password, "#passwordInput")
addAutofillTaskOnValue(&tasks, oc.username, "#userNameInput")

return tasks
}

// addAutofillTaskOnValue adds a task if value is not empty
func addAutofillTaskOnValue(actions *chromedp.Tasks, value, selector string) {
if value != "" {
*actions = append(
*actions,
chromedp.WaitVisible(selector, chromedp.ByID),
chromedp.SendKeys(selector, value, chromedp.ByID),
)
}
}
24 changes: 12 additions & 12 deletions internal/http-calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package internal
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
"net/http"
"regexp"
)

Expand Down Expand Up @@ -82,32 +82,32 @@ type AuthenticationInitExpectedResponse struct {
}

// 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)
func (oc *OpenconnectCtx) AuthenticationInit() (*AuthenticationInitExpectedResponse, error) {
payload := fmt.Sprintf(postAuthInitRequestPayload, VERSION, oc.targetUrl)

post, err := client.Post(_url, `application/x-www-form-urlencoded`, bytes.NewBuffer([]byte(payload)))
post, err := oc.client.Post(oc.targetUrl, `application/x-www-form-urlencoded`, bytes.NewBuffer([]byte(payload)))
if err != nil {
log.Fatal(err)
return nil, err
}

body, err := io.ReadAll(post.Body)
defer post.Body.Close()
if err != nil {
log.Fatal(err)
return nil, err
}

var response AuthenticationInitExpectedResponse
err = xml.Unmarshal(body, &response)
if err != nil {
log.Fatal(err)
return nil, err
}

return &response
return &response, nil
}

// 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) {
func (oc *OpenconnectCtx) AuthenticationConfirmation(auth *AuthenticationInitExpectedResponse, ssoToken string) (string, string, error) {
payload := fmt.Sprintf(
postAuthConfirmLoginPayload,
VERSION,
Expand All @@ -117,7 +117,7 @@ func AuthenticationConfirmation(client *http.Client, auth *AuthenticationInitExp
ssoToken,
)

post, err := client.Post(_url, `application/x-www-form-urlencoded`, bytes.NewBuffer([]byte(payload)))
post, err := oc.client.Post(oc.targetUrl, `application/x-www-form-urlencoded`, bytes.NewBuffer([]byte(payload)))
if err != nil {
log.Fatal(err)
}
Expand All @@ -132,8 +132,8 @@ func AuthenticationConfirmation(client *http.Client, auth *AuthenticationInitExp
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 "", "", errors.New("Could not extract cert and/or token")
}

return token[1], cert[1]
return token[1], cert[1], nil
}
119 changes: 119 additions & 0 deletions internal/openconnect-context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package internal

import (
"context"
"fmt"
"github.com/chromedp/chromedp"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
)

type OpenconnectCtx struct {
process *exec.Cmd
client *http.Client
exit chan os.Signal
cookieFoundChan chan string
exitChan chan os.Signal
targetUrl string
server string
username string
password string
browserCtx context.Context
closeBrowser context.CancelFunc
}

func NewOpenconnectCtx(server, username, password string) *OpenconnectCtx {
client := NewHttpClient(server)
exit := make(chan os.Signal)

// register exit signals
signal.Notify(exit, os.Kill, os.Interrupt)

return &OpenconnectCtx{
client: client,
cookieFoundChan: make(chan string),
exitChan: exit,
targetUrl: getActualUrl(client, server),
username: username,
password: password,
}
}

func (oc *OpenconnectCtx) Run() error {
samlAuth, err := oc.AuthenticationInit()
if err != nil {
log.Println("Could not start authentication process...")
return err
}

tasks, err := oc.startBrowser(samlAuth)
if err != nil {
log.Println("Could not start browser properly...")
return err
}

// close browser at the end - no matter what happens
defer oc.closeBrowser()

// handle exit signal
log.Println("Starting goroutine to handle exit signals")
go oc.handleExit()

log.Println("Starting goroutine to search for cookie", samlAuth.Auth.SsoV2TokenCookieName)
go oc.browserCookieFinder(samlAuth.Auth.SsoV2TokenCookieName)

log.Println("Open browser and navigate to SSO login page : ", samlAuth.Auth.SsoV2Login)
err = chromedp.Run(oc.browserCtx, tasks)
if err != nil {
return err
}

// consume cookie and connect to vpn
return oc.startVpnOnLoginCookie(samlAuth)
}

func (oc *OpenconnectCtx) startBrowser(samlAuth *AuthenticationInitExpectedResponse) (chromedp.Tasks, error) {
oc.browserCtx, oc.closeBrowser = createBrowserContext()
tasks := oc.generateDefaultBrowserTasks(samlAuth)

// setup listener to exit program when browser is closed
chromedp.ListenTarget(oc.browserCtx, func(ev interface{}) {
closeBrowserOnRenderProcessGone(ev, oc.exitChan)
})

return tasks, nil
}

// startVpnOnLoginCookie waits to get a cookie from the authenticationCookies channel before confirming
// the authentication process (to get token/cert) and then starting openconnect
func (oc *OpenconnectCtx) startVpnOnLoginCookie(auth *AuthenticationInitExpectedResponse) error {
for cookie := range oc.cookieFoundChan {
token, cert, err := oc.AuthenticationConfirmation(auth, cookie)
oc.closeBrowser() // close browser

if err != nil {
return err
}

oc.process = exec.Command("sudo",
"openconnect",
fmt.Sprintf("--useragent=AnyConnect Linux_64 %s", VERSION),
fmt.Sprintf("--version-string=%s", VERSION),
fmt.Sprintf("--cookie=%s", token),
fmt.Sprintf("--servercert=%s", cert),
oc.targetUrl,
)

oc.process.Stdout = os.Stdout
oc.process.Stderr = os.Stdout
oc.process.Stdin = os.Stdin

log.Println("Starting openconnect: ", oc.process.String())
return oc.process.Run()
}

return nil
}
Loading

0 comments on commit 947188b

Please sign in to comment.