// Forwards the local server listener to the specified target address (format host:port) using the SSH connection as tunnel.
// What this method does is the same as "ssh -L $ANY-PORT:jenkins-host:$TARGET-PORT" jenkins-host.
func (self *SSHTunnelEstablisher) forwardLocalConnectionsTo(config *util.Config, ssh *ssh.Client, listener net.Listener, targetAddress string) {
	transfer := func(source io.ReadCloser, target io.Writer) {
		defer source.Close()
		_, _ = io.Copy(target, source)
	}

	establishBIDITransport := func(source net.Conn, target net.Conn) {
		go transfer(source, target)
		go transfer(target, source)
	}

	sshAddress := ssh.Conn.RemoteAddr().String()
	localAddress := listener.Addr().String()

	util.GOut("ssh-tunnel", "Forwarding local connections on '%v' to '%v' via '%v'.", localAddress, targetAddress, sshAddress)

	for {
		if sourceConnection, err := listener.Accept(); err == nil {
			if targetConnection, err := ssh.Dial("tcp", targetAddress); err == nil {
				establishBIDITransport(sourceConnection, targetConnection)
			} else {
				util.GOut("ssh-tunnel", "ERROR: Failed forwarding incoming local connection on '%v' to '%v' via '%v'.", localAddress, targetAddress, sshAddress)
			}
		} else {
			util.GOut("ssh-tunnel", "Stop forwarding local connections on '%v' to '%v'.", localAddress, targetAddress)
			return
		}
	}
}
func (self *LocationCleaner) cleanupLocations(dirsToKeepClean, exclusions []string, mode string, maxTTL time.Duration) {
	for _, rootDir := range dirsToKeepClean {
		rootDir = filepath.Clean(rootDir)
		dirToEmptyMap := map[string]bool{}
		expiredTimeOffset := time.Now().Add(-maxTTL)

		if mode == ModeTTLPerLocation {
			util.GOut("cleanup", "Checking %v for expiration.", rootDir)
			exclusionCount := self.cleanupFiles(rootDir, expiredTimeOffset, true, exclusions, dirToEmptyMap)
			if exclusionCount > 0 {
				return
			}
			util.GOut("cleanup", "Cleaning %v", rootDir)
		} else {
			util.GOut("cleanup", "Cleaning expired files in %v", rootDir)
		}

		// Handling outdated temporary files
		_ = self.cleanupFiles(rootDir, expiredTimeOffset, false, exclusions, dirToEmptyMap)

		// Handling all directories that are known to be empty
		for dirPath, emptyDir := range dirToEmptyMap {
			// Root-Dir is only cleaned for "TTLPerLocation".
			if mode != ModeTTLPerLocation && rootDir == dirPath {
				continue
			}

			if emptyDir {
				if err := os.Remove(dirPath); err == nil {
					util.GOut("cleanup", "\x1b[39mRemoved empty directory: %v", dirPath)
				}
			}
		}
	}
}
func (self *ServerMode) execute(privateKey ssh.Signer) {
	// An SSH server is represented by a ServerConfig, which holds
	// certificate details and handles authentication of ServerConns.
	config := &ssh.ServerConfig{
		PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
			// Slowing down password check to make BF attacks more difficult.
			time.Sleep(time.Second * 1)

			if c.User() == self.config.SSHUsername && string(pass) == self.config.SSHPassword {
				return nil, nil
			} else {
				return nil, fmt.Errorf("SSH: Password rejected for %q", c.User())
			}
		},
		AuthLogCallback: func(c ssh.ConnMetadata, method string, err error) {
			if err == nil {
				util.GOut("SSH", "Authentication succeeded '%v' using '%v'", c.User(), method)
			} else {
				util.GOut("SSH", "Failed attempt to authenticate '%v' using '%v' ; Caused by: %v", c.User(), method, err)
			}
		},
	}

	config.AddHostKey(privateKey)

	// Once a ServerConfig has been configured, connections can be
	// accepted.
	address := fmt.Sprintf("%v:%v", self.config.SSHListenAddress, self.config.SSHListenPort)

	util.GOut("SSH", "Starting to listen @ %v", address)

	listener, err := net.Listen("tcp", address)
	if err != nil {
		panic("SSH: Failed to listen @ " + address)
	} else {
		defer func() {
			listener.Close()
			self.status.Set(ModeStopped)
		}()
	}

	go func() {
		for {
			connection, err := listener.Accept()
			if err != nil {
				util.GOut("SSH", "Failed to accept next incoming SSH connection, assuming connection was closed.")
				return
			}

			// Handling only one connection at a time should be enough.
			self.handleSSHRequest(&connection, config)
		}
	}()

	// Entering main loop and remain there until the terminal is stopped and the deferred channel close is triggered.
	self.status.Set(ModeStarted)
	for self.status.Get() == ModeStarted {
		time.Sleep(time.Millisecond * 100)
	}
}
// Implements Java installation for Windows
func (self *JavaDownloader) InstallJava(config *util.Config) error {
	util.GOut("DOWNLOAD", "Getting %v", config.CIHostURI)
	util.GOut("INSTALL", "Installing %v", config.CIHostURI)

	// TODO: Implement like done here:
	// TODO: https://github.com/jenkinsci/jenkins/blob/main/core/src/main/java/hudson/tools/JDKInstaller.java

	return fmt.Errorf("Installing Java is not implemented yet. Install it manually.")
}
func (self *JenkinsClientDownloader) downloadJar(config *util.Config) error {
	util.GOut("DOWNLOAD", "Getting latest Jenkins client %v", (config.CIHostURI + "/" + ClientJarURL))

	// Create the HTTP request.
	request, err := config.CIRequest("GET", ClientJarURL, nil)
	if err != nil {
		return err
	}

	if fi, err := os.Stat(ClientJarName); err == nil {
		request.Header.Add("If-Modified-Since", fi.ModTime().Format(http.TimeFormat))
	}

	// Perform the HTTP request.
	var source io.ReadCloser
	sourceTime := time.Now()
	if response, err := config.CIClient().Do(request); err == nil {
		defer response.Body.Close()

		source = response.Body

		if response.StatusCode == 304 {
			util.GOut("DOWNLOAD", "Jenkins client is up-to-date, no need to download.")
			return nil
		} else if response.StatusCode != 200 {
			return fmt.Errorf("Failed downloading jenkins client. Cause: HTTP-%v %v", response.StatusCode, response.Status)
		}

		if value := response.Header.Get("Last-Modified"); value != "" {
			if time, err := http.ParseTime(value); err == nil {
				sourceTime = time
			}
		}
	} else {
		return fmt.Errorf("Failed downloading jenkins client. Connect failed. Cause: %v", err)
	}

	target, err := os.Create(ClientJarDownloadName)
	defer target.Close()

	if err != nil {
		return fmt.Errorf("Failed downloading jenkins client. Cannot create local file. Cause: %v", err)
	}

	if _, err = io.Copy(target, source); err == nil {
		target.Close()
		if err = os.Remove(ClientJarName); err == nil || os.IsNotExist(err) {
			if err = os.Rename(ClientJarDownloadName, ClientJarName); err == nil {
				os.Chtimes(ClientJarName, sourceTime, sourceTime)
			}
		}
		return err
	} else {
		return fmt.Errorf("Failed downloading jenkins client. Transfer failed. Cause: %v", err)
	}
}
// Opens a new local server socket.
func (self *SSHTunnelEstablisher) newLocalServerListener() (serverListener net.Listener, err error) {
	serverListener, err = net.Listen("tcp", "localhost:0")
	if err == nil {
		self.closables = append(self.closables, serverListener)
		util.GOut("ssh-tunnel", "Opened local listener on '%v'.", serverListener.Addr())
	} else {
		util.GOut("ssh-tunnel", "ERROR: Failed opening local listener. Cause: %v", err)
	}
	return
}
func (self *SSHTunnelEstablisher) IsConfigAcceptable(config *util.Config) bool {
	if config.CITunnelSSHEnabled && config.CITunnelSSHAddress == "" {
		util.GOut("ssh-tunnel", "WARN: SSH tunnel is enabled but SSH server address is empty.")
		return false
	}
	if config.CITunnelSSHAddress != "" && !config.HasCIConnection() {
		util.GOut("ssh-tunnel", "WARN: No Jenkins URI defined. SSH tunnel settings are not enough to connect to Jenkins.")
		return false
	}
	return true
}
Example #8
0
func (self *FullGCInvoker) invokeSystemGC(config *util.Config) {
	// curl -d "script=System.gc()" -X POST http://user:password@jenkins-host/ci/computer/%s/scriptText
	postBody := strings.NewReader(fmt.Sprintf(FullGCPostBody, url.QueryEscape(FullGCScript)))
	request, err := config.CIRequest("POST", fmt.Sprintf(FullGCURL, config.ClientName), postBody)
	if err == nil {
		if response, err := config.CIClient().Do(request); err == nil {
			response.Body.Close()
			if response.StatusCode != 200 {
				util.GOut("gc", "ERROR: Failed invoking full GC as node request in Jenkins failed with %s", response.Status)
			}
		} else {
			util.GOut("gc", "ERROR: Failed invoking full GC as Jenkins cannot be contacted. Cause: %v", err)
		}
	}
}
func (self *ClientMode) IsConfigAcceptable(config *util.Config) bool {
	if !config.HasCIConnection() {
		util.GOut(self.Name(), "ERROR: No Jenkins URI defined. Cannot connect to the CI server.")
		return false
	}

	if config.SecretKey == "" && !self.isAuthCredentialsPassedViaCommandline(config) {
		if config.SecretKey = self.getSecretFromJenkins(config); config.SecretKey == "" {
			util.GOut(self.Name(), "ERROR: No secret key set for node %v and the attempt to fetch it from Jenkins failed.", config.ClientName)
			return false
		}
	}

	return true
}
// Monitors that the tunnel is alive by periodically querying the node status off Jenkins.
// Timeout, hanging connections or connection errors lead to a restart of the current execution mode (which implicitly closes SSH tunnel as well).
func (self *SSHTunnelEstablisher) startAliveStateMonitoring(config *util.Config) {
	// Periodically check the node status and increment lastAliveTick on success
	go func() {
		for _ = range self.aliveTicker.C {
			if !self.tunnelConnected.Get() {
				continue
			}

			if _, err := GetJenkinsNodeStatus(config); err == nil {
				self.lastAliveTick.Set(self.expectedAliveTick.Get())
			}
		}
	}()

	// Periodically check that lastAliveTick was incremented.
	go func() {
		for _ = range self.aliveTickEvaluator.C {
			if !self.tunnelConnected.Get() {
				continue
			}

			if math.Abs(float64(self.expectedAliveTick.Get()-self.lastAliveTick.Get())) > 1 {
				util.GOut("ssh-tunnel", "WARN: The SSH tunnel appears to be dead or Jenkins is gone. Forcing restart of client and SSH tunnel.")
				modes.GetConfiguredMode(config).Stop()
			} else {
				self.expectedAliveTick.AddAndGet(1)
			}
		}
	}()
}
// Creates a new tunnel establisher.
func NewSSHTunnelEstablisher(registerInMode bool) *SSHTunnelEstablisher {
	self := new(SSHTunnelEstablisher)
	self.closables = []io.Closer{}
	self.ciHostURL = nil

	self.aliveTicker, self.aliveTickEvaluator = time.NewTicker(nodeSshTunnelAliveMonitoringInterval), time.NewTicker(nodeSshTunnelAliveMonitoringInterval)
	self.expectedAliveTick, self.lastAliveTick = util.NewAtomicInt32(), util.NewAtomicInt32()
	self.tunnelConnected = util.NewAtomicBoolean()

	if registerInMode {
		modes.RegisterModeListener(func(mode modes.ExecutableMode, nextStatus int32, config *util.Config) {
			if !config.CITunnelSSHEnabled || config.CITunnelSSHAddress == "" || mode.Name() != "client" || !config.HasCIConnection() {
				return
			}

			if nextStatus == modes.ModeStarting {
				var err error
				if self.ciHostURL, err = url.Parse(config.CIHostURI); err != nil {
					util.GOut("ssh-tunnel", "ERROR: Failed parsing Jenkins URI. Cannot tunnel connections to Jenkins. Cause: %v", err)
					return
				}

				self.setupSSHTunnel(config)

			} else if nextStatus == modes.ModeStopped {
				self.tearDownSSHTunnel(config)
			}
		})
	}

	return self
}
func (self *JenkinsNodeMonitor) IsConfigAcceptable(config *util.Config) bool {
	if config.ClientMonitorStateOnServer && !config.HasCIConnection() {
		util.GOut("monitor", "No Jenkins URI defined. Cannot monitor this node within Jenkins.")
		return false
	}
	return true
}
Example #13
0
func (self *FullGCInvoker) IsConfigAcceptable(config *util.Config) bool {
	if config.ForceFullGC && !config.HasCIConnection() {
		util.GOut("gc", "WARN: No Jenkins URI defined. System.GC() cannot be called inside the Jenkins client.")
		return false
	}
	return true
}
func (self *PeriodicRestarter) waitForIdleIfRequired(config *util.Config) {
	if config.PeriodicClientRestartOnlyWhenIDLE {
		for !util.NodeIsIdle.Get() {
			util.GOut("periodic", "Waiting for node to become IDLE before triggering a restart.")
			time.Sleep(time.Minute * 5)
		}
	}
}
// Checks if Jenkins shows this node as connected and returns the node's IDLE state as second return value.
func (self *JenkinsNodeMonitor) isServerSideConnected(config *util.Config) (connected bool, idle bool, serverReachable bool) {
	if status, err := GetJenkinsNodeStatus(config); err == nil {
		return !status.Offline, status.Idle, true
	} else {
		util.GOut("monitor", "ERROR: Failed to monitor node %v using %v. Cause: %v", config.ClientName, config.CIHostURI, err)
		return false, true, false
	}
}
// Performs registration & deregistration for autostart on windows.
func (self *AutostartHandler) Prepare(config *util.Config) {
	cwd, _ := os.Getwd()
	self.commandline = fmt.Sprintf("\"%s\" \"-directory=%s\"", os.Args[0], cwd)

	if config.Autostart {
		util.GOut("autostart", "Registering %v for autostart.", self.commandline)
		err := self.register()
		if err != nil {
			util.GOut("autostart", "ERROR: FAILED to register for autostart. Cause: %v", err)
		}
	} else {
		if self.isRegistered() {
			util.GOut("autostart", "Unregistering %v from autostart.", self.commandline)
			err := self.unregister()
			if err != nil {
				util.GOut("autostart", "ERROR: FAILED to unregister from autostart. Cause: %v", err)
			}
		}
	}
}
func (self *ClientMode) getSecretFromJenkins(config *util.Config) string {
	response, err := config.CIGet(fmt.Sprintf("computer/%s/", config.ClientName))
	if err == nil {
		defer response.Body.Close()

		if response.StatusCode == 200 {
			var content []byte
			if content, err = ioutil.ReadAll(response.Body); err == nil {
				return self.extractSecret(content)
			}
		} else {
			util.GOut("client", "ERROR: Failed fetching secret key from Jenkins. Cause: %v", response.Status)
		}
	}

	if err != nil {
		util.GOut("client", "ERROR: Failed fetching secret key from Jenkins. Cause: %v", err)
	}

	return ""
}
func (self *ClientMode) getCustomizedAgentJnlp(config *util.Config) []byte {
	response, err := config.CIGet(fmt.Sprintf("computer/%s/slave-agent.jnlp", config.ClientName))
	if err == nil {
		defer response.Body.Close()

		if response.StatusCode == 200 {
			var content []byte
			if content, err = ioutil.ReadAll(response.Body); err == nil {
				return self.applyCustomJnlpArgs(config, content)
			}
		} else {
			util.GOut("client", "ERROR: Failed JNLP config from Jenkins. Cause: %v", response.Status)
		}
	}

	if err != nil {
		util.GOut("client", "ERROR: Failed JNLP config from Jenkins. Cause: %v", err)
	}

	return nil
}
Example #19
0
// Checks if java is installed and if the java version is greater or equals the required version.
func (self *JavaDownloader) javaIsInstalled() bool {
	if java, err := exec.LookPath("java"); err == nil {
		// Set absolute path to java
		util.Java = java
		// Check the version
		if output, err := exec.Command(java, "-version").CombinedOutput(); err == nil {
			if pattern, err := regexp.Compile(`(?i)java version "([^"]+)"`); err == nil {
				if matches := pattern.FindSubmatch(output); matches != nil && len(matches) == 2 {
					javaVersion := string(matches[1])
					if version.Compare(javaVersion, MinJavaVersion, ">=") {
						util.GOut("java", "Found java version %v, no need to install a newer version.", javaVersion)
						return true
					}
					util.GOut("java", "Found java version %v. A newer version is required to run the Jenkins client.", javaVersion)
				}
			}
		}
	}

	return false
}
func (self *NodeNameHandler) Prepare(config *util.Config) {
	if !config.HasCIConnection() {
		return
	}

	if foundNode, err := self.verifyNodeName(config); err == nil {

		if !foundNode {
			if config.CreateClientIfMissing {
				if err := self.createNode(config); err == nil {
					util.GOut("naming", "Created node '%s' in Jenkins.", config.ClientName)
				} else {
					util.GOut("naming", "ERROR: Failed to create node '%s' in Jenkins. Cause: %v", config.ClientName, err)
				}
				foundNode, _ = self.verifyNodeName(config)
			} else {
				util.GOut("naming", "Will not attempt to auto generate node '%s' in Jenkins. Enable this with '-create' or within the configuration.", config.ClientName)
			}
		}

		if foundNode {
			util.GOut("naming", "Found client node name in Jenkins, using '%v'.", config.ClientName)
		} else {
			util.GOut("naming", "WARN: Client node name '%v' was %s in Jenkins. Likely the next operations will fail.", config.ClientName, "NOT FOUND")
		}
	} else {
		util.GOut("nameing", "ERROR: Failed to verify the client node name in Jenkins. Cause: %v", err)
	}
}
func (self *ClientMode) redirectConsoleOutput(config *util.Config, input io.ReadCloser, output io.Writer, outputMutex *sync.Mutex) {
	defer input.Close()
	reader := bufio.NewReader(input)

	holdsLock := false
	lock, unlock := func() {
		if !holdsLock {
			outputMutex.Lock()
			holdsLock = true
		}
	}, func() {
		if holdsLock {
			outputMutex.Unlock()
			holdsLock = false
		}
	}
	defer unlock()

	restartTriggered := false

	for {
		line, isPrefix, err := reader.ReadLine()

		if len(line) > 0 || holdsLock {
			lock()

			// Send to output
			if len(line) > 0 {
				output.Write(line)
			}

			if !isPrefix {
				output.Write([]byte("\n"))
				unlock()
			}

			if config.ClientMonitorConsole && !restartTriggered && config.ConsoleMonitor.IsRestartTriggered(string(line)) {
				go func() {
					time.Sleep(time.Second * 1)
					go self.Stop()
					util.GOut("client", "WARN: %s found in console output. Client state may be invalid, forced a restart.", "RESTART TOKEN")
				}()
				restartTriggered = true
			}
		}

		if err != nil {
			break
		}
	}
}
// Checks if both, this side and the remote side show the node as connected and increments a offline count if not.
// Forces a restart of the connector when offline count reaches the threshold.
func (self *JenkinsNodeMonitor) monitor(config *util.Config) {
	if self.isThisSideConnected(config) {
		if connected, idle, serverReachable := self.isServerSideConnected(config); connected {
			util.NodeIsIdle.Set(idle)
			self.offlineCount = 0

			if !self.onlineShown {
				util.GOut("monitor", "Node is online in Jenkins.")
				self.onlineShown = true
			}
		} else {
			util.NodeIsIdle.Set(true)

			if serverReachable {
				self.offlineCount++
			}

			if self.offlineCount > 3*maxOfflineCountBeforeRestart {
				self.offlineCount = maxOfflineCountBeforeRestart
			}

			if self.offlineCount == maxOfflineCountBeforeRestart {
				self.forceReconnect(config)
			}

			util.GOut("monitor", "WARN: Node is OFFLINE in Jenkins.")
			self.onlineShown = false
		}
	} else {
		util.NodeIsIdle.Set(true)
		self.offlineCount = 0

		if self.onlineShown {
			util.GOut("monitor", "WARN: Node went OFFLINE locally.")
			self.onlineShown = false
		}
	}
}
func (self *JenkinsClientDownloader) Prepare(config *util.Config) {
	util.ClientJar, _ = filepath.Abs(ClientJarName)

	modes.RegisterModeListener(func(mode modes.ExecutableMode, nextStatus int32, config *util.Config) {
		if mode.Name() == "client" && nextStatus == modes.ModeStarting && config.HasCIConnection() {
			if err := self.downloadJar(config); err != nil {
				jar, e := os.Open(ClientJarName)
				defer jar.Close()
				if os.IsNotExist(e) {
					panic(fmt.Sprintf("No jenkins client: %s", err))
				} else {
					util.GOut("DOWNLOAD", "%s", err)
				}
			}
		}
	})
}
func (self *PeriodicRestarter) Prepare(config *util.Config) {
	if self.ticker != nil {
		self.ticker.Stop()
	}

	if !config.PeriodicClientRestartEnabled || config.PeriodicClientRestartIntervalHours <= 0 {
		return
	}

	self.ticker = time.NewTicker(time.Hour * time.Duration(config.PeriodicClientRestartIntervalHours))

	go func() {
		// Run in schedule
		for time := range self.ticker.C {
			util.GOut("periodic", "Triggering periodic restart.", time)
			self.waitForIdleIfRequired(config)
			// Stopping the mode as this will automatically do a restart.
			modes.GetConfiguredMode(config).Stop()
		}
	}()
}
func (self *LocationCleaner) cleanupFiles(rootDir string, expiredTimeOffset time.Time, dryRun bool,
	exclusions []string, dirToEmptyMap map[string]bool) (exclusionCount int64) {

	filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
		if err == nil {
			dirToEmptyMap[filepath.Dir(path)] = false

			if info.IsDir() {
				dirToEmptyMap[filepath.Clean(path)] = true
			} else {
				fileIsToRemove := true

				if fileIsToRemove && util.GetFileLastTouched(info).After(expiredTimeOffset) {
					fileIsToRemove = false
				}

				if fileIsToRemove && len(exclusions) > 0 {
					for _, pattern := range exclusions {
						if matchesExclusionPattern, _ := filepath.Match(pattern, path); matchesExclusionPattern {
							fileIsToRemove = false
							break
						}
					}
				}

				if fileIsToRemove {
					if !dryRun {
						if err := os.Remove(path); err == nil {
							util.GOut("cleanup", "\x1b[39mRemoved expired: %v", path)
						}
					}
				} else {
					exclusionCount++
				}
			}
		}
		return err
	})
	return
}
Example #26
0
func (self *FullGCInvoker) Prepare(config *util.Config) {
	if self.tickers != nil {
		for _, ticker := range self.tickers {
			ticker.Stop()
		}
	} else {
		self.tickers = []*time.Ticker{}
	}

	if !config.ForceFullGC {
		return
	}

	util.GOut("gc", "Periodic forced full GC is enabled.")

	if config.ForceFullGCIntervalMinutes > 0 {
		self.tickers = append(self.tickers, self.scheduleGCInvoker(config, config.ForceFullGCIntervalMinutes, false))
	}

	if config.ForceFullGCIDLEIntervalMinutes > 0 {
		self.tickers = append(self.tickers, self.scheduleGCInvoker(config, config.ForceFullGCIDLEIntervalMinutes, true))
	}
}
func (self *LocationCleaner) waitForIdle() {
	for !util.NodeIsIdle.Get() {
		util.GOut("cleanup", "Waiting for node to become IDLE before cleaning configured locations.")
		time.Sleep(time.Minute * 5)
	}
}
// Forces a reconnect with Jenkins by stopping the current mode.
func (self *JenkinsNodeMonitor) forceReconnect(config *util.Config) {
	if self.isThisSideConnected(config) {
		util.GOut("monitor", "WARN: This node appears dead in Jenkins, forcing a reconnect.")
		modes.GetConfiguredMode(config).Stop()
	}
}
// Opens a new SSH connection, local server ports (JNLP, HTTP) and forwards it to the corresponding ports on Jenkins.
func (self *SSHTunnelEstablisher) setupSSHTunnel(config *util.Config) {
	if self.ciHostURL == nil {
		return
	}

	if !config.PassCIAuth && config.SecretKey != "" {
		util.GOut("ssh-tunnel", "WARN: Secret key is not supported in combination with SSH tunnel. Implicitly setting %v to %v", "client>passAuth", "true")
		config.PassCIAuth = true
	}

	// Ensure no other SSL connections are still open.
	self.tearDownSSHTunnel(config)

	// Configuring the SSH client
	clientConfig := &ssh.ClientConfig{
		User: config.CITunnelSSHUsername,
		Auth: []ssh.AuthMethod{
			ssh.Password(config.CITunnelSSHPassword),
		},
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			expected, actual := config.CITunnelSSHFingerprint, self.formatHostFingerprint(key)
			if actual != expected && expected != "-" {
				if expected == "" {
					return fmt.Errorf("The host fingerprint of '%v' is '%v'. Please add this to the configuration in order to connect.", hostname, actual)
				} else {
					return fmt.Errorf("The host fingerprint of '%v' is '%v' while '%v' was expected. Connection aborted.", hostname, actual, expected)
				}
			}
			return nil
		},
	}

	// Connecting to the SSH host
	if config.CITunnelSSHPort == 0 {
		config.CITunnelSSHPort = 22
	}
	sshAddress := fmt.Sprintf("%v:%v", config.CITunnelSSHAddress, config.CITunnelSSHPort)

	sshClient, err := ssh.Dial("tcp", sshAddress, clientConfig)
	if err == nil {
		self.closables = append(self.closables, sshClient)
		util.GOut("ssh-tunnel", "Successfully connected with '%v'.", sshAddress)
	} else {
		util.GOut("ssh-tunnel", "ERROR: Failed connecting with %v. Cause: %v", sshAddress, err)
		return
	}

	// Fetching target ports
	jnlpTargetAddress, err := self.formatJNLPHostAndPort(config)
	if err != nil {
		util.GOut("ssh-tunnel", "ERROR: Failed fetching JNLP port from '%v'. Cause: %v.", config.CIHostURI, err)
		return
	}

	httpTargetAddress := self.formatHttpHostAndPort()

	// Creating a local server listeners to use for port forwarding.
	httpListener, err1 := self.newLocalServerListener()
	jnlpListener, err2 := self.newLocalServerListener()

	if err1 != nil || err2 != nil {
		self.tearDownSSHTunnel(config)
		return
	}

	// Forward local connections to the HTTP(S)/JNLP ports.
	go self.forwardLocalConnectionsTo(config, sshClient, httpListener, httpTargetAddress)
	go self.forwardLocalConnectionsTo(config, sshClient, jnlpListener, jnlpTargetAddress)

	// Apply the tunnel configuration
	localCiURL, _ := url.Parse(self.ciHostURL.String())
	localCiURL.Host = httpListener.Addr().String()
	config.CIHostURI = localCiURL.String()
	util.JnlpArgs["-url"] = localCiURL.String()
	util.JnlpArgs["-tunnel"] = jnlpListener.Addr().String()

	// Mark tunnel as connected when we passed this line.
	self.tunnelConnected.Set(true)
}
func (self *ClientMode) execute(config *util.Config) {
	commandline := []string{}
	commandline = append(commandline, util.JavaArgs...)
	commandline = append(commandline, config.JavaArgs...)

	if config.JavaMaxMemory != "" {
		commandline = append(commandline, "-Xmx"+config.JavaMaxMemory)
	}

	commandline = append(commandline, "-jar", util.ClientJar)

	if len(util.JnlpArgs) > 0 {
		if err := ioutil.WriteFile("~slave-agent.jnlp", self.getCustomizedAgentJnlp(config), os.ModeTemporary); err == nil {
			defer os.Remove("~slave-agent.jnlp")
			commandline = append(commandline, "-jnlpUrl", "file:./~slave-agent.jnlp")
		} else {
			util.GOut("client", "ERROR: Failed creating customized JNLP config. Cause: %v", err)
		}
	} else {
		commandline = append(commandline, "-jnlpUrl", fmt.Sprintf("%v/computer/%v/slave-agent.jnlp", config.CIHostURI, config.ClientName))

		if config.SecretKey != "" && !self.isAuthCredentialsPassedViaCommandline(config) {
			commandline = append(commandline, "-secret", config.SecretKey)
		}
	}

	if config.CIAcceptAnyCert {
		commandline = append(commandline, "-noCertificateCheck")
	}

	if config.HandleReconnectsInLauncher {
		commandline = append(commandline, "-noReconnect")
	}

	if self.isAuthCredentialsPassedViaCommandline(config) {
		commandline = append(commandline, "-auth", fmt.Sprintf("%s:%s", config.CIUsername, config.CIPassword))
		commandline = append(commandline, "-jnlpCredentials", fmt.Sprintf("%s:%s", config.CIUsername, config.CIPassword))
	}

	stoppingClient, clientStopped := make(chan bool), make(chan bool)

	go func() {
		command := exec.Command(util.Java, commandline...)
		if pOut, err := command.StdoutPipe(); err == nil {
			go self.redirectConsoleOutput(config, pOut, os.Stdout, util.OutputMutex)
		} else {
			panic("Failed connecting stdout with console")
		}

		if pErr, err := command.StderrPipe(); err == nil {
			go self.redirectConsoleOutput(config, pErr, os.Stderr, util.OutputMutex)
		} else {
			panic("Failed connecting stderr with console")
		}

		util.GOut("client", "Starting: %s", self.createFilteredCommands(commandline))

		if err := command.Start(); err != nil {
			util.GOut("client", "ERROR: Jenkins client failed to start with %v", err)
		} else {
			util.GOut("client", "Jenkins client was started.")

			go func() {
				<-stoppingClient
				command.Process.Kill()
				time.Sleep(time.Second * 1)
			}()

			if err := command.Wait(); err != nil {
				util.GOut("client", "WARN: Jenkins client quit with %v", err)
			} else {
				util.GOut("client", "Jenkins client was stopped.")
			}

			self.status.Set(ModeStopped)
			clientStopped <- true
		}
	}()

	// Entering main loop
	self.status.Set(ModeStarted)

	for self.status.Get() == ModeStarted {
		time.Sleep(time.Millisecond * 100)
	}

	stoppingClient <- true
	<-clientStopped

	self.status.Set(ModeStopped)
}